v0.29.0 73d6864

floating_pane_test.go RawHistoryFolder

1package tui
2
3import (
4	"strings"
5	"testing"
6)
7
8func TestFloatingPaneComposeCentersAndDimsLiveBackground(t *testing.T) {
9	var pane FloatingPane
10	frame, cursorRow, cursorCol := pane.Compose(Dark, "settings", "settings", []string{"live transcript", "draft input"}, []string{"settings body"}, 100, 30, -1, 0)
11
12	if frame.Rect.Drawer {
13		t.Fatal("100-column terminal unexpectedly used drawer layout")
14	}
15	if want := (100 - frame.Rect.Width) / 2; frame.Rect.X != want {
16		t.Fatalf("pane x = %d, want %d", frame.Rect.X, want)
17	}
18	if !strings.Contains(frame.Background[len(frame.Background)-2], Dim("live transcript")) {
19		t.Fatalf("live background was not dimmed: %q", frame.Background)
20	}
21	if got := stripANSI(frame.Lines[0]); !strings.HasPrefix(got, "╭") {
22		t.Fatalf("pane is missing top border: %q", got)
23	} else if title := strings.Index(got, "settings"); title < 0 {
24		t.Fatalf("pane title is missing from its border: %q", got)
25	} else {
26		left := strings.Count(got[:title], "─")
27		right := strings.Count(got[title+len("settings"):], "─")
28		if left-right > 1 || right-left > 1 {
29			t.Fatalf("pane title is not centered in its border: %q", got)
30		}
31	}
32	if cursorRow != -1 || cursorCol != 0 {
33		t.Fatalf("cursor = (%d, %d), want hidden", cursorRow, cursorCol)
34	}
35}
36
37func TestFloatingPaneUsesBottomDrawerBelowEightyColumns(t *testing.T) {
38	var pane FloatingPane
39	frame, _, _ := pane.Compose(Dark, "settings", "settings", nil, []string{"settings body"}, 79, 24, -1, 0)
40
41	if !frame.Rect.Drawer {
42		t.Fatal("79-column terminal did not use drawer layout")
43	}
44	if frame.Rect.X != 0 || frame.Rect.Width != 79 || frame.Rect.Y+frame.Rect.Height != 24 {
45		t.Fatalf("drawer geometry = %+v, want full-width bottom drawer", frame.Rect)
46	}
47	if got := stripANSI(frame.Lines[0]); !strings.Contains(got, "settings") {
48		t.Fatalf("drawer title = %q, want centered settings title", got)
49	}
50}
51
52func TestFloatingPaneRecomputesGeometryOnResize(t *testing.T) {
53	var pane FloatingPane
54	wide, _, _ := pane.Compose(Dark, "settings", "settings", nil, []string{"setting"}, 100, 30, -1, 0)
55	narrow, _, _ := pane.Compose(Dark, "settings", "settings", nil, []string{"setting"}, 79, 24, -1, 0)
56
57	if wide.Rect.Drawer {
58		t.Fatal("wide terminal unexpectedly used a drawer")
59	}
60	if !narrow.Rect.Drawer || narrow.Rect.Width != 79 || narrow.Rect.Y+narrow.Rect.Height != 24 {
61		t.Fatalf("resize did not recompute a bottom drawer: %+v", narrow.Rect)
62	}
63}
64
65func TestFloatingPaneClampsCursorToContentAndHandlesTinyTerminals(t *testing.T) {
66	var pane FloatingPane
67	frame, _, col := pane.Compose(Dark, "search", "search", nil, []string{"query"}, 100, 20, 0, 1000)
68	if want := frame.Rect.contentX() + frame.Rect.ContentWidth() - 1; col != want {
69		t.Fatalf("cursor col = %d, want content edge %d", col, want)
70	}
71
72	tiny, row, tinyCol := pane.Compose(Dark, "search", "search", nil, []string{"x"}, 2, 2, 0, 10)
73	if !tiny.Rect.Borderless {
74		t.Fatalf("tiny terminal rect = %+v, want borderless", tiny.Rect)
75	}
76	if row != 0 || tinyCol != 1 {
77		t.Fatalf("tiny cursor = (%d, %d), want (0, 1)", row, tinyCol)
78	}
79}
80
81func TestFloatingPaneKeepsFocusedContentVisibleAndResetsForOtherView(t *testing.T) {
82	content := make([]string, 30)
83	for row := range content {
84		content[row] = "row " + itoa(row)
85	}
86	var pane FloatingPane
87	frame, cursorRow, _ := pane.Compose(Dark, "settings", "settings", nil, content, 100, 12, 25, 0)
88	if cursorRow < frame.Rect.Y+1 || cursorRow >= frame.Rect.Y+frame.Rect.Height-1 {
89		t.Fatalf("focused cursor row %d is outside pane %+v", cursorRow, frame.Rect)
90	}
91	if !strings.Contains(stripANSI(strings.Join(frame.Lines, "\n")), "row 25") {
92		t.Fatal("focused row was not retained in the pane viewport")
93	}
94
95	other, _, _ := pane.Compose(Dark, "subagents", "subagents", nil, content, 100, 12, -1, 0)
96	if strings.Contains(stripANSI(strings.Join(other.Lines, "\n")), "row 25") {
97		t.Fatal("new view inherited the previous view's scroll position")
98	}
99}
100
101func TestRendererDrawFloatingPaintsBackgroundThenPaneAtAbsolutePosition(t *testing.T) {
102	var out strings.Builder
103	r := NewRenderer(&out)
104	r.Resize(100, 20)
105	var pane FloatingPane
106	frame, _, _ := pane.Compose(Dark, "settings", "settings", []string{"background"}, []string{"foreground"}, 100, 20, -1, 0)
107	r.DrawFloating(frame, -1, 0)
108	got := out.String()
109	if !strings.Contains(got, Dim("background")) || !strings.Contains(got, "foreground") {
110		t.Fatalf("floating render omitted a layer: %q", got)
111	}
112	if !strings.Contains(got, MoveTo(frame.Rect.Y+1, frame.Rect.X+1)) {
113		t.Fatalf("floating pane was not painted at its geometry: %q", got)
114	}
115
116	out.Reset()
117	r.Draw(frame.Background, -1, 0)
118	if !strings.Contains(out.String(), MoveTo(1, 1)) {
119		t.Fatalf("normal frame did not repaint after a floating pane: %q", out.String())
120	}
121}
122
123func TestRendererDrawFloatingPreservesThemeBackgroundAndImageInvalidation(t *testing.T) {
124	var out strings.Builder
125	r := NewRenderer(&out)
126	background := Color256(237)
127	th := Dark
128	th.Background = &background
129	r.SetTheme(th)
130	r.Resize(20, 4)
131
132	pane := FloatingPaneFrame{
133		Background: make([]string, 4),
134		Lines:      []string{"\x1b_Ga=T,f=100;image\x1b\\"},
135		Rect:       FloatingPaneRect{X: 2, Y: 1, Width: 10, Height: 1, Borderless: true},
136	}
137	r.DrawFloating(pane, -1, 0)
138	if !strings.Contains(out.String(), th.BackgroundStyle()) {
139		t.Fatalf("floating pane omitted theme background: %q", out.String())
140	}
141	if !r.prevHadImage || !r.prevHadKittyImage {
142		t.Fatalf("floating image state = image:%t kitty:%t, want both true", r.prevHadImage, r.prevHadKittyImage)
143	}
144
145	out.Reset()
146	r.Draw([]string{""}, -1, 0)
147	if !strings.Contains(out.String(), SeqDeleteKittyImages) {
148		t.Fatalf("stale kitty image was not deleted: %q", out.String())
149	}
150}

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