v0.4.0 0b9cfda

query.go RawHistoryFolder

1// Package terminalquery provides bounded parsers for terminal capability
2// probes. Probe responses are terminal input, not application input: callers
3// can remove only the two exact responses while replaying every unrelated byte.
4package terminalquery
5
6import "bytes"
7
8const (
9	// KittyGraphicsQuery asks a terminal whether it understands the Kitty
10	// graphics protocol. The image id is deliberately reserved for probing and
11	// the 1×1 RGB payload is validated without being retained by a=q.
12	KittyGraphicsQuery = "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\"
13	// DeviceAttributesQuery is the primary DA1 query. It is paired with the
14	// Kitty query so a graphics declaration requires both terminal identity and
15	// protocol support.
16	DeviceAttributesQuery = "\x1b[c"
17
18	maxProbeResponseBytes = 256
19)
20
21// Probe incrementally recognizes the bounded responses to KittyGraphicsQuery
22// and DeviceAttributesQuery. Feed returns bytes that are not recognized probe
23// responses in their original order. A response may be split across any
24// number of calls.
25type Probe struct {
26	pending []byte
27	kitty   bool
28	da1     bool
29}
30
31// Feed consumes one terminal input fragment and returns unrelated input. The
32// returned slice is owned by the Probe and is invalidated by the next call.
33func (p *Probe) Feed(data []byte) []byte {
34	if p == nil || len(data) == 0 {
35		return append([]byte(nil), data...)
36	}
37	// Never retain or copy an unbounded caller fragment into pending. scan
38	// leaves at most a bounded incomplete probe prefix, so feeding bounded
39	// chunks also prevents a small suffix from retaining a large backing array.
40	var unrelated []byte
41	for len(data) != 0 {
42		n := min(len(data), maxProbeResponseBytes+2)
43		p.pending = append(p.pending, data[:n]...)
44		unrelated = append(unrelated, p.scan(false)...)
45		data = data[n:]
46	}
47	return unrelated
48}
49
50// Finish flushes an incomplete or unrelated prefix without recognizing any
51// further response. It is used when the bounded probe deadline expires.
52func (p *Probe) Finish() []byte {
53	if p == nil {
54		return nil
55	}
56	return p.scan(true)
57}
58
59// Ready reports whether both independent capability responses were observed.
60func (p *Probe) Ready() bool { return p != nil && p.kitty && p.da1 }
61
62// KittyGraphics reports whether the Kitty graphics response was observed.
63func (p *Probe) KittyGraphics() bool { return p != nil && p.kitty }
64
65// DA1 reports whether a primary device-attributes response was observed.
66func (p *Probe) DA1() bool { return p != nil && p.da1 }
67
68func (p *Probe) scan(flush bool) []byte {
69	var unrelated []byte
70	for len(p.pending) != 0 {
71		kittyStart := bytes.Index(p.pending, []byte("\x1b_G"))
72		da1Start := bytes.Index(p.pending, []byte("\x1b["))
73		start := -1
74		if kittyStart >= 0 {
75			start = kittyStart
76		}
77		if da1Start >= 0 && (start < 0 || da1Start < start) {
78			start = da1Start
79		}
80		if start < 0 {
81			if flush || len(p.pending) > 2 {
82				keep := 0
83				if !flush {
84					keep = suffixPrefixLen(p.pending)
85				}
86				if len(p.pending) > keep {
87					unrelated = append(unrelated, p.pending[:len(p.pending)-keep]...)
88					p.pending = p.pending[len(p.pending)-keep:]
89				}
90			}
91			p.retainPending()
92			break
93		}
94		if start > 0 {
95			unrelated = append(unrelated, p.pending[:start]...)
96			p.pending = p.pending[start:]
97		}
98		if p.pending[1] == '_' {
99			end, complete := probeTerminator(p.pending)
100			if !complete {
101				if flush || len(p.pending) > maxProbeResponseBytes {
102					unrelated = append(unrelated, p.pending[0])
103					p.pending = p.pending[1:]
104					continue
105				}
106				p.retainPending()
107				break
108			}
109			response := p.pending[:end]
110			if validKittyResponse(response) {
111				p.kitty = true
112				p.pending = p.pending[end:]
113				continue
114			}
115			unrelated = append(unrelated, p.pending[0])
116			p.pending = p.pending[1:]
117			continue
118		}
119		end, complete := da1Terminator(p.pending)
120		if !complete {
121			if flush || len(p.pending) > maxProbeResponseBytes {
122				unrelated = append(unrelated, p.pending[0])
123				p.pending = p.pending[1:]
124				continue
125			}
126			p.retainPending()
127			break
128		}
129		response := p.pending[:end]
130		if validDA1Response(response) {
131			p.da1 = true
132			p.pending = p.pending[end:]
133			continue
134		}
135		unrelated = append(unrelated, p.pending[0])
136		p.pending = p.pending[1:]
137	}
138	return unrelated
139}
140
141func (p *Probe) retainPending() {
142	if len(p.pending) == 0 {
143		p.pending = nil
144		return
145	}
146	p.pending = append([]byte(nil), p.pending...)
147}
148
149func probeTerminator(data []byte) (int, bool) {
150	for i := 3; i+1 < len(data) && i <= maxProbeResponseBytes; i++ {
151		if data[i] == '\x1b' && data[i+1] == '\\' {
152			return i + 2, true
153		}
154	}
155	return 0, false
156}
157
158func da1Terminator(data []byte) (int, bool) {
159	for i := 2; i < len(data) && i <= maxProbeResponseBytes; i++ {
160		if data[i] >= 0x40 && data[i] <= 0x7e {
161			return i + 1, true
162		}
163	}
164	return 0, false
165}
166
167func validKittyResponse(data []byte) bool {
168	return bytes.Equal(data, []byte("\x1b_Gi=31;OK\x1b\\"))
169}
170
171func validDA1Response(data []byte) bool {
172	if len(data) < 5 || !bytes.HasPrefix(data, []byte("\x1b[")) || data[len(data)-1] != 'c' || data[2] != '?' || len(data) > maxProbeResponseBytes {
173		return false
174	}
175	// DA1 is a primary response. Secondary DA (ESC[>...c) is deliberately
176	// not sufficient for a direct graphics declaration. The parameter body is
177	// a non-empty semicolon-separated list of decimal numbers.
178	body := data[3 : len(data)-1]
179	if len(body) == 0 || body[0] == ';' || body[len(body)-1] == ';' {
180		return false
181	}
182	previousSeparator := false
183	for _, b := range body {
184		switch {
185		case b >= '0' && b <= '9':
186			previousSeparator = false
187		case b == ';':
188			if previousSeparator {
189				return false
190			}
191			previousSeparator = true
192		default:
193			return false
194		}
195	}
196	return !previousSeparator
197}
198
199func suffixPrefixLen(data []byte) int {
200	for n := min(len(data), 2); n > 0; n-- {
201		if bytes.HasPrefix([]byte("\x1b_G"), data[len(data)-n:]) || bytes.HasPrefix([]byte("\x1b["), data[len(data)-n:]) {
202			return n
203		}
204	}
205	return 0
206}
207
208func min(a, b int) int {
209	if a < b {
210		return a
211	}
212	return b
213}

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