v0.10.0 8a8e2b3

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 thr...") || 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 TestRenderRightBarLeavesOneCellOfRightPadding(t *testing.T) {
75	lines := RenderRightBar(Dark, []RightBarWidget{{
76		Extension: "tasked-phases",
77		Title:     "p 0/1 | t 0/1",
78		Lines:     []string{"[>] A phase title that is much too long to fit  0/1"},
79	}}, 24, 4)
80
81	for idx, line := range lines {
82		plain := stripANSI(line)
83		if !strings.HasSuffix(plain, " ") {
84			t.Fatalf("right bar line %d lacks right padding: %q", idx, plain)
85		}
86		if visibleWidth(strings.TrimRight(plain, " ")) > 23 {
87			t.Fatalf("right bar line %d content reaches the right edge: %q", idx, plain)
88		}
89	}
90}
91
92func TestRenderRightBarPreservesPhaseProgressWhenTitleIsClipped(t *testing.T) {
93	lines := RenderRightBar(Dark, []RightBarWidget{
94		{
95			Extension: "tasked-phases",
96			Lines:     []string{"[ ] A phase title that is much too long to fit  0/3"},
97		},
98	}, 24, 5)
99	plainLines := strings.Split(stripANSI(strings.Join(lines, "\n")), "\n")
100	if len(plainLines) < 3 {
101		t.Fatalf("phase row is missing: %q", plainLines)
102	}
103	phase := strings.TrimRight(plainLines[2], " ")
104	if !strings.HasSuffix(phase, "0/3") || !strings.Contains(phase, "...  0/3") {
105		t.Fatalf("phase progress was clipped with its title: %q", phase)
106	}
107}
108
109func TestTruncateRightBarPhaseLinePreservesProgressWithoutRoomForName(t *testing.T) {
110	got := truncateRightBarPhaseLine("[>] Current phase  0/1", 9)
111	if want := "[>]   0/1"; got != want {
112		t.Fatalf("truncated phase = %q, want %q", got, want)
113	}
114}
115
116func TestRenderRightBarUsesChecklistIndentEllipsisAndBoldPhases(t *testing.T) {
117	lines := RenderRightBar(Dark, []RightBarWidget{{
118		Extension: "tasked-phases",
119		Title:     "p 0/1 | t 0/1",
120		Lines: []string{
121			"[>] Copper Lantern Setup  0/1",
122			" [ ] Sort the leftover star stickers",
123		},
124	}}, 36, 8)
125	plain := stripANSI(strings.Join(lines, "\n"))
126	if !strings.Contains(plain, " [>] Copper Lantern Setup  0/1") {
127		t.Fatalf("phase row has the wrong indentation: %q", plain)
128	}
129	plainLines := strings.Split(plain, "\n")
130	if len(plainLines) < 4 {
131		t.Fatalf("checklist rows are missing: %q", plain)
132	}
133	const expectedTask = "  [ ] Sort the leftover star sti..."
134	if got := strings.TrimRight(plainLines[3], " "); got != expectedTask {
135		t.Fatalf("task row = %q, want clipped row %q", got, expectedTask)
136	}
137	if !strings.Contains(strings.Join(lines, "\n"), Bold("Copper Lantern Setup")) {
138		t.Fatalf("phase name is not bold: %q", lines)
139	}
140	if strings.Contains(strings.Join(lines, "\n"), Bold("Sort the leftover")) {
141		t.Fatalf("task text should not be bold: %q", lines)
142	}
143	if len(plainLines) < 2 || !strings.HasPrefix(plainLines[0], "[tasked-phases] p 0/1 | t 0/1") || strings.TrimSpace(plainLines[1]) != "" {
144		t.Fatalf("widget title is not followed by an empty row: %q", plain)
145	}
146}
147
148func TestRenderRightBarDimsChecklistMarkers(t *testing.T) {
149	lines := RenderRightBar(Dark, []RightBarWidget{{
150		Extension: "plan",
151		Lines:     []string{"[x] done", "[>] active", "[ ] pending"},
152	}}, 24, 8)
153	rendered := strings.Join(lines, "\n")
154	for _, marker := range []string{"[x]", "[>]", "[ ]"} {
155		if !strings.Contains(rendered, Dark.FGColor(Dark.Muted, marker)) &&
156			!strings.Contains(rendered, Dark.FGColor(Dark.Muted, Dim(marker))) {
157			t.Fatalf("marker %q was not muted: %q", marker, rendered)
158		}
159	}
160	if !strings.Contains(stripANSI(rendered), " done") {
161		t.Fatalf("checklist text was not kept readable: %q", rendered)
162	}
163}
164
165func TestRenderRightBarDimsInactiveChecklistSections(t *testing.T) {
166	lines := RenderRightBar(Dark, []RightBarWidget{{
167		Extension: "plan",
168		Lines: []string{
169			"[x] finished phase",
170			"  [x] finished task",
171			"[>] active phase",
172			"  [>] active task",
173			"[ ] pending phase",
174			"  [ ] pending task",
175		},
176	}}, 32, 12)
177	for _, line := range lines {
178		if strings.Contains(stripANSI(line), "active phase") || strings.Contains(stripANSI(line), "active task") {
179			if strings.Contains(line, "\x1b[2m") {
180				t.Fatalf("active checklist row was dimmed: %q", line)
181			}
182		}
183	}
184	plain := stripANSI(strings.Join(lines, "\n"))
185	for _, text := range []string{"finished phase", "finished task", "pending phase", "pending task"} {
186		if !strings.Contains(plain, text) {
187			t.Fatalf("inactive checklist text %q missing: %q", text, plain)
188		}
189	}
190	if !strings.Contains(strings.Join(lines, "\n"), "\x1b[2m") {
191		t.Fatalf("inactive checklist rows were not dimmed: %q", lines)
192	}
193}
194
195func TestJoinRightBarUsesOnlyMutedSeparator(t *testing.T) {
196	line := JoinRightBar(Dark, "main", "right", 8, 8)
197	if !strings.Contains(line, Dark.FGColor(Dark.Muted, "│")) {
198		t.Fatalf("separator is not muted: %q", line)
199	}
200	if strings.ContainsAny(stripANSI(line), "┌┐└┘─") {
201		t.Fatalf("joined right bar added an outer rule: %q", line)
202	}
203}
204
205func TestJoinRightBarPreservesCellBudgetForUnicode(t *testing.T) {
206	line := JoinRightBar(Dark, "界", "✓ done", 8, 6)
207	if got, want := visibleWidth(line), 8+RightBarSeparatorWidth+6; got != want {
208		t.Fatalf("joined width = %d, want %d: %q", got, want, line)
209	}
210}
211
212func TestRendererDrawRightBarToDrawLogKeepsScrollback(t *testing.T) {
213	t.Setenv("TERM_PROGRAM", "")
214
215	var out bytes.Buffer
216	r := NewRenderer(&out)
217	r.SetTheme(Dark)
218	r.Resize(80, 5)
219	r.DrawRightBar(
220		[]string{"chat"},
221		[]string{"input"},
222		RenderRightBar(Dark, []RightBarWidget{{Extension: "plan", Title: "Plan", Lines: []string{"task"}}}, 26, 5),
223		0,
224		0,
225	)
226
227	out.Reset()
228	r.DrawLog([]string{"chat"}, []string{"input"}, 0, 0)
229	if !strings.Contains(stripANSI(out.String()), "input") {
230		t.Fatalf("right-bar fallback did not repaint flow content: %q", out.String())
231	}
232	if strings.Contains(out.String(), SeqClearScrollback) {
233		t.Fatalf("right-bar fallback cleared terminal scrollback: %q", out.String())
234	}
235}
236
237func TestRendererDrawRightBarUpdatesClearsAndResizes(t *testing.T) {
238	var out bytes.Buffer
239	r := NewRenderer(&out)
240	r.SetTheme(Dark)
241	r.Resize(80, 5)
242	r.DrawRightBar(
243		[]string{"chat"},
244		[]string{"input"},
245		RenderRightBar(Dark, []RightBarWidget{{Extension: "plan", Title: "Old", Lines: []string{"first"}}}, 26, 5),
246		0,
247		0,
248	)
249	if !strings.Contains(stripANSI(out.String()), "Old") {
250		t.Fatalf("initial right-bar frame omitted widget: %q", out.String())
251	}
252
253	out.Reset()
254	r.DrawRightBar(
255		[]string{"chat"},
256		[]string{"input"},
257		RenderRightBar(Dark, []RightBarWidget{{Extension: "plan", Title: "Updated", Lines: []string{"second"}}}, 26, 5),
258		0,
259		0,
260	)
261	if !strings.Contains(stripANSI(out.String()), "Updated") {
262		t.Fatalf("right-bar update did not repaint changed content: %q", out.String())
263	}
264	if r.logInit {
265		t.Fatal("right-bar mode left flow renderer initialized")
266	}
267
268	out.Reset()
269	r.DrawLog([]string{"chat"}, []string{"input"}, 0, 0)
270	if !r.logInit {
271		t.Fatal("flow renderer was not reinitialized after right-bar clearing")
272	}
273
274	out.Reset()
275	r.Resize(100, 6)
276	if out.Len() == 0 {
277		t.Fatal("resize did not invalidate the right-bar frame")
278	}
279	main, rail, ok := RightBarColumns(100)
280	if !ok {
281		t.Fatal("right bar should fit after resize")
282	}
283	if main+RightBarSeparatorWidth+rail != 100 {
284		t.Fatalf("resized column budget = %d", main+RightBarSeparatorWidth+rail)
285	}
286}
287
288func TestRendererDrawRightBarDimmedDimsSeparator(t *testing.T) {
289	var out bytes.Buffer
290	r := NewRenderer(&out)
291	r.SetTheme(Dark)
292	r.Resize(80, 5)
293
294	r.DrawRightBarDimmed(
295		DimLines([]string{"chat"}),
296		DimLines([]string{"input"}),
297		DimLines(RenderRightBar(Dark, []RightBarWidget{{Extension: "plan", Lines: []string{"task"}}}, 26, 5)),
298		0,
299		0,
300	)
301
302	separator := Dim(Dark.FGColor(Dark.Muted, "│"))
303	if !strings.Contains(out.String(), separator) {
304		t.Fatalf("dimmed right bar left its separator bright; missing %q in %q", separator, out.String())
305	}
306}

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