mouse.go RawHistoryFolder

1package mouse
2
3import (
4	"bytes"
5	"strconv"
6)
7
8// Type describes the kind of SGR mouse event.
9type Type int
10
11const (
12	Press Type = iota
13	Release
14	Motion
15)
16
17// Button describes the button encoded by an SGR mouse event.
18type Button int
19
20const (
21	Left Button = iota
22	Middle
23	Right
24	WheelUp
25	WheelDown
26	Other
27)
28
29// Event is a decoded SGR mouse report.
30type Event struct {
31	Type   Type
32	Button Button
33	Col    int
34	Row    int
35	Raw    []byte
36}
37
38// Scanner decodes SGR mouse reports from a byte stream while preserving the
39// ordering of decoded mouse events and ordinary byte segments.
40type Scanner struct {
41	pending []byte
42}
43
44// Scan decodes SGR mouse reports of the form ESC [ < Cb ; Cx ; Cy M/m.
45// Non-mouse bytes are emitted through onBytes in their original order. Only
46// partial reports after a complete ESC[< introducer are buffered between calls;
47// a bare ESC or ESC[ at the end of data is passed through unchanged.
48func (s *Scanner) Scan(data []byte, onMouse func(Event), onBytes func([]byte)) {
49	if len(s.pending) > 0 {
50		combined := make([]byte, 0, len(s.pending)+len(data))
51		combined = append(combined, s.pending...)
52		combined = append(combined, data...)
53		data = combined
54		s.pending = nil
55	}
56
57	byteStart := 0
58	for i := 0; i < len(data); i++ {
59		if data[i] != '\x1b' {
60			continue
61		}
62
63		remaining := len(data) - i
64		if remaining < 3 {
65			continue
66		}
67		if data[i+1] != '[' || data[i+2] != '<' {
68			continue
69		}
70
71		if byteStart < i {
72			onBytes(data[byteStart:i])
73		}
74
75		raw, ev, complete, ok := parseSGR(data[i:])
76		if !complete {
77			s.pending = append(s.pending, data[i:]...)
78			return
79		}
80		if !ok {
81			onBytes(raw)
82			i += len(raw) - 1
83			byteStart = i + 1
84			continue
85		}
86
87		onMouse(ev)
88		i += len(raw) - 1
89		byteStart = i + 1
90	}
91
92	if byteStart < len(data) {
93		onBytes(data[byteStart:])
94	}
95}
96
97func parseSGR(data []byte) (raw []byte, ev Event, complete bool, ok bool) {
98	end := -1
99	for i := 3; i < len(data); i++ {
100		switch data[i] {
101		case 'M', 'm':
102			end = i
103			goto found
104		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ';':
105			continue
106		default:
107			return data[:i+1], Event{}, true, false
108		}
109	}
110found:
111	if end == -1 {
112		return nil, Event{}, false, false
113	}
114
115	raw = data[:end+1]
116	parts := bytes.Split(data[3:end], []byte(";"))
117	if len(parts) != 3 {
118		return raw, Event{}, true, false
119	}
120	cb, ok := atoiBytes(parts[0])
121	if !ok {
122		return raw, Event{}, true, false
123	}
124	cx, ok := atoiBytes(parts[1])
125	if !ok {
126		return raw, Event{}, true, false
127	}
128	cy, ok := atoiBytes(parts[2])
129	if !ok {
130		return raw, Event{}, true, false
131	}
132	if cx < 1 || cy < 1 {
133		return raw, Event{}, true, false
134	}
135
136	ev = Event{
137		Type:   eventType(cb, data[end]),
138		Button: eventButton(cb),
139		Col:    cx - 1,
140		Row:    cy - 1,
141		Raw:    append([]byte(nil), raw...),
142	}
143	return raw, ev, true, true
144}
145
146func atoiBytes(b []byte) (int, bool) {
147	if len(b) == 0 {
148		return 0, false
149	}
150	v, err := strconv.Atoi(string(b))
151	return v, err == nil
152}
153
154func eventType(cb int, final byte) Type {
155	if final == 'm' {
156		return Release
157	}
158	if cb&32 != 0 {
159		return Motion
160	}
161	return Press
162}
163
164func eventButton(cb int) Button {
165	if cb&64 != 0 {
166		switch cb {
167		case 64:
168			return WheelUp
169		case 65:
170			return WheelDown
171		default:
172			return Other
173		}
174	}
175
176	switch cb & 3 {
177	case 0:
178		return Left
179	case 1:
180		return Middle
181	case 2:
182		return Right
183	default:
184		return Other
185	}
186}

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