1package vt
2
3import (
4 "strconv"
5 "strings"
6
7 renderer "github.com/bnema/vev-vt/core"
8)
9
10func (s *Screen) consumeCSI(data []byte) (consumed int, partial bool) {
11 end := -1
12 for i := 2; i < len(data); i++ {
13 if data[i] >= '@' && data[i] <= '~' {
14 end = i
15 break
16 }
17 }
18 if end == -1 {
19 return 0, true
20 }
21 params := string(data[2:end])
22 cmd := data[end]
23 s.applyCSI(params, cmd)
24 return end + 1, false
25}
26
27func (s *Screen) applyCSI(params string, cmd byte) {
28 private := strings.HasPrefix(params, "?")
29 parts := s.parseCSIInts(params)
30 switch cmd {
31 case 'c':
32 switch params {
33 case "", "0":
34 // Identify as a VT220-compatible terminal with ANSI color. Modern
35 // capability probes require a DA1 response with a conformance level;
36 // the shorter VT102 response is ignored by kitten icat.
37 s.respond([]byte("\x1b[?62;22c"))
38 case ">", ">0":
39 s.respond([]byte("\x1b[>0;0;0c"))
40 }
41 case 'n':
42 switch firstPositive(parts, 0) {
43 case 5:
44 if !private {
45 s.respond([]byte("\x1b[0n"))
46 }
47 case 996:
48 if private {
49 if report := s.colorSchemeReport(); report != nil {
50 s.respond(report)
51 }
52 }
53 case 6:
54 resp := make([]byte, 0, 16)
55 resp = append(resp, "\x1b["...)
56 if private {
57 resp = append(resp, '?')
58 }
59 resp = strconv.AppendInt(resp, int64(s.cursorReportRow()), 10)
60 resp = append(resp, ';')
61 resp = strconv.AppendInt(resp, int64(s.Col+1), 10)
62 resp = append(resp, 'R')
63 s.respond(resp)
64 }
65 case 'p':
66 if modeText, ok := strings.CutSuffix(params, "$"); ok {
67 if private {
68 modeText = strings.TrimPrefix(modeText, "?")
69 }
70 mode, err := strconv.Atoi(modeText)
71 if err != nil {
72 return
73 }
74 state := s.modeReportState(private, mode)
75 resp := make([]byte, 0, 16)
76 resp = append(resp, "\x1b["...)
77 if private {
78 resp = append(resp, '?')
79 }
80 resp = strconv.AppendInt(resp, int64(mode), 10)
81 resp = append(resp, ';')
82 resp = strconv.AppendInt(resp, int64(state), 10)
83 resp = append(resp, "$y"...)
84 s.respond(resp)
85 }
86 case 'm':
87 if strings.HasPrefix(params, ">") {
88 return
89 }
90 s.applySGR(params)
91 case 'q':
92 s.applyCursorStyle(params)
93 case 'J':
94 mode := 0
95 if len(parts) > 0 {
96 mode = parts[0]
97 }
98 s.clearScreenMode(mode)
99 case 'K':
100 mode := 0
101 if len(parts) > 0 {
102 mode = parts[0]
103 }
104 s.clearLineMode(mode)
105 case 'X':
106 s.eraseChars(firstPositive(parts, 1))
107 case 'S':
108 s.scrollUpBy(firstPositive(parts, 1))
109 case 'T':
110 s.scrollDownBy(firstPositive(parts, 1))
111 case 'H', 'f':
112 row, col := 1, 1
113 if len(parts) > 0 && parts[0] > 0 {
114 row = parts[0]
115 }
116 if len(parts) > 1 && parts[1] > 0 {
117 col = parts[1]
118 }
119 s.addressCursor(row, col)
120 case 'A':
121 s.Row = clamp(s.Row-firstPositive(parts, 1), s.cursorMinRow(), s.cursorMaxRow())
122 case 'B':
123 s.Row = clamp(s.Row+firstPositive(parts, 1), s.cursorMinRow(), s.cursorMaxRow())
124 case 'C':
125 s.Col = clamp(s.Col+firstPositive(parts, 1), 0, s.frame.Width-1)
126 case 'D':
127 s.Col = clamp(s.Col-firstPositive(parts, 1), 0, s.frame.Width-1)
128 case 'E':
129 s.Row = clamp(s.Row+firstPositive(parts, 1), s.cursorMinRow(), s.cursorMaxRow())
130 s.Col = 0
131 case 'F':
132 s.Row = clamp(s.Row-firstPositive(parts, 1), s.cursorMinRow(), s.cursorMaxRow())
133 s.Col = 0
134 case 'G':
135 col := firstPositive(parts, 1)
136 s.Col = clamp(col-1, 0, s.frame.Width-1)
137 case 'd':
138 row := firstPositive(parts, 1)
139 s.Row = s.addressedRow(row)
140 case '@':
141 s.insertChars(firstPositive(parts, 1))
142 case 'P':
143 s.deleteChars(firstPositive(parts, 1))
144 case 'L':
145 s.insertLines(firstPositive(parts, 1))
146 case 'M':
147 s.deleteLines(firstPositive(parts, 1))
148 case 'r':
149 s.setScrollRegion(parts)
150 case 't':
151 if !private {
152 s.reportWindowSize(parts)
153 }
154 case 's':
155 s.saveCursor()
156 case 'u':
157 if params == "" {
158 s.restoreCursor()
159 }
160 case 'h':
161 s.setMode(private, parts, true)
162 case 'l':
163 s.setMode(private, parts, false)
164 }
165}
166
167func firstPositive(parts []int, fallback int) int {
168 if len(parts) == 0 || parts[0] <= 0 {
169 return fallback
170 }
171 return parts[0]
172}
173
174func (s *Screen) modeReportState(private bool, mode int) int {
175 if private {
176 switch mode {
177 case 6:
178 return boolModeReportState(s.originMode)
179 case 2026:
180 return boolModeReportState(s.syncUpdateActive)
181 case 2031:
182 return boolModeReportState(s.colorSchemeMode)
183 }
184 return 0
185 }
186 if mode == 4 {
187 return boolModeReportState(s.insertMode)
188 }
189 return 0
190}
191
192func boolModeReportState(enabled bool) int {
193 if enabled {
194 return 1
195 }
196 return 2
197}
198
199const sgrUnderlineStyleMarker = -1000
200
201func (s *Screen) applySGR(params string) {
202 if params == "" {
203 s.Style = renderer.DefaultStyle()
204 return
205 }
206 parts := s.parseSGRInts(params)
207 for i := 0; i < len(parts); i++ {
208 switch parts[i] {
209 case 0:
210 s.Style = renderer.DefaultStyle()
211 case 1:
212 s.Style.Bold = true
213 case 2:
214 s.Style.Attrs |= renderer.AttrDim
215 case 3:
216 s.Style.Italic = true
217 case 4:
218 s.Style.Attrs |= renderer.AttrUnderline
219 s.Style.UnderlineStyle = renderer.UnderlineSingle
220 if i+2 < len(parts) && parts[i+1] == sgrUnderlineStyleMarker {
221 underlineStyle := sgrUnderlineStyle(parts[i+2])
222 if underlineStyle == renderer.UnderlineNone {
223 s.Style.Attrs &^= renderer.AttrUnderline
224 } else {
225 s.Style.Attrs |= renderer.AttrUnderline
226 }
227 s.Style.UnderlineStyle = underlineStyle
228 i += 2
229 }
230 case 5, 6:
231 s.Style.Attrs |= renderer.AttrBlink
232 case 7:
233 s.Style.Inverse = true
234 case 9:
235 s.Style.Attrs |= renderer.AttrStrikethrough
236 case 21:
237 s.Style.Attrs |= renderer.AttrUnderline
238 s.Style.UnderlineStyle = renderer.UnderlineDouble
239 case 22:
240 s.Style.Bold = false
241 s.Style.Attrs &^= renderer.AttrDim
242 case 23:
243 s.Style.Italic = false
244 case 24:
245 s.Style.Attrs &^= renderer.AttrUnderline
246 s.Style.UnderlineStyle = renderer.UnderlineNone
247 case 25:
248 s.Style.Attrs &^= renderer.AttrBlink
249 case 27:
250 s.Style.Inverse = false
251 case 29:
252 s.Style.Attrs &^= renderer.AttrStrikethrough
253 case 30, 31, 32, 33, 34, 35, 36, 37:
254 s.Style.Foreground = parts[i] - 30
255 s.Style.HasForegroundRGB = false
256 case 39:
257 s.Style.Foreground = -1
258 s.Style.HasForegroundRGB = false
259 case 40, 41, 42, 43, 44, 45, 46, 47:
260 s.Style.Background = parts[i] - 40
261 s.Style.HasBackgroundRGB = false
262 case 49:
263 s.Style.Background = -1
264 s.Style.HasBackgroundRGB = false
265 case 90, 91, 92, 93, 94, 95, 96, 97:
266 s.Style.Foreground = parts[i] - 90 + 8
267 s.Style.HasForegroundRGB = false
268 case 100, 101, 102, 103, 104, 105, 106, 107:
269 s.Style.Background = parts[i] - 100 + 8
270 s.Style.HasBackgroundRGB = false
271 case 38:
272 if i+2 < len(parts) && parts[i+1] == 5 {
273 s.Style.Foreground = parts[i+2]
274 s.Style.HasForegroundRGB = false
275 i += 2
276 } else if i+4 < len(parts) && parts[i+1] == 2 {
277 s.Style.Foreground = -1
278 s.Style.HasForegroundRGB = true
279 s.Style.ForegroundRGB = sgrRGB(parts[i+2], parts[i+3], parts[i+4])
280 i += 4
281 }
282 case 48:
283 if i+2 < len(parts) && parts[i+1] == 5 {
284 s.Style.Background = parts[i+2]
285 s.Style.HasBackgroundRGB = false
286 i += 2
287 } else if i+4 < len(parts) && parts[i+1] == 2 {
288 s.Style.Background = -1
289 s.Style.HasBackgroundRGB = true
290 s.Style.BackgroundRGB = sgrRGB(parts[i+2], parts[i+3], parts[i+4])
291 i += 4
292 }
293 case 58:
294 if i+2 < len(parts) && parts[i+1] == 5 {
295 s.Style.UnderlineColor = parts[i+2]
296 s.Style.HasUnderlineColor = true
297 s.Style.HasUnderlineColorRGB = false
298 i += 2
299 } else if i+4 < len(parts) && parts[i+1] == 2 {
300 s.Style.HasUnderlineColor = false
301 s.Style.HasUnderlineColorRGB = true
302 s.Style.UnderlineColorRGB = sgrRGB(parts[i+2], parts[i+3], parts[i+4])
303 i += 4
304 }
305 case 59:
306 s.Style.HasUnderlineColor = false
307 s.Style.HasUnderlineColorRGB = false
308 }
309 }
310}
311
312func sgrRGB(r, g, b int) renderer.RGB {
313 return renderer.RGB{
314 R: uint8(clamp(r, 0, 255)),
315 G: uint8(clamp(g, 0, 255)),
316 B: uint8(clamp(b, 0, 255)),
317 }
318}
319
320func sgrUnderlineStyle(param int) renderer.UnderlineStyle {
321 switch param {
322 case 0:
323 return renderer.UnderlineNone
324 case 2:
325 return renderer.UnderlineDouble
326 case 3:
327 return renderer.UnderlineCurly
328 case 4:
329 return renderer.UnderlineDotted
330 case 5:
331 return renderer.UnderlineDashed
332 default:
333 return renderer.UnderlineSingle
334 }
335}
336
337func (s *Screen) insertChars(n int) {
338 if s.Row < 0 || s.Row >= s.frame.Height || s.Col >= s.frame.Width || n <= 0 {
339 return
340 }
341 w := s.frame.Width
342 if n > w-s.Col {
343 n = w - s.Col
344 }
345 // A wide left half at Col-1 whose continuation sits at Col will be orphaned
346 // by the shift; its repair falls outside the default damage rect.
347 leftSplit := s.Col > 0 && s.frame.At(s.Col, s.Row).Continuation
348 s.frame.CopyRow(s.Row, s.Col+n, s.Col, w-s.Col-n)
349 s.frame.FillRow(s.Row, s.Col, s.Col+n, renderer.BlankCell())
350 s.repairRow(s.Row)
351 dmgX := s.Col
352 if leftSplit {
353 dmgX = s.Col - 1
354 }
355 s.record(renderer.Damage{Kind: renderer.DamageText, X: dmgX, Y: s.Row, Width: w - dmgX, Height: 1, Count: 1})
356}
357
358func (s *Screen) deleteChars(n int) {
359 if s.Row < 0 || s.Row >= s.frame.Height || s.Col >= s.frame.Width || n <= 0 {
360 return
361 }
362 w := s.frame.Width
363 if n > w-s.Col {
364 n = w - s.Col
365 }
366 // A wide left half at Col-1 whose continuation sits at Col will be orphaned
367 // by the shift; its repair falls outside the default damage rect.
368 leftSplit := s.Col > 0 && s.frame.At(s.Col, s.Row).Continuation
369 s.frame.CopyRow(s.Row, s.Col, s.Col+n, w-s.Col-n)
370 s.frame.FillRow(s.Row, w-n, w, renderer.BlankCell())
371 s.repairRow(s.Row)
372 dmgX := s.Col
373 if leftSplit {
374 dmgX = s.Col - 1
375 }
376 s.record(renderer.Damage{Kind: renderer.DamageText, X: dmgX, Y: s.Row, Width: w - dmgX, Height: 1, Count: 1})
377}
378
379func (s *Screen) insertLines(n int) {
380 if s.Row < s.scrollTop || s.Row > s.scrollBottom || n <= 0 {
381 return
382 }
383 s.scrollDownRegion(s.Row, s.scrollBottom, n)
384}
385
386func (s *Screen) deleteLines(n int) {
387 if s.Row < s.scrollTop || s.Row > s.scrollBottom || n <= 0 {
388 return
389 }
390 s.scrollUpRegion(s.Row, s.scrollBottom, n)
391}
392
393func (s *Screen) clearScreenMode(mode int) {
394 s.clampCursor()
395 switch mode {
396 case 1:
397 for y := range min(s.Row+1, s.frame.Height) {
398 end := s.frame.Width
399 if y == s.Row {
400 end = min(s.Col+1, s.frame.Width)
401 }
402 s.clearRow(y, 0, end)
403 }
404 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: 0, Width: s.frame.Width, Height: min(s.Row+1, s.frame.Height), Count: 1})
405 case 2, 3:
406 blank := s.eraseCell()
407 for y := range s.frame.Height {
408 s.frame.FillRow(y, 0, s.frame.Width, blank)
409 }
410 for y := range s.buffer.boundaries {
411 s.buffer.boundaries[y] = LineBound{Soft: s.buffer.boundaries[y].Soft}
412 s.buffer.rowIDs[y] = s.nextRowIDValue()
413 }
414 s.graphicsClearPlacements()
415 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: 0, Width: s.frame.Width, Height: s.frame.Height, Count: 1})
416 default:
417 for y := s.Row; y < s.frame.Height; y++ {
418 start := 0
419 if y == s.Row {
420 start = s.Col
421 }
422 s.clearRow(y, start, s.frame.Width)
423 }
424 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: s.Row, Width: s.frame.Width, Height: s.frame.Height - s.Row, Count: 1})
425 }
426}
427
428func (s *Screen) clearLineMode(mode int) {
429 s.clampCursor()
430 switch mode {
431 case 1:
432 start, width := s.clearRow(s.Row, 0, min(s.Col+1, s.frame.Width))
433 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
434 case 2:
435 start, width := s.clearRow(s.Row, 0, s.frame.Width)
436 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
437 default:
438 start, width := s.clearRow(s.Row, s.Col, s.frame.Width)
439 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
440 }
441}
442
443func (s *Screen) eraseChars(n int) {
444 if n <= 0 {
445 return
446 }
447 s.clampCursor()
448 if remaining := s.frame.Width - s.Col; n > remaining {
449 n = remaining
450 }
451 end := s.Col + n
452 start, width := s.clearRow(s.Row, s.Col, end)
453 if width > 0 {
454 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
455 }
456}
457
458func (s *Screen) clampCursor() {
459 s.Row = clamp(s.Row, 0, s.frame.Height-1)
460 s.Col = clamp(s.Col, 0, s.frame.Width-1)
461}
462
463func (s *Screen) parseCSIInts(params string) []int {
464 s.csiScratch = parseCSIIntsInto(s.csiScratch[:0], params)
465 return s.csiScratch
466}
467
468func parseCSIInts(params string) []int {
469 return parseCSIIntsInto(nil, params)
470}
471
472func parseCSIIntsInto(out []int, params string) []int {
473 if params == "" {
474 return out
475 }
476 params = strings.TrimPrefix(params, "?")
477 params = strings.TrimPrefix(params, ">")
478 start := 0
479 for i := 0; i <= len(params); i++ {
480 if i == len(params) || params[i] == ';' {
481 out = append(out, parseCSIInt(params[start:i]))
482 start = i + 1
483 }
484 }
485 return out
486}
487
488func (s *Screen) parseSGRInts(params string) []int {
489 s.sgrScratch = parseSGRIntsInto(s.sgrScratch[:0], params)
490 return s.sgrScratch
491}
492
493func parseSGRInts(params string) []int {
494 return parseSGRIntsInto(nil, params)
495}
496
497func parseSGRIntsInto(out []int, params string) []int {
498 if params == "" {
499 return out
500 }
501 start := 0
502 for i := 0; i <= len(params); i++ {
503 if i == len(params) || params[i] == ';' {
504 out = appendSGRGroup(out, params[start:i])
505 start = i + 1
506 }
507 }
508 return out
509}
510
511func appendSGRGroup(out []int, group string) []int {
512 colon := false
513 for i := 0; i < len(group); i++ {
514 if group[i] == ':' {
515 colon = true
516 break
517 }
518 }
519 if !colon {
520 return append(out, parseCSIInt(group))
521 }
522
523 parts := countSGRColonFields(group)
524 code := parseCSIInt(sgrColonField(group, 0))
525 if code == 4 {
526 if parts < 2 {
527 return append(out, code)
528 }
529 return append(out, code, sgrUnderlineStyleMarker, parseCSIInt(sgrColonField(group, 1)))
530 }
531 if code != 38 && code != 48 && code != 58 {
532 return append(out, code)
533 }
534 mode := 0
535 if parts > 1 {
536 mode = parseCSIInt(sgrColonField(group, 1))
537 }
538 switch mode {
539 case 5:
540 if parts < 3 {
541 return out
542 }
543 out = append(out, code, mode, parseCSIInt(sgrColonField(group, 2)))
544 case 2:
545 // code:mode::R:G:B and code:mode:cs:R:G:B both put RGB after
546 // the colorspace slot; code:mode:R:G:B omits that slot.
547 start := 2
548 if parts > 2 && (sgrColonField(group, 2) == "" || parts >= 6) {
549 start = 3
550 }
551 if parts < start+3 {
552 return out
553 }
554 out = append(out, code, mode)
555 for i := range 3 {
556 out = append(out, parseCSIInt(sgrColonField(group, start+i)))
557 }
558 }
559 return out
560}
561
562func countSGRColonFields(group string) int {
563 count := 1
564 for i := 0; i < len(group); i++ {
565 if group[i] == ':' {
566 count++
567 }
568 }
569 return count
570}
571
572func sgrColonField(group string, index int) string {
573 start := 0
574 field := 0
575 for i := 0; i <= len(group); i++ {
576 if i == len(group) || group[i] == ':' {
577 if field == index {
578 return group[start:i]
579 }
580 field++
581 start = i + 1
582 }
583 }
584 return ""
585}
586
587func parseCSIInt(param string) int {
588 if param == "" {
589 return 0
590 }
591 v, err := strconv.Atoi(param)
592 if err != nil {
593 return 0
594 }
595 return v
596}