gitstack

grindlemire/go-tui code browser

3.7 KB Go 141 lines 2026-06-17 · 7d3f10d raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package tui

import "io"

// StreamWriter wraps an inner stream writer with convenience methods for
// styled and gradient-colored output. It implements io.WriteCloser for
// backward compatibility with code that used the previous io.WriteCloser
// return type of App.StreamAbove().
type StreamWriter struct {
	w     io.WriteCloser // inner writer (inlineStreamWriter or nopStreamWriter)
	app   *App           // app reference for WriteElement
	col   int            // current column for gradient position
	width int            // terminal width (cached at creation)
	esc   escBuilder     // reusable escape sequence builder
	caps  Capabilities   // terminal capabilities
	nop   bool           // true when not in inline mode
}

// Write delegates to the inner writer. Plain Write does not track column
// position since it handles raw bytes that may contain ANSI sequences.
func (sw *StreamWriter) Write(p []byte) (int, error) {
	return sw.w.Write(p)
}

// Close delegates to the inner writer.
func (sw *StreamWriter) Close() error {
	return sw.w.Close()
}

// WriteStyled writes text wrapped in the style's ANSI prefix and a reset
// suffix. Uses escBuilder.SetStyle for capability-aware rendering.
// Advances the internal column counter by the display width of the text.
func (sw *StreamWriter) WriteStyled(text string, style Style) (int, error) {
	if sw.nop {
		return len(text), nil
	}

	sw.esc.Reset()
	sw.esc.SetStyle(style, sw.caps)
	sw.esc.WriteString(text)
	sw.esc.ResetStyle()

	// Track column position by cluster width.
	rest := text
	for len(rest) > 0 {
		if rest[0] == '\n' {
			rest = rest[1:]
			sw.col = 0
			continue
		}
		_, cw, size := nextCluster(rest)
		if size == 0 {
			break
		}
		rest = rest[size:]
		sw.col += cw
		if sw.width > 0 && sw.col >= sw.width {
			sw.col = 0
		}
	}

	return sw.w.Write(sw.esc.Bytes())
}

// WriteGradient writes each character with an interpolated gradient foreground
// color. An optional base style provides additional attributes (bold, italic,
// etc.) and background color. The gradient color replaces the base style's
// foreground.
//
// Column position is tracked internally and wraps at the terminal width.
// Newlines reset the column to 0. The entire styled output is built in a
// single buffer and written in one call to the inner writer.
func (sw *StreamWriter) WriteGradient(text string, g Gradient, base ...Style) (int, error) {
	if sw.nop {
		return len(text), nil
	}

	var baseStyle Style
	if len(base) > 0 {
		baseStyle = base[0]
	}

	sw.esc.Reset()

	rest := text
	for len(rest) > 0 {
		if rest[0] == '\n' {
			rest = rest[1:]
			sw.esc.ResetStyle()
			sw.esc.WriteRune('\n')
			sw.col = 0
			continue
		}

		// Emit one cluster with a single gradient color for the whole cluster.
		cluster, cw, size := nextCluster(rest)
		if size == 0 {
			break
		}
		rest = rest[size:]

		// Compute gradient position from the cluster's starting column.
		w := sw.width
		if w < 1 {
			w = 80
		}
		t := float64(sw.col) / float64(w-1)
		if t > 1 {
			t = 1
		}

		charStyle := baseStyle
		charStyle.Fg = g.At(t)
		sw.esc.SetStyle(charStyle, sw.caps)
		sw.esc.WriteString(cluster)

		sw.col += cw
		if sw.width > 0 && sw.col >= sw.width {
			sw.col = 0
		}
	}

	sw.esc.ResetStyle()

	return sw.w.Write(sw.esc.Bytes())
}

// WriteElement renders a Viewable and inserts the resulting rows into the
// inline scrollback mid-stream. Any current partial line is finalized before
// the element rows are inserted. After insertion, the next Write call starts
// a fresh partial line.
// No-op if the writer is in nop mode or the app is nil.
func (sw *StreamWriter) WriteElement(v Viewable) {
	if sw.nop || sw.app == nil {
		return
	}
	sw.app.QueueUpdate(func() {
		sw.app.PrintAboveElement(v)
	})
	sw.col = 0
}