v0.2.0 c616855

layout_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 5

Tags 4

↑↓ moveenter openesc close