v0.1.1 7141aff

rightbar_test.go RawHistoryFolder

1package tui
2
3import (
4	"bytes"
5	"strings"
6	"testing"
7)
8
9func TestRightBarColumnsFallsBackWhenMainPaneWouldBeTooNarrow(t *testing.T) {
10	if _, _, ok := RightBarColumns(72); ok {
11		t.Fatal("right bar should fall back at 72 columns")
12	}
13	main, rail, ok := RightBarColumns(73)
14	if !ok {
15		t.Fatal("right bar should fit at 73 columns")
16	}
17	if main < RightBarMinMainWidth || rail < RightBarMinWidth || rail > RightBarMaxWidth {
18		t.Fatalf("right bar columns = main %d, rail %d", main, rail)
19	}
20	if main+RightBarSeparatorWidth+rail != 73 {
21		t.Fatalf("column budget = %d, want 73", main+RightBarSeparatorWidth+rail)
22	}
23}
24
25func TestRenderRightBarSortsWidgetsAndBoundsRows(t *testing.T) {
26	lines := RenderRightBar(Dark, []RightBarWidget{
27		{Extension: "zeta", ID: "second", Title: "Z", Lines: []string{"z"}},
28		{Extension: "alpha", ID: "first", Title: "A", Lines: []string{"a"}},
29	}, 24, 7)
30	if len(lines) != 7 {
31		t.Fatalf("right bar rows = %d, want 7", len(lines))
32	}
33	for idx, line := range lines {
34		if width := visibleWidth(line); width != 24 {
35			t.Fatalf("right bar line %d width = %d, want 24: %q", idx, width, line)
36		}
37	}
38	joined := stripANSI(strings.Join(lines, "\n"))
39	if strings.Index(joined, "[alpha] A") > strings.Index(joined, "[zeta] Z") {
40		t.Fatalf("widgets are not deterministic by extension: %q", joined)
41	}
42	if strings.ContainsAny(joined, "┌┐└┘─") {
43		t.Fatalf("right bar added an outer border: %q", joined)
44	}
45
46	truncated := RenderRightBar(Dark, []RightBarWidget{
47		{Extension: "alpha", ID: "large", Lines: []string{"a", "b", "c", "d", "e", "f", "g", "h"}},
48	}, 24, 7)
49	if !strings.Contains(stripANSI(strings.Join(truncated, "\n")), "right bar") {
50		t.Fatalf("truncation marker missing: %q", truncated)
51	}
52}
53
54func TestRenderRightBarClipsLongWidgetLinesWithEllipsis(t *testing.T) {
55	lines := RenderRightBar(Dark, []RightBarWidget{{
56		Extension: "plan",
57		Lines:     []string{"one two three four five six"},
58	}}, 16, 8)
59
60	if len(lines) != 8 {
61		t.Fatalf("right bar rows = %d, want 8", len(lines))
62	}
63	plain := stripANSI(strings.Join(lines, "\n"))
64	if !strings.Contains(plain, " one two thre...") || strings.Contains(plain, "four five six") {
65		t.Fatalf("long widget line was not clipped with an ellipsis: %q", plain)
66	}
67	for idx, line := range lines {
68		if width := visibleWidth(line); width != 16 {
69			t.Fatalf("clipped right bar line %d width = %d, want 16: %q", idx, width, line)
70		}
71	}
72}
73
74func TestRenderRightBarPreservesPhaseProgressWhenTitleIsClipped(t *testing.T) {
75	lines := RenderRightBar(Dark, []RightBarWidget{
76		{
77			Extension: "tasked-phases",
78			Lines:     []string{"[ ] A phase title that is much too long to fit  0/3"},
79		},
80	}, 24, 5)
81	plainLines := strings.Split(stripANSI(strings.Join(lines, "\n")), "\n")
82	if len(plainLines) < 3 {
83		t.Fatalf("phase row is missing: %q", plainLines)
84	}
85	phase := strings.TrimRight(plainLines[2], " ")
86	if !strings.HasSuffix(phase, "0/3") || !strings.Contains(phase, "...  0/3") {
87		t.Fatalf("phase progress was clipped with its title: %q", phase)
88	}
89}
90
91func TestRenderRightBarUsesChecklistIndentEllipsisAndBoldPhases(t *testing.T) {
92	lines := RenderRightBar(Dark, []RightBarWidget{{
93		Extension: "tasked-phases",
94		Title:     "p 0/1 | t 0/1",
95		Lines: []string{
96			"[>] Copper Lantern Setup  0/1",
97			" [ ] Sort the leftover star stickers",
98		},
99	}}, 36, 8)
100	plain := stripANSI(strings.Join(lines, "\n"))
101	if !strings.Contains(plain, " [>] Copper Lantern Setup  0/1") {
102		t.Fatalf("phase row has the wrong indentation: %q", plain)
103	}
104	plainLines := strings.Split(plain, "\n")
105	if len(plainLines) < 4 {
106		t.Fatalf("checklist rows are missing: %q", plain)
107	}
108	expectedTask := truncateRightBarLine("  [ ] Sort the leftover star stickers", 36)
109	if got := strings.TrimRight(plainLines[3], " "); got != expectedTask {
110		t.Fatalf("task row = %q, want clipped row %q", got, expectedTask)
111	}
112	if !strings.Contains(strings.Join(lines, "\n"), Bold("Copper Lantern Setup")) {
113		t.Fatalf("phase name is not bold: %q", lines)
114	}
115	if strings.Contains(strings.Join(lines, "\n"), Bold("Sort the leftover")) {
116		t.Fatalf("task text should not be bold: %q", lines)
117	}
118	if len(plainLines) < 2 || !strings.HasPrefix(plainLines[0], "[tasked-phases] p 0/1 | t 0/1") || strings.TrimSpace(plainLines[1]) != "" {
119		t.Fatalf("widget title is not followed by an empty row: %q", plain)
120	}
121}
122
123func TestRenderRightBarDimsChecklistMarkers(t *testing.T) {
124	lines := RenderRightBar(Dark, []RightBarWidget{{
125		Extension: "plan",
126		Lines:     []string{"[x] done", "[>] active", "[ ] pending"},
127	}}, 24, 8)
128	rendered := strings.Join(lines, "\n")
129	for _, marker := range []string{"[x]", "[>]", "[ ]"} {
130		if !strings.Contains(rendered, Dark.FGColor(Dark.Muted, marker)) &&
131			!strings.Contains(rendered, Dark.FGColor(Dark.Muted, Dim(marker))) {
132			t.Fatalf("marker %q was not muted: %q", marker, rendered)
133		}
134	}
135	if !strings.Contains(stripANSI(rendered), " done") {
136		t.Fatalf("checklist text was not kept readable: %q", rendered)
137	}
138}
139
140func TestRenderRightBarDimsInactiveChecklistSections(t *testing.T) {
141	lines := RenderRightBar(Dark, []RightBarWidget{{
142		Extension: "plan",
143		Lines: []string{
144			"[x] finished phase",
145			"  [x] finished task",
146			"[>] active phase",
147			"  [>] active task",
148			"[ ] pending phase",
149			"  [ ] pending task",
150		},
151	}}, 32, 12)
152	for _, line := range lines {
153		if strings.Contains(stripANSI(line), "active phase") || strings.Contains(stripANSI(line), "active task") {
154			if strings.Contains(line, "\x1b[2m") {
155				t.Fatalf("active checklist row was dimmed: %q", line)
156			}
157		}
158	}
159	plain := stripANSI(strings.Join(lines, "\n"))
160	for _, text := range []string{"finished phase", "finished task", "pending phase", "pending task"} {
161		if !strings.Contains(plain, text) {
162			t.Fatalf("inactive checklist text %q missing: %q", text, plain)
163		}
164	}
165	if !strings.Contains(strings.Join(lines, "\n"), "\x1b[2m") {
166		t.Fatalf("inactive checklist rows were not dimmed: %q", lines)
167	}
168}
169
170func TestJoinRightBarUsesOnlyMutedSeparator(t *testing.T) {
171	line := JoinRightBar(Dark, "main", "right", 8, 8)
172	if !strings.Contains(line, Dark.FGColor(Dark.Muted, "│")) {
173		t.Fatalf("separator is not muted: %q", line)
174	}
175	if strings.ContainsAny(stripANSI(line), "┌┐└┘─") {
176		t.Fatalf("joined right bar added an outer rule: %q", line)
177	}
178}
179
180func TestJoinRightBarPreservesCellBudgetForUnicode(t *testing.T) {
181	line := JoinRightBar(Dark, "界", "✓ done", 8, 6)
182	if got, want := visibleWidth(line), 8+RightBarSeparatorWidth+6; got != want {
183		t.Fatalf("joined width = %d, want %d: %q", got, want, line)
184	}
185}
186
187func TestRendererDrawRightBarToDrawLogKeepsScrollback(t *testing.T) {
188	t.Setenv("TERM_PROGRAM", "")
189
190	var out bytes.Buffer
191	r := NewRenderer(&out)
192	r.SetTheme(Dark)
193	r.Resize(80, 5)
194	r.DrawRightBar(
195		[]string{"chat"},
196		[]string{"input"},
197		RenderRightBar(Dark, []RightBarWidget{{Extension: "plan", Title: "Plan", Lines: []string{"task"}}}, 26, 5),
198		0,
199		0,
200	)
201
202	out.Reset()
203	r.DrawLog([]string{"chat"}, []string{"input"}, 0, 0)
204	if !strings.Contains(stripANSI(out.String()), "input") {
205		t.Fatalf("right-bar fallback did not repaint flow content: %q", out.String())
206	}
207	if strings.Contains(out.String(), SeqClearScrollback) {
208		t.Fatalf("right-bar fallback cleared terminal scrollback: %q", out.String())
209	}
210}
211
212func TestRendererDrawRightBarUpdatesClearsAndResizes(t *testing.T) {
213	var out bytes.Buffer
214	r := NewRenderer(&out)
215	r.SetTheme(Dark)
216	r.Resize(80, 5)
217	r.DrawRightBar(
218		[]string{"chat"},
219		[]string{"input"},
220		RenderRightBar(Dark, []RightBarWidget{{Extension: "plan", Title: "Old", Lines: []string{"first"}}}, 26, 5),
221		0,
222		0,
223	)
224	if !strings.Contains(stripANSI(out.String()), "Old") {
225		t.Fatalf("initial right-bar frame omitted widget: %q", out.String())
226	}
227
228	out.Reset()
229	r.DrawRightBar(
230		[]string{"chat"},
231		[]string{"input"},
232		RenderRightBar(Dark, []RightBarWidget{{Extension: "plan", Title: "Updated", Lines: []string{"second"}}}, 26, 5),
233		0,
234		0,
235	)
236	if !strings.Contains(stripANSI(out.String()), "Updated") {
237		t.Fatalf("right-bar update did not repaint changed content: %q", out.String())
238	}
239	if r.logInit {
240		t.Fatal("right-bar mode left flow renderer initialized")
241	}
242
243	out.Reset()
244	r.DrawLog([]string{"chat"}, []string{"input"}, 0, 0)
245	if !r.logInit {
246		t.Fatal("flow renderer was not reinitialized after right-bar clearing")
247	}
248
249	out.Reset()
250	r.Resize(100, 6)
251	if out.Len() == 0 {
252		t.Fatal("resize did not invalidate the right-bar frame")
253	}
254	main, rail, ok := RightBarColumns(100)
255	if !ok {
256		t.Fatal("right bar should fit after resize")
257	}
258	if main+RightBarSeparatorWidth+rail != 100 {
259		t.Fatalf("resized column budget = %d", main+RightBarSeparatorWidth+rail)
260	}
261}

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 1

Tags 476

↑↓ moveenter openesc close