mouse_test.go RawHistoryFolder

1package mouse
2
3import (
4	"bytes"
5	"reflect"
6	"testing"
7)
8
9type scanToken struct {
10	kind  string
11	data  []byte
12	event Event
13}
14
15func collectScan(s *Scanner, chunks ...[]byte) []scanToken {
16	var tokens []scanToken
17	for _, chunk := range chunks {
18		s.Scan(chunk, func(ev Event) {
19			tokens = append(tokens, scanToken{kind: "mouse", event: ev})
20		}, func(b []byte) {
21			tokens = append(tokens, scanToken{kind: "bytes", data: append([]byte(nil), b...)})
22		})
23	}
24	return tokens
25}
26
27func TestScanDecodesWheelClickAndDrag(t *testing.T) {
28	tests := []struct {
29		name string
30		in   []byte
31		want Event
32	}{
33		{
34			name: "wheel up",
35			in:   []byte("\x1b[<64;10;20M"),
36			want: Event{Type: Press, Button: WheelUp, Col: 9, Row: 19, Raw: []byte("\x1b[<64;10;20M")},
37		},
38		{
39			name: "wheel down",
40			in:   []byte("\x1b[<65;3;4M"),
41			want: Event{Type: Press, Button: WheelDown, Col: 2, Row: 3, Raw: []byte("\x1b[<65;3;4M")},
42		},
43		{
44			name: "left press",
45			in:   []byte("\x1b[<0;1;2M"),
46			want: Event{Type: Press, Button: Left, Col: 0, Row: 1, Raw: []byte("\x1b[<0;1;2M")},
47		},
48		{
49			name: "left release",
50			in:   []byte("\x1b[<0;1;2m"),
51			want: Event{Type: Release, Button: Left, Col: 0, Row: 1, Raw: []byte("\x1b[<0;1;2m")},
52		},
53		{
54			name: "right motion",
55			in:   []byte("\x1b[<34;7;8M"),
56			want: Event{Type: Motion, Button: Right, Col: 6, Row: 7, Raw: []byte("\x1b[<34;7;8M")},
57		},
58	}
59
60	for _, tt := range tests {
61		t.Run(tt.name, func(t *testing.T) {
62			tokens := collectScan(&Scanner{}, tt.in)
63			if len(tokens) != 1 || tokens[0].kind != "mouse" {
64				t.Fatalf("tokens = %#v, want one mouse token", tokens)
65			}
66			if !reflect.DeepEqual(tokens[0].event, tt.want) {
67				t.Fatalf("event = %#v, want %#v", tokens[0].event, tt.want)
68			}
69		})
70	}
71}
72
73func TestScanCoalescedEvents(t *testing.T) {
74	tokens := collectScan(&Scanner{}, []byte("\x1b[<0;1;1M\x1b[<65;2;3M"))
75	if len(tokens) != 2 || tokens[0].kind != "mouse" || tokens[1].kind != "mouse" {
76		t.Fatalf("tokens = %#v, want two mouse tokens", tokens)
77	}
78	if tokens[0].event.Button != Left || tokens[1].event.Button != WheelDown {
79		t.Fatalf("buttons = %v, %v; want Left, WheelDown", tokens[0].event.Button, tokens[1].event.Button)
80	}
81}
82
83func TestScanBuffersSplitReportAfterIntroducer(t *testing.T) {
84	var s Scanner
85	first := collectScan(&s, []byte("\x1b[<64;"))
86	if len(first) != 0 {
87		t.Fatalf("first scan tokens = %#v, want none while pending", first)
88	}
89	second := collectScan(&s, []byte("5;6M"))
90	if len(second) != 1 || second[0].kind != "mouse" {
91		t.Fatalf("second scan tokens = %#v, want one mouse token", second)
92	}
93	want := Event{Type: Press, Button: WheelUp, Col: 4, Row: 5, Raw: []byte("\x1b[<64;5;6M")}
94	if !reflect.DeepEqual(second[0].event, want) {
95		t.Fatalf("event = %#v, want %#v", second[0].event, want)
96	}
97}
98
99func TestScanPreservesInterleavedOrdering(t *testing.T) {
100	tokens := collectScan(&Scanner{}, []byte("a\x1b[<0;1;1Mb\x1b[<65;2;2Mc"))
101	want := []scanToken{
102		{kind: "bytes", data: []byte("a")},
103		{kind: "mouse", event: Event{Type: Press, Button: Left, Col: 0, Row: 0, Raw: []byte("\x1b[<0;1;1M")}},
104		{kind: "bytes", data: []byte("b")},
105		{kind: "mouse", event: Event{Type: Press, Button: WheelDown, Col: 1, Row: 1, Raw: []byte("\x1b[<65;2;2M")}},
106		{kind: "bytes", data: []byte("c")},
107	}
108	if !reflect.DeepEqual(tokens, want) {
109		t.Fatalf("tokens = %#v, want %#v", tokens, want)
110	}
111}
112
113func TestScanMalformedBailsToBytes(t *testing.T) {
114	tests := [][]byte{
115		[]byte("\x1b[<x"),
116		[]byte("\x1b[<0;;1M"),
117		[]byte("\x1b[<0;0;1M"),
118		[]byte("\x1b[<0;1M"),
119	}
120	for _, in := range tests {
121		t.Run(string(bytes.ReplaceAll(in, []byte("\x1b"), []byte("ESC"))), func(t *testing.T) {
122			tokens := collectScan(&Scanner{}, in)
123			if len(tokens) != 1 || tokens[0].kind != "bytes" || !bytes.Equal(tokens[0].data, in) {
124				t.Fatalf("tokens = %#v, want one byte token %q", tokens, in)
125			}
126		})
127	}
128}
129
130func TestScanPassesThroughBareESCAndESCBracket(t *testing.T) {
131	tests := [][]byte{
132		[]byte("\x1b"),
133		[]byte("\x1b["),
134		[]byte("x\x1b"),
135		[]byte("x\x1b["),
136	}
137	for _, in := range tests {
138		t.Run(string(bytes.ReplaceAll(in, []byte("\x1b"), []byte("ESC"))), func(t *testing.T) {
139			tokens := collectScan(&Scanner{}, in)
140			if len(tokens) != 1 || tokens[0].kind != "bytes" || !bytes.Equal(tokens[0].data, in) {
141				t.Fatalf("tokens = %#v, want passthrough %q", tokens, in)
142			}
143		})
144	}
145}

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 12

Tags 11

↑↓ moveenter openesc close