1package layout
2
3import (
4 "fmt"
5 "log/slog"
6 "testing"
7
8 "github.com/bnema/neferbar/internal/glyph"
9 "github.com/bnema/neferbar/internal/module"
10 "github.com/bnema/neferbar/internal/racecheck"
11)
12
13var testPalette = [16][3]uint8{{1, 1, 1}, {255, 0, 0}, {0, 255, 0}, {255, 255, 0}, {0, 0, 255}}
14
15func newTestLayout(cols int) (*Layout, []*module.Module) {
16 wake := make(chan struct{}, 1)
17 log := slog.New(slog.DiscardHandler)
18 mods := []*module.Module{
19 module.New("l", module.Left, "true", wake, log),
20 module.New("c", module.Center, "true", wake, log),
21 module.New("r", module.Right, "true", wake, log),
22 }
23 return New(cols, [3]uint8{200, 200, 200}, [3]uint8{10, 10, 10}, testPalette, mods), mods
24}
25
26func publish(m *module.Module, text string) { module.PublishForTest(m, []byte(text)) }
27
28func rowText(l *Layout) string {
29 row := l.Compose()
30 rs := make([]rune, len(row))
31 for i, c := range row {
32 rs[i] = c.Rune
33 }
34 return string(rs)
35}
36
37func TestZones(t *testing.T) {
38 l, m := newTestLayout(20)
39 publish(m[0], "LEFT")
40 publish(m[1], "mid")
41 publish(m[2], "RIGHT")
42 if !l.Update() {
43 t.Fatal("Update reported no change")
44 }
45 got := rowText(l)
46 want := "LEFT mid RIGHT"
47 if len(want) < 20 {
48 want += " "[:20-len(want)]
49 }
50 if got[:4] != "LEFT" || got[15:] != "RIGHT" {
51 t.Fatalf("row = %q", got)
52 }
53 if i := indexOf(got, "mid"); i < 4 || i > 15 {
54 t.Fatalf("center misplaced in %q", got)
55 }
56}
57
58func indexOf(s, sub string) int {
59 for i := 0; i+len(sub) <= len(s); i++ {
60 if s[i:i+len(sub)] == sub {
61 return i
62 }
63 }
64 return -1
65}
66
67func TestOverlapCutsCenterFirst(t *testing.T) {
68 l, m := newTestLayout(12)
69 publish(m[0], "AAAAA")
70 publish(m[1], "CCCCCCCC")
71 publish(m[2], "ZZZZZ")
72 l.Update()
73 got := rowText(l)
74 if got[:5] != "AAAAA" || got[7:] != "ZZZZZ" {
75 t.Fatalf("sides must survive: %q", got)
76 }
77}
78
79func TestColorsAndClip(t *testing.T) {
80 l, m := newTestLayout(6)
81 publish(m[0], "\x1b[31mR\x1b[0mx\x1b[1;38;2;1;2;3mverylongtext")
82 l.Update()
83 row := l.Compose()
84 if row[0].Rune != 'R' || row[0].FG == l.fg {
85 t.Fatalf("red cell: %+v", row[0])
86 }
87 if row[1].FG != l.fg {
88 t.Fatalf("reset should restore default fg: %+v", row[1])
89 }
90 if row[2].Style&glyph.Bold == 0 || row[2].FG != [3]uint8{1, 2, 3} {
91 t.Fatalf("bold truecolor cell: %+v", row[2])
92 }
93}
94
95func TestNewFrameReplacesOld(t *testing.T) {
96 l, m := newTestLayout(10)
97 publish(m[0], "long text")
98 l.Update()
99 publish(m[0], "ab")
100 l.Update()
101 if got := rowText(l); got[:2] != "ab" || got[2] != ' ' {
102 t.Fatalf("old text lingers: %q", got)
103 }
104}
105
106// TestUpdateComposeAllocs: parsing a frame and composing the row must not
107// allocate once the buffers are warm.
108func TestUpdateComposeAllocs(t *testing.T) {
109 if racecheck.Enabled {
110 t.Skip("the race detector allocates")
111 }
112 l, m := newTestLayout(100)
113 frames := make([]string, 8)
114 for i := range frames {
115 frames[i] = fmt.Sprintf("\x1b[48;2;%d;10;10m \x1b[0m clock %02d \x1b[1;32mok\x1b[0m \uf017", i*30, i)
116 }
117 n := 0
118 step := func() {
119 publish(m[2], frames[n%len(frames)])
120 n++
121 l.Update()
122 l.Compose()
123 }
124 for i := 0; i < 20; i++ {
125 step()
126 }
127 if got := testing.AllocsPerRun(500, step); got > 0 {
128 t.Errorf("Update+Compose allocates %.1f objects per frame; want 0", got)
129 }
130}
131
132func TestTextStyles(t *testing.T) {
133 l, m := newTestLayout(10)
134 publish(m[0], "\x1b[3mi\x1b[0m\x1b[4mu\x1b[0m\x1b[9ms\x1b[0m\x1b[1;3;4;9mA\x1b[0mn")
135 l.Update()
136 row := l.Compose()
137 want := []glyph.Style{glyph.Italic, glyph.Underline, glyph.Strike, glyph.Bold | glyph.Italic | glyph.Underline | glyph.Strike, 0}
138 for i, w := range want {
139 if row[i].Style != w {
140 t.Errorf("cell %d (%q): style %04b, want %04b", i, row[i].Rune, row[i].Style, w)
141 }
142 }
143}
144
145func TestAnsiColorsComeFromThePalette(t *testing.T) {
146 l, m := newTestLayout(6)
147 publish(m[0], "\x1b[31mr\x1b[44mb\x1b[0m\x1b[38;5;2mg")
148 l.Update()
149 row := l.Compose()
150 if row[0].FG != testPalette[1] {
151 t.Errorf("\\e[31m = %v, want palette[1] %v", row[0].FG, testPalette[1])
152 }
153 if row[1].BG != testPalette[4] {
154 t.Errorf("\\e[44m background = %v, want palette[4] %v", row[1].BG, testPalette[4])
155 }
156 if row[2].FG != testPalette[2] {
157 t.Errorf("\\e[38;5;2m = %v, want palette[2] %v", row[2].FG, testPalette[2])
158 }
159 pal := testPalette
160 pal[1] = [3]uint8{9, 9, 9}
161 l.SetColors(l.fg, l.bg, pal)
162 if got := l.Compose()[0].FG; got != pal[1] {
163 t.Errorf("after SetColors, \\e[31m = %v, want %v", got, pal[1])
164 }
165}
166
167// A frame that stops inside an escape sequence must not blank the frames after it.
168func TestBrokenEscapeDoesNotSwallowNextFrames(t *testing.T) {
169 for name, broken := range map[string]string{
170 "osc": "\x1b]0;title",
171 "csi": "\x1b[38;2;1",
172 "dcs": "\x1bP1;2",
173 "apc": "\x1b_Gf=24",
174 "esc": "\x1b",
175 "utf8": "ab\xe2\x82",
176 } {
177 l, m := newTestLayout(10)
178 publish(m[0], broken)
179 l.Update()
180 publish(m[0], "hello")
181 l.Update()
182 if got := rowText(l); got[:5] != "hello" {
183 t.Errorf("%s: row after the next frame = %q, want hello", name, got)
184 }
185 }
186}
187
188// The center zone sits on the middle of the bar, not on the middle of the
189// space left between a wide left zone and a narrow right one.
190func TestCenterIsOnTheMiddleOfTheBar(t *testing.T) {
191 l, m := newTestLayout(40)
192 publish(m[0], "LLLLLLLLLL") // 10 cells on the left
193 publish(m[1], "cccc") // 4 in the middle
194 publish(m[2], "RRRR") // 4 on the right
195 l.Update()
196 got := rowText(l)
197 if i := indexOf(got, "cccc"); i != 18 {
198 t.Fatalf("center starts at cell %d, want 18 (the bar is 40 cells wide): %q", i, got)
199 }
200}
201
202// When the middle of the bar is taken, the center moves just enough to fit.
203func TestCenterMovesOnlyAsFarAsNeeded(t *testing.T) {
204 l, m := newTestLayout(40)
205 publish(m[0], "LLLLLLLLLLLLLLLLLLLL") // 20 cells: reaches past the ideal start of 18
206 publish(m[1], "cccc")
207 publish(m[2], "RRRR")
208 l.Update()
209 got := rowText(l)
210 if i := indexOf(got, "cccc"); i != 20 {
211 t.Fatalf("center starts at cell %d, want 20 (right after the left zone): %q", i, got)
212 }
213 // And the other way round: a wide right zone pushes it left.
214 l, m = newTestLayout(40)
215 publish(m[0], "LL")
216 publish(m[1], "cccc")
217 publish(m[2], "RRRRRRRRRRRRRRRRRRRR") // 20 cells: the center must end by cell 20
218 l.Update()
219 got = rowText(l)
220 if i := indexOf(got, "cccc"); i != 16 {
221 t.Fatalf("center starts at cell %d, want 16 (ending where the right zone starts): %q", i, got)
222 }
223}