gitstack

grindlemire/go-tui code browser

2.4 KB Go 103 lines 2026-06-23 · 2a4aa99 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
package tui

// bufferRowToANSI converts a single row of buffer cells to an ANSI-escaped
// string suitable for direct terminal output. Trailing empty cells are trimmed.
// The caller's escBuilder is reused to minimize allocations across rows.
func bufferRowToANSI(buf *Buffer, row int, esc *escBuilder, caps Capabilities) string {
	width := buf.Width()
	if width == 0 {
		return ""
	}

	// Find rightmost non-empty cell (trim point).
	trimEnd := -1
	for x := width - 1; x >= 0; x-- {
		c := buf.Cell(x, row)
		if !c.IsEmpty() && !c.IsContinuation() {
			trimEnd = x
			break
		}
	}
	if trimEnd < 0 {
		return ""
	}

	esc.Reset()

	var prevStyle Style
	styleSet := false
	openLink := "" // currently-open OSC 8 hyperlink ("" = none)

	for x := 0; x <= trimEnd; x++ {
		c := buf.Cell(x, row)

		// Skip continuation cells of wide characters.
		if c.IsContinuation() {
			continue
		}

		// Open/close OSC 8 hyperlinks around contiguous same-link runs.
		if caps.Hyperlinks {
			openLink = linkTransition(esc, openLink, c.Link)
		}

		// Emit style change if needed.
		if !styleSet || !c.Style.Equal(prevStyle) {
			if c.Style.Equal(NewStyle()) {
				esc.ResetStyle()
			} else {
				esc.SetStyle(c.Style, caps)
			}
			prevStyle = c.Style
			styleSet = true
		}

		// Emit the cluster glyph (empty cell renders as a space).
		r := c.Rune
		if r == 0 {
			r = ' '
		}
		esc.WriteRune(r)
		if c.Combining != "" {
			esc.WriteString(c.Combining)
		}
	}

	// Close any open hyperlink before resetting style.
	if caps.Hyperlinks {
		linkTransition(esc, openLink, "")
	}

	// Reset at end so styling doesn't bleed.
	esc.ResetStyle()

	return string(esc.Bytes())
}

// renderElementToBuffer lays out and renders an element tree into a standalone
// buffer. Returns the buffer and its height. Returns (nil, 0) if the element
// has no renderable content. The element does not need to be attached to an
// App — this is a standalone render for baking elements into ANSI text.
func renderElementToBuffer(el *Element, width int, caps Capabilities) (*Buffer, int) {
	if el == nil || width <= 0 {
		return nil, 0
	}

	// Ensure layout runs from scratch.
	el.MarkDirty()

	// Compute natural height for the given width.
	height := el.HeightForWidth(width)
	if height <= 0 {
		return nil, 0
	}

	// Run full flexbox layout.
	Calculate(el, width, height)

	// Render to a throwaway buffer.
	buf := NewBuffer(width, height)
	RenderTree(buf, el)

	return buf, height
}