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 // KittyKeyboardQuery asks for the current kitty keyboard enhancement
18 // flags. Send it before DeviceAttributesQuery: every terminal answers DA1,
19 // so a DA1 response without a keyboard response means no support.
20 KittyKeyboardQuery = "\x1b[?u"
21
22 maxProbeResponseBytes = 256
23)
24
25// Probe incrementally recognizes the bounded responses to KittyGraphicsQuery
26// and DeviceAttributesQuery. Feed returns bytes that are not recognized probe
27// responses in their original order. A response may be split across any
28// number of calls.
29type Probe struct {
30 pending []byte
31 kitty bool
32 da1 bool
33 // keyboard is set once a KittyKeyboardQuery response was observed.
34 keyboard bool
35}
36
37// Feed consumes one terminal input fragment and returns unrelated input. The
38// returned slice is owned by the Probe and is invalidated by the next call.
39func (p *Probe) Feed(data []byte) []byte {
40 if p == nil || len(data) == 0 {
41 return append([]byte(nil), data...)
42 }
43 // Never retain or copy an unbounded caller fragment into pending. scan
44 // leaves at most a bounded incomplete probe prefix, so feeding bounded
45 // chunks also prevents a small suffix from retaining a large backing array.
46 var unrelated []byte
47 for len(data) != 0 {
48 n := min(len(data), maxProbeResponseBytes+2)
49 p.pending = append(p.pending, data[:n]...)
50 unrelated = append(unrelated, p.scan(false)...)
51 data = data[n:]
52 }
53 return unrelated
54}
55
56// Finish flushes an incomplete or unrelated prefix without recognizing any
57// further response. It is used when the bounded probe deadline expires.
58func (p *Probe) Finish() []byte {
59 if p == nil {
60 return nil
61 }
62 return p.scan(true)
63}
64
65// Ready reports whether both independent capability responses were observed.
66func (p *Probe) Ready() bool { return p != nil && p.kitty && p.da1 }
67
68// KittyGraphics reports whether the Kitty graphics response was observed.
69func (p *Probe) KittyGraphics() bool { return p != nil && p.kitty }
70
71// KittyKeyboard reports whether a kitty keyboard protocol response was
72// observed. It is meaningful once DA1 is true.
73func (p *Probe) KittyKeyboard() bool { return p != nil && p.keyboard }
74
75// DA1 reports whether a primary device-attributes response was observed.
76func (p *Probe) DA1() bool { return p != nil && p.da1 }
77
78func (p *Probe) scan(flush bool) []byte {
79 var unrelated []byte
80 for len(p.pending) != 0 {
81 kittyStart := bytes.Index(p.pending, []byte("\x1b_G"))
82 da1Start := bytes.Index(p.pending, []byte("\x1b["))
83 start := -1
84 if kittyStart >= 0 {
85 start = kittyStart
86 }
87 if da1Start >= 0 && (start < 0 || da1Start < start) {
88 start = da1Start
89 }
90 if start < 0 {
91 if flush || len(p.pending) > 2 {
92 keep := 0
93 if !flush {
94 keep = suffixPrefixLen(p.pending)
95 }
96 if len(p.pending) > keep {
97 unrelated = append(unrelated, p.pending[:len(p.pending)-keep]...)
98 p.pending = p.pending[len(p.pending)-keep:]
99 }
100 }
101 p.retainPending()
102 break
103 }
104 if start > 0 {
105 unrelated = append(unrelated, p.pending[:start]...)
106 p.pending = p.pending[start:]
107 }
108 if p.pending[1] == '_' {
109 end, complete := probeTerminator(p.pending)
110 if !complete {
111 if flush || len(p.pending) > maxProbeResponseBytes {
112 unrelated = append(unrelated, p.pending[0])
113 p.pending = p.pending[1:]
114 continue
115 }
116 p.retainPending()
117 break
118 }
119 response := p.pending[:end]
120 if validKittyResponse(response) {
121 p.kitty = true
122 p.pending = p.pending[end:]
123 continue
124 }
125 unrelated = append(unrelated, p.pending[0])
126 p.pending = p.pending[1:]
127 continue
128 }
129 end, complete := da1Terminator(p.pending)
130 if !complete {
131 if flush || len(p.pending) > maxProbeResponseBytes {
132 unrelated = append(unrelated, p.pending[0])
133 p.pending = p.pending[1:]
134 continue
135 }
136 p.retainPending()
137 break
138 }
139 response := p.pending[:end]
140 if validDA1Response(response) {
141 p.da1 = true
142 p.pending = p.pending[end:]
143 continue
144 }
145 if validKittyKeyboardResponse(response) {
146 p.keyboard = true
147 p.pending = p.pending[end:]
148 continue
149 }
150 unrelated = append(unrelated, p.pending[0])
151 p.pending = p.pending[1:]
152 }
153 return unrelated
154}
155
156func (p *Probe) retainPending() {
157 if len(p.pending) == 0 {
158 p.pending = nil
159 return
160 }
161 p.pending = append([]byte(nil), p.pending...)
162}
163
164func probeTerminator(data []byte) (int, bool) {
165 for i := 3; i+1 < len(data) && i <= maxProbeResponseBytes; i++ {
166 if data[i] == '\x1b' && data[i+1] == '\\' {
167 return i + 2, true
168 }
169 }
170 return 0, false
171}
172
173func da1Terminator(data []byte) (int, bool) {
174 for i := 2; i < len(data) && i <= maxProbeResponseBytes; i++ {
175 if data[i] >= 0x40 && data[i] <= 0x7e {
176 return i + 1, true
177 }
178 }
179 return 0, false
180}
181
182func validKittyResponse(data []byte) bool {
183 return bytes.Equal(data, []byte("\x1b_Gi=31;OK\x1b\\"))
184}
185
186func validDA1Response(data []byte) bool {
187 if len(data) < 5 || !bytes.HasPrefix(data, []byte("\x1b[")) || data[len(data)-1] != 'c' || data[2] != '?' || len(data) > maxProbeResponseBytes {
188 return false
189 }
190 // DA1 is a primary response. Secondary DA (ESC[>...c) is deliberately
191 // not sufficient for a direct graphics declaration. The parameter body is
192 // a non-empty semicolon-separated list of decimal numbers.
193 body := data[3 : len(data)-1]
194 if len(body) == 0 || body[0] == ';' {
195 return false
196 }
197 previousSeparator := false
198 for _, b := range body {
199 switch {
200 case b >= '0' && b <= '9':
201 previousSeparator = false
202 case b == ';':
203 if previousSeparator {
204 return false
205 }
206 previousSeparator = true
207 default:
208 return false
209 }
210 }
211 // Kitty terminates its DA1 parameter list with a separator. DEC treats the
212 // resulting empty final parameter as zero, so both forms are valid.
213 return true
214}
215
216func suffixPrefixLen(data []byte) int {
217 for n := min(len(data), 2); n > 0; n-- {
218 if bytes.HasPrefix([]byte("\x1b_G"), data[len(data)-n:]) || bytes.HasPrefix([]byte("\x1b["), data[len(data)-n:]) {
219 return n
220 }
221 }
222 return 0
223}
224
225func min(a, b int) int {
226 if a < b {
227 return a
228 }
229 return b
230}
231
232// validKittyKeyboardResponse accepts ESC [ ? <decimal flags> u.
233func validKittyKeyboardResponse(data []byte) bool {
234 if len(data) < 5 || !bytes.HasPrefix(data, []byte("\x1b[?")) || data[len(data)-1] != 'u' {
235 return false
236 }
237 for _, b := range data[3 : len(data)-1] {
238 if b < '0' || b > '9' {
239 return false
240 }
241 }
242 return true
243}