okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Kieran Klukasfix(renderer): keep the cursor model honest about width disagreementsb2b0f8d

upstream
7.3 KiB196 linesraw
1package uv
2
3import (
4	"bytes"
5	"strings"
6	"testing"
7)
8
9func TestRendererOutput(t *testing.T) {
10	cases := []struct {
11		name      string
12		input     []string
13		wrap      []bool
14		relative  bool
15		altscreen bool
16		expected  []string
17	}{
18		{
19			name:     "scroll to bottom in inline mode",
20			input:    []string{"ABC", "XXX"},
21			expected: []string{"\rABC", "\rXXX"},
22			relative: true,
23		},
24		{
25			name: "scroll one line",
26			input: []string{
27				loremIpsum[0],
28				loremIpsum[0][10:],
29			},
30			wrap: []bool{
31				true,
32				true,
33			},
34			expected: func() []string {
35				if isWindows {
36					return []string{
37						"\x1b[H\x1b[2JLorem ipsu\r\nm dolor si\r\nt amet, co\r\nnsectetur\r\nadipiscin\x1b[?7lg\x1b[?7h",
38						"\x1b[Hm dolor si\r\nt amet, co\r\nnsectetur\x1b[K\r\nadipiscing\r\n elit. Vi\x1b[?7lv\x1b[?7h",
39					}
40				} else {
41					return []string{
42						"\x1b[H\x1b[2JLorem ipsu\r\nm dolor si\r\nt amet, co\r\nnsectetur\r\nadipiscin\x1b[?7lg\x1b[?7h",
43						"\r\n elit. Vi\x1b[?7lv\x1b[?7h",
44					}
45				}
46			}(),
47			altscreen: true,
48		},
49		{
50			name: "scroll two lines",
51			input: []string{
52				loremIpsum[0],
53				loremIpsum[0][20:],
54			},
55			wrap: []bool{
56				true,
57				true,
58			},
59			expected: func() []string {
60				if isWindows {
61					return []string{
62						"\x1b[H\x1b[2JLorem ipsu\r\nm dolor si\r\nt amet, co\r\nnsectetur\r\nadipiscin\x1b[?7lg\x1b[?7h",
63						"\x1b[Ht amet, co\r\nnsectetur\x1b[K\r\nadipiscing\r\n elit. Viv\r\namus at o\x1b[?7lr\x1b[?7h",
64					}
65				} else {
66					return []string{
67						"\x1b[H\x1b[2JLorem ipsu\r\nm dolor si\r\nt amet, co\r\nnsectetur\r\nadipiscin\x1b[?7lg\x1b[?7h",
68						"\r\x1b[2S\x1bM elit. Viv\r\namus at o\x1b[?7lr\x1b[?7h",
69					}
70				}
71			}(),
72			altscreen: true,
73		},
74		{
75			name: "insert line in the middle",
76			input: []string{
77				"ABC\nDEF\nGHI\n",
78				"ABC\n\nDEF\nGHI",
79			},
80			wrap: []bool{
81				true,
82				true,
83			},
84			expected: func() []string {
85				if isWindows {
86					return []string{
87						"\x1b[H\x1b[2JABC\r\nDEF\r\nGHI",
88						"\r\x1bM\x1b[K\nDEF\r\nGHI",
89					}
90				} else {
91					return []string{
92						"\x1b[H\x1b[2JABC\r\nDEF\r\nGHI",
93						"\r\x1bM\x1b[L",
94					}
95				}
96			}(),
97			altscreen: true,
98		},
99		{
100			name: "erase until end of line",
101			input: []string{
102				"\nABCEFGHIJK",
103				"\nABCE      ",
104			},
105			expected: []string{
106				"\x1b[2;1HABCEFGHIJK",
107				"\r\x1b[5G\x1b[K",
108			},
109		},
110	}
111
112	for _, c := range cases {
113		t.Run(c.name, func(t *testing.T) {
114			var buf bytes.Buffer
115			s := NewTerminalRenderer(&buf, []string{
116				"TERM=xterm-256color", // Enable 256 colors
117				"COLORTERM=truecolor", // Enable true color support
118			})
119
120			s.SetScrollOptim(!isWindows) // Disable scroll optimization on Windows for consistent results
121			s.SetFullscreen(c.altscreen)
122			s.SetRelativeCursor(c.relative)
123			if c.altscreen {
124				s.SaveCursor()
125				s.Erase()
126			}
127
128			scr := NewScreenBuffer(10, 5)
129			for i := range c.input {
130				buf.Reset()
131
132				comp := NewStyledString(c.input[i])
133				if i < len(c.wrap) {
134					comp.Wrap = c.wrap[i]
135				}
136				comp.Draw(scr, scr.Bounds())
137				s.Render(scr.RenderBuffer)
138				if err := s.Flush(); err != nil {
139					t.Fatalf("Flush failed: %v", err)
140				}
141
142				if buf.String() != c.expected[i] {
143					t.Errorf("Expected output[%d]:\n%q\nGot:\n%q", i, c.expected[i], buf.String())
144				}
145			}
146		})
147	}
148}
149
150func TestRendererWideCellReanchor(t *testing.T) {
151	render := func(grapheme bool) string {
152		var buf bytes.Buffer
153		s := NewTerminalRenderer(&buf, []string{
154			"TERM=xterm-256color",
155			"COLORTERM=truecolor",
156		})
157		s.SetFullscreen(true)
158		s.SetGraphemeWidth(grapheme)
159		s.SaveCursor()
160		s.Erase()
161
162		scr := NewScreenBuffer(10, 1)
163		buf.Reset()
164		NewStyledString("δΈ–η•Œ").Draw(scr, scr.Bounds())
165		s.Render(scr.RenderBuffer)
166		if err := s.Flush(); err != nil {
167			t.Fatalf("Flush failed: %v", err)
168		}
169		return buf.String()
170	}
171
172	// The re-anchor is a full absolute move, not a column-only one: a terminal
173	// that measured the clusters wider than the model may have wrapped the
174	// line, which moves the row as well.
175	out := render(false)
176	reanchors := strings.Count(out, "\x1b[1;5H")
177	if reanchors != 1 {
178		t.Errorf("non-grapheme line with wide cells: want 1 re-anchor, got %d in %q", reanchors, out)
179	}
180	if perCell := strings.Count(out, "\x1b[1;3H"); perCell != 0 {
181		t.Errorf("adjacent wide cells emitted a per-cell re-anchor: %q", out)
182	}
183
184	gout := render(true)
185	if n := strings.Count(gout, "\x1b[1;5H"); n != 0 {
186		t.Errorf("grapheme-mode line should not re-anchor, got %d in %q", n, gout)
187	}
188}
189
190var loremIpsum = []string{
191	"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at ornare risus, quis lacinia magna. Suspendisse egestas purus risus, id rutrum diam porta non. Duis luctus tempus dictum. Maecenas luctus metus vitae nulla consectetur egestas. Curabitur faucibus nunc vel eros semper scelerisque. Proin dictum aliquam lacus dignissim fringilla. Praesent ut quam id dui aliquam vehicula in vitae orci. Fusce imperdiet aliquam quam. Nullam euismod magna tincidunt nisl ullamcorper, dignissim rutrum arcu rutrum. Nulla ac fringilla velit. Duis non pellentesque erat.",
192	"In egestas ex et sem vulputate, congue bibendum diam ultrices. Nam auctor dictum enim, in rutrum nulla vestibulum sit amet. Vestibulum vel velit ac sem pellentesque accumsan. Vivamus pharetra mi non arcu tristique gravida. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed molestie lectus nunc, sit amet rhoncus orci laoreet vel. Nulla eget mattis massa. Nunc porta eros sollicitudin lorem dapibus luctus. Vestibulum ut turpis ut nibh tincidunt feugiat. Integer eget augue nunc. Morbi vitae ultrices neque. Nulla et convallis libero. Cras nec faucibus odio. Maecenas lacinia sed odio sit amet ultrices.",
193	"Nunc at molestie massa. Phasellus commodo dui odio, quis pulvinar orci eleifend a. In et erat nec nisl auctor facilisis at at orci. Curabitur ut ligula in ipsum consequat consectetur. Suspendisse pulvinar arcu metus, et faucibus risus interdum pharetra. Vestibulum vulputate, arcu at malesuada varius, nisl turpis molestie risus, ut lobortis dolor neque vitae diam. Donec lectus libero, iaculis non diam sit amet, sagittis mattis lectus. Vestibulum a magna molestie neque molestie faucibus sagittis et ante. Etiam porta tincidunt nisi sit amet blandit. Vivamus et tellus diam. Vivamus id dolor placerat, tristique magna non, congue est. Nulla a condimentum nulla. Fusce maximus semper nunc, at bibendum mi. Nam malesuada vitae mi molestie tincidunt. Pellentesque sed vestibulum lectus, eu ultrices ligula. Phasellus id nibh tristique, ultricies diam vel, cursus odio.",
194	"Integer sed mi viverra, convallis urna congue, efficitur libero. Duis non eros commodo, ultricies quam hendrerit, molestie velit. Nunc non eros vitae lectus hendrerit gravida. Nunc lacinia neque sapien, et accumsan orci elementum vel. Praesent vel interdum nisl. Duis eget diam turpis. Nunc gravida, lacus dictum congue pharetra, dui est laoreet massa, ac convallis elit est sed dui. Morbi luctus convallis dui id tristique.",
195	"Praesent vitae laoreet risus. Sed ac facilisis justo. Morbi fringilla in est vel volutpat. Aliquam erat tortor, posuere ac libero sit amet, vehicula blandit sapien. Nullam feugiat purus eget sapien bibendum, id posuere risus finibus. Aliquam erat volutpat. Pellentesque ac purus accumsan, accumsan mi vel, viverra lectus. Ut sed porta erat, vitae mollis nibh. Nunc dignissim quis tellus sed blandit. Mauris id velit in odio commodo aliquet.",
196}