detached ab6686c

screen_modes_test.go RawHistoryFolder

1package vt
2
3import (
4	"testing"
5)
6
7func TestESCSequences(t *testing.T) {
8	tests := []struct {
9		name string
10		run  func(t *testing.T)
11	}{
12		{
13			name: "ESC = (DECKPAM) is ignored, following text is printable",
14			run: func(t *testing.T) {
15				s := NewScreen(10, 2)
16				s.Write([]byte("\x1b=abc"))
17
18				assertCell(t, s, 0, 0, 'a')
19				assertCell(t, s, 1, 0, 'b')
20				assertCell(t, s, 2, 0, 'c')
21				if s.Col != 3 || s.Row != 0 {
22					t.Errorf("cursor at row=%d col=%d, want row=0 col=3", s.Row, s.Col)
23				}
24			},
25		},
26		{
27			name: "ESC 7 / ESC 8 save and restore the cursor",
28			run: func(t *testing.T) {
29				s := NewScreen(10, 3)
30				s.Write([]byte("abc\x1b7\x1b[2;5HZZ\x1b8X"))
31
32				assertCell(t, s, 0, 0, 'a')
33				assertCell(t, s, 1, 0, 'b')
34				assertCell(t, s, 2, 0, 'c')
35				assertCell(t, s, 3, 0, 'X')
36				assertCell(t, s, 4, 1, 'Z')
37				assertCell(t, s, 5, 1, 'Z')
38				if s.Row != 0 || s.Col != 4 {
39					t.Errorf("cursor at row=%d col=%d, want row=0 col=4", s.Row, s.Col)
40				}
41			},
42		},
43		{
44			name: "ESC 7 / ESC 8 restore origin and insert modes",
45			run: func(t *testing.T) {
46				s := NewScreen(6, 5)
47				s.Write([]byte("\x1b[2;4r\x1b[?6h\x1b[4h\x1b7"))
48				s.Write([]byte("\x1b[?6l\x1b[4l\x1b8"))
49
50				if !s.originMode {
51					t.Fatal("origin mode was not restored")
52				}
53				if !s.insertMode {
54					t.Fatal("insert mode was not restored")
55				}
56
57				s.Write([]byte("\x1b[1;1Habcd\x1b[1;3HXY"))
58				if s.Row != 1 || s.Col != 4 {
59					t.Fatalf("cursor after restored origin and insert modes = (%d,%d), want (1,4)", s.Row, s.Col)
60				}
61				if got := lineText(s, 1); got != "abXYcd" {
62					t.Fatalf("line after restored insert mode = %q, want %q", got, "abXYcd")
63				}
64			},
65		},
66		{
67			name: "ESC D (index), ESC E (next line), ESC M (reverse index)",
68			run: func(t *testing.T) {
69				s := NewScreen(5, 3)
70				s.Write([]byte("A\x1bDB\x1bEC"))
71
72				assertCell(t, s, 0, 0, 'A')
73				assertCell(t, s, 1, 1, 'B')
74				assertCell(t, s, 0, 2, 'C')
75
76				s = NewScreen(5, 3)
77				s.Write([]byte("\x1b[2;1Hmid\r\x1bMtop"))
78				assertCell(t, s, 0, 0, 't')
79				assertCell(t, s, 0, 1, 'm')
80			},
81		},
82	}
83
84	for _, tt := range tests {
85		t.Run(tt.name, tt.run)
86	}
87}
88
89func TestScreenReportsSynchronizedUpdateMode(t *testing.T) {
90	s := NewScreen(10, 2)
91	s.Write([]byte("\x1b[?2026h"))
92	if !s.SyncUpdateActive() {
93		t.Fatal("sync update mode should be active")
94	}
95	s.Write([]byte("\x1b[?2026l"))
96	if s.SyncUpdateActive() {
97		t.Fatal("sync update mode should be inactive")
98	}
99}
100
101func TestForceSyncEnd(t *testing.T) {
102	s := NewScreen(10, 2)
103	s.Write([]byte("\x1b[?2026h"))
104	if !s.SyncUpdateActive() {
105		t.Fatal("sync update mode should be active before forcing end")
106	}
107	s.ForceSyncEnd()
108	if s.SyncUpdateActive() {
109		t.Fatal("sync update mode should be inactive after forcing end")
110	}
111}
112
113func TestScreenCursorAndMouseStateAccessors(t *testing.T) {
114	tests := []struct {
115		name  string
116		seq   string
117		check func(t *testing.T, s *Screen)
118	}{
119		{
120			name: "cursor position and visibility are exposed",
121			seq:  "\x1b[2;3H\x1b[?25l",
122			check: func(t *testing.T, s *Screen) {
123				if s.CursorRow() != 1 || s.CursorCol() != 2 {
124					t.Fatalf("cursor = row %d col %d, want row 1 col 2", s.CursorRow(), s.CursorCol())
125				}
126				if s.CursorVisible() {
127					t.Fatal("cursor should be hidden")
128				}
129				s.Write([]byte("\x1b[?25h"))
130				if !s.CursorVisible() {
131					t.Fatal("cursor should be visible")
132				}
133			},
134		},
135		{
136			name: "mouse mode and SGR are exposed",
137			seq:  "\x1b[?1002h\x1b[?1006h",
138			check: func(t *testing.T, s *Screen) {
139				mode, sgr := s.MouseMode()
140				if mode != 1002 || !sgr {
141					t.Fatalf("MouseMode() = (%d, %v), want (1002, true)", mode, sgr)
142				}
143			},
144		},
145		{
146			name: "reset restores cursor and mouse defaults",
147			seq:  "\x1b[?25l\x1b[?1003h\x1b[?1006h\x1bc",
148			check: func(t *testing.T, s *Screen) {
149				mode, sgr := s.MouseMode()
150				if !s.CursorVisible() || mode != 0 || sgr {
151					t.Fatalf("after reset CursorVisible=%v MouseMode=(%d,%v), want true,(0,false)", s.CursorVisible(), mode, sgr)
152				}
153			},
154		},
155	}
156	for _, tt := range tests {
157		t.Run(tt.name, func(t *testing.T) {
158			s := NewScreen(10, 4)
159			s.Write([]byte(tt.seq))
160			tt.check(t, s)
161		})
162	}
163}
164
165func TestScreenMouseModeDisableInactiveIsNoOp(t *testing.T) {
166	s := NewScreen(10, 2)
167	s.Write([]byte("\x1b[?1002h"))
168	s.Write([]byte("\x1b[?1000l"))
169	mode, _ := s.MouseMode()
170	if mode != 1002 {
171		t.Fatalf("mouse mode after disabling inactive 1000 = %d, want 1002", mode)
172	}
173	s.Write([]byte("\x1b[?1002l"))
174	mode, _ = s.MouseMode()
175	if mode != 0 {
176		t.Fatalf("mouse mode after disabling active 1002 = %d, want 0", mode)
177	}
178}
179
180func TestScreenCursorStyleDECSCUSR(t *testing.T) {
181	tests := []struct {
182		name      string
183		seq       string
184		wantStyle int
185		wantSet   bool
186	}{
187		{name: "explicit style", seq: "\x1b[5 q", wantStyle: 5, wantSet: true},
188		{name: "blank style parameter", seq: "\x1b[ q", wantStyle: 0, wantSet: true},
189		{name: "invalid low style is ignored", seq: "\x1b[-1 q", wantStyle: 0, wantSet: false},
190		{name: "invalid high style is ignored", seq: "\x1b[7 q", wantStyle: 0, wantSet: false},
191		{name: "XTVERSION is ignored", seq: "\x1b[>0q", wantStyle: 0, wantSet: false},
192	}
193	for _, tt := range tests {
194		t.Run(tt.name, func(t *testing.T) {
195			s := NewScreen(10, 2)
196			s.Write([]byte(tt.seq))
197			style, set := s.CursorStyle()
198			if style != tt.wantStyle || set != tt.wantSet {
199				t.Fatalf("CursorStyle() = (%d, %v), want (%d, %v)", style, set, tt.wantStyle, tt.wantSet)
200			}
201		})
202	}
203}
204
205func TestScreenInvalidCursorStyleDoesNotOverwriteCurrentStyle(t *testing.T) {
206	s := NewScreen(10, 2)
207	s.Write([]byte("\x1b[5 q"))
208	s.Write([]byte("\x1b[99 q"))
209	style, set := s.CursorStyle()
210	if style != 5 || !set {
211		t.Fatalf("CursorStyle() = (%d, %v), want (5, true)", style, set)
212	}
213}
214
215func TestScreenAltScreenActiveAccessor(t *testing.T) {
216	s := NewScreen(10, 2)
217	if s.AltScreenActive() {
218		t.Fatal("alt screen should start inactive")
219	}
220	s.Write([]byte("\x1b[?1049h"))
221	if !s.AltScreenActive() {
222		t.Fatal("alt screen should be active")
223	}
224	s.Write([]byte("\x1b[?1049l"))
225	if s.AltScreenActive() {
226		t.Fatal("alt screen should be inactive after exit")
227	}
228}

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 2

Tags 13

↑↓ moveenter openesc close