v0.1.81 1aea23e

render_test.go RawHistoryFolder

1package tui
2
3import (
4	"bytes"
5	"strings"
6	"testing"
7)
8
9// TestDrawLogIdleNoOpEmitsNothing pins the cursor-blink fix: when
10// DrawLog is called with the exact same buffer and cursor position
11// as the previous call, it must emit ZERO bytes.
12//
13// The bug this regresses: at the 120ms animation tick the renderer
14// used to always emit SeqHideCursor + cursor-position +
15// SeqShowCursor, which resets the terminal's blink timer. Faster
16// than the OS blink interval, so an idle dialog editor (e.g. a
17// re-opened swarm transcript whose agent isn't producing output)
18// rendered the caret as a solid non-blinking block.
19//
20// With the no-op fast path the renderer leaves the screen alone
21// on idle frames, letting the terminal run its own blink cycle.
22func TestDrawLogIdleNoOpEmitsNothing(t *testing.T) {
23	var buf bytes.Buffer
24	r := NewRenderer(&buf)
25	r.Resize(80, 24)
26
27	chat := []string{"hello", "world"}
28	bottom := []string{"▌ "}
29	// First draw populates the renderer's cached buffer.
30	r.DrawLog(chat, bottom, 0, 2)
31	first := buf.Len()
32	if first == 0 {
33		t.Fatal("first DrawLog wrote nothing; setup is broken")
34	}
35	buf.Reset()
36
37	// Identical second draw: same content, same cursor placement.
38	r.DrawLog(chat, bottom, 0, 2)
39	if buf.Len() != 0 {
40		t.Fatalf("idle re-draw emitted %d bytes; expected 0 so terminal blink keeps ticking\n%q",
41			buf.Len(), buf.String())
42	}
43}
44
45// TestDrawLogContentChangeBreaksFastPath proves the no-op fast path
46// only fires when nothing changed. A buffer mutation must still
47// produce output, otherwise streaming agent replies would freeze on
48// screen.
49func TestDrawLogContentChangeBreaksFastPath(t *testing.T) {
50	var buf bytes.Buffer
51	r := NewRenderer(&buf)
52	r.Resize(80, 24)
53
54	r.DrawLog([]string{"hello"}, []string{"▌ "}, 0, 2)
55	buf.Reset()
56
57	// New chat row lands.
58	r.DrawLog([]string{"hello", "world"}, []string{"▌ "}, 0, 2)
59	if buf.Len() == 0 {
60		t.Fatal("content change suppressed by fast path; streaming output would freeze")
61	}
62}
63
64// TestDrawLogCursorMoveBreaksFastPath proves a cursor-only change
65// (no buffer change) still produces output. Without this, typing in
66// the editor would visually move the caret but the terminal would
67// keep drawing it at the old column.
68func TestDrawLogCursorMoveBreaksFastPath(t *testing.T) {
69	var buf bytes.Buffer
70	r := NewRenderer(&buf)
71	r.Resize(80, 24)
72
73	r.DrawLog([]string{"hi"}, []string{"▌ "}, 0, 2)
74	buf.Reset()
75
76	// Same buffer, different cursor column.
77	r.DrawLog([]string{"hi"}, []string{"▌ "}, 0, 3)
78	if buf.Len() == 0 {
79		t.Fatal("cursor-only change suppressed by fast path; caret would lag behind typing")
80	}
81	// And the emitted bytes must at least reposition the cursor.
82	if !strings.Contains(buf.String(), "\x1b[") {
83		t.Errorf("cursor move emission missing CSI escapes: %q", buf.String())
84	}
85}
86
87// TestDrawLogResizeForcesFullRedraw confirms a resize invalidates
88// the cache so the next DrawLog with identical inputs still emits.
89// Resize sets logInit=false; without that, a resize followed by an
90// identical buffer would falsely no-op and leave a stale frame.
91func TestDrawLogResizeForcesFullRedraw(t *testing.T) {
92	var buf bytes.Buffer
93	r := NewRenderer(&buf)
94	r.Resize(80, 24)
95	r.DrawLog([]string{"hi"}, []string{"▌ "}, 0, 2)
96	buf.Reset()
97
98	r.Resize(100, 30)
99	r.DrawLog([]string{"hi"}, []string{"▌ "}, 0, 2)
100	if buf.Len() == 0 {
101		t.Fatal("post-resize redraw skipped; the new frame would never reach the terminal")
102	}
103}

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