v0.51.0 a9d674e

appearance_protocol_test.go RawHistoryFolder

1package tui
2
3import (
4	"io"
5	"testing"
6	"time"
7)
8
9func TestParseAppearanceOSC(t *testing.T) {
10	events := ParseAppearanceOSC([]byte("\x1b]4;1;rgb:ff/00/00;12;#123456\x1b\\"))
11	if len(events) != 2 {
12		t.Fatalf("events = %#v, want two palette entries", events)
13	}
14	if events[0].Slot != 1 || events[0].Color != ColorRGB(255, 0, 0) {
15		t.Fatalf("first event = %#v", events[0])
16	}
17	if events[1].Slot != 12 || events[1].Color != ColorRGB(0x12, 0x34, 0x56) {
18		t.Fatalf("second event = %#v", events[1])
19	}
20}
21
22func TestParseAppearanceCSI(t *testing.T) {
23	scheme, mode := ParseAppearanceCSI([]byte("\x1b[?997;2n"))
24	if scheme == nil || !scheme.Light || mode != nil {
25		t.Fatalf("scheme parse = %#v, %#v", scheme, mode)
26	}
27	scheme, mode = ParseAppearanceCSI([]byte("\x1b[?2031;2$y"))
28	if scheme != nil || mode == nil || mode.Status != 2 {
29		t.Fatalf("mode parse = %#v, %#v", scheme, mode)
30	}
31}
32
33type appearanceBytes struct {
34	data    []byte
35	minPeek time.Duration
36}
37
38func (s *appearanceBytes) read() (byte, error) {
39	if len(s.data) == 0 {
40		return 0, io.EOF
41	}
42	b := s.data[0]
43	s.data = s.data[1:]
44	return b, nil
45}
46
47func (s *appearanceBytes) peek(timeout time.Duration) (byte, bool, error) {
48	if timeout < s.minPeek {
49		return 0, false, nil
50	}
51	if len(s.data) == 0 {
52		return 0, false, nil
53	}
54	b := s.data[0]
55	s.data = s.data[1:]
56	return b, true, nil
57}
58
59func TestAppearanceSourceConsumesOnlyPendingReplies(t *testing.T) {
60	input := &appearanceBytes{data: []byte("a\x1b]11;#123456\ab")}
61	parser := &AppearanceParser{}
62	parser.SetPendingColors(true)
63	var events []InputEvent
64	source := NewAppearanceSource(input.read, input.peek, parser, func(event InputEvent) { events = append(events, event) })
65	var got []byte
66	for {
67		b, err := source.ReadByte()
68		if err == io.EOF {
69			break
70		}
71		if err != nil {
72			t.Fatal(err)
73		}
74		got = append(got, b)
75	}
76	if string(got) != "ab" {
77		t.Fatalf("remaining bytes = %q, want ab", got)
78	}
79	if len(events) != 1 || events[0].Color == nil || events[0].Color.Kind != 11 {
80		t.Fatalf("events = %#v", events)
81	}
82}
83
84func TestAppearanceSourceAcceptsDelayedReplyWhileQueryPending(t *testing.T) {
85	input := &appearanceBytes{data: []byte("\x1b]11;#123456\a"), minPeek: 2 * time.Millisecond}
86	parser := &AppearanceParser{}
87	parser.SetPendingColors(true)
88	var events []InputEvent
89	source := NewAppearanceSource(input.read, input.peek, parser, func(event InputEvent) { events = append(events, event) })
90	if _, err := source.ReadByte(); err != io.EOF {
91		t.Fatalf("ReadByte error = %v, want EOF after consuming reply", err)
92	}
93	if len(events) != 1 || events[0].Color == nil || events[0].Color.Kind != 11 {
94		t.Fatalf("delayed reply events = %#v", events)
95	}
96}
97
98func TestAppearanceSourceMarksOnlyPendingSchemeReplySolicited(t *testing.T) {
99	input := &appearanceBytes{data: []byte("\x1b[?997;1n\x1b[?997;2n")}
100	parser := &AppearanceParser{}
101	parser.SetPendingScheme(true)
102	parser.SetNotifications(true)
103	var events []InputEvent
104	source := NewAppearanceSource(input.read, input.peek, parser, func(event InputEvent) { events = append(events, event) })
105	for {
106		_, err := source.ReadByte()
107		if err == io.EOF {
108			break
109		}
110		if err != nil {
111			t.Fatal(err)
112		}
113	}
114	if len(events) != 2 || events[0].Scheme == nil || events[1].Scheme == nil || !events[0].Scheme.Solicited || events[1].Scheme.Solicited {
115		t.Fatalf("scheme events = %#v", events)
116	}
117}
118
119func TestAppearanceSourceLeavesPasteOpaque(t *testing.T) {
120	input := &appearanceBytes{data: []byte("\x1b[200~x\x1b]11;#123456\ay\x1b[201~")}
121	parser := &AppearanceParser{}
122	parser.SetPendingColors(true)
123	var events []InputEvent
124	source := NewAppearanceSource(input.read, input.peek, parser, func(event InputEvent) { events = append(events, event) })
125	reader := NewReader(source.ReadByte)
126	key, err := reader.Read()
127	if err != nil {
128		t.Fatal(err)
129	}
130	if key.Kind != KeyPaste || key.Paste != "x\x1b]11;#123456\ay" {
131		t.Fatalf("paste = %#v", key)
132	}
133	if len(events) != 0 {
134		t.Fatalf("appearance events leaked from paste: %#v", events)
135	}
136}

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