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] == ';' {
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 // Kitty terminates its DA1 parameter list with a separator. DEC treats the
197 // resulting empty final parameter as zero, so both forms are valid.
198 return true
199}
200
201func suffixPrefixLen(data []byte) int {
202 for n := min(len(data), 2); n > 0; n-- {
203 if bytes.HasPrefix([]byte("\x1b_G"), data[len(data)-n:]) || bytes.HasPrefix([]byte("\x1b["), data[len(data)-n:]) {
204 return n
205 }
206 }
207 return 0
208}
209
210func min(a, b int) int {
211 if a < b {
212 return a
213 }
214 return b
215}