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 return
160 }
161 s.applyKittyKeyboard(params)
162 case 'h':
163 s.setMode(private, parts, true)
164 case 'l':
165 s.setMode(private, parts, false)
166 }
167}
168
169func firstPositive(parts []int, fallback int) int {
170 if len(parts) == 0 || parts[0] <= 0 {
171 return fallback
172 }
173 return parts[0]
174}
175
176func (s *Screen) modeReportState(private bool, mode int) int {
177 if private {
178 switch mode {
179 case 1:
180 return boolModeReportState(s.applicationCursorMode)
181 case 6:
182 return boolModeReportState(s.originMode)
183 case 7:
184 return boolModeReportState(s.autoWrapMode)
185 case 2026:
186 return boolModeReportState(s.syncUpdateActive)
187 case 2031:
188 return boolModeReportState(s.colorSchemeMode)
189 }
190 return 0
191 }
192 if mode == 4 {
193 return boolModeReportState(s.insertMode)
194 }
195 return 0
196}
197
198func boolModeReportState(enabled bool) int {
199 if enabled {
200 return 1
201 }
202 return 2
203}
204
205const sgrUnderlineStyleMarker = -1000
206
207func (s *Screen) applySGR(params string) {
208 if params == "" {
209 s.Style = renderer.DefaultStyle()
210 return
211 }
212 parts := s.parseSGRInts(params)
213 for i := 0; i < len(parts); i++ {
214 switch parts[i] {
215 case 0:
216 s.Style = renderer.DefaultStyle()
217 case 1:
218 s.Style.Bold = true
219 case 2:
220 s.Style.Attrs |= renderer.AttrDim
221 case 3:
222 s.Style.Italic = true
223 case 4:
224 s.Style.Attrs |= renderer.AttrUnderline
225 s.Style.UnderlineStyle = renderer.UnderlineSingle
226 if i+2 < len(parts) && parts[i+1] == sgrUnderlineStyleMarker {
227 underlineStyle := sgrUnderlineStyle(parts[i+2])
228 if underlineStyle == renderer.UnderlineNone {
229 s.Style.Attrs &^= renderer.AttrUnderline
230 } else {
231 s.Style.Attrs |= renderer.AttrUnderline
232 }
233 s.Style.UnderlineStyle = underlineStyle
234 i += 2
235 }
236 case 5, 6:
237 s.Style.Attrs |= renderer.AttrBlink
238 case 7:
239 s.Style.Inverse = true
240 case 9:
241 s.Style.Attrs |= renderer.AttrStrikethrough
242 case 21:
243 s.Style.Attrs |= renderer.AttrUnderline
244 s.Style.UnderlineStyle = renderer.UnderlineDouble
245 case 22:
246 s.Style.Bold = false
247 s.Style.Attrs &^= renderer.AttrDim
248 case 23:
249 s.Style.Italic = false
250 case 24:
251 s.Style.Attrs &^= renderer.AttrUnderline
252 s.Style.UnderlineStyle = renderer.UnderlineNone
253 case 25:
254 s.Style.Attrs &^= renderer.AttrBlink
255 case 27:
256 s.Style.Inverse = false
257 case 29:
258 s.Style.Attrs &^= renderer.AttrStrikethrough
259 case 30, 31, 32, 33, 34, 35, 36, 37:
260 s.Style.Foreground = parts[i] - 30
261 s.Style.HasForegroundRGB = false
262 case 39:
263 s.Style.Foreground = -1
264 s.Style.HasForegroundRGB = false
265 case 40, 41, 42, 43, 44, 45, 46, 47:
266 s.Style.Background = parts[i] - 40
267 s.Style.HasBackgroundRGB = false
268 case 49:
269 s.Style.Background = -1
270 s.Style.HasBackgroundRGB = false
271 case 90, 91, 92, 93, 94, 95, 96, 97:
272 s.Style.Foreground = parts[i] - 90 + 8
273 s.Style.HasForegroundRGB = false
274 case 100, 101, 102, 103, 104, 105, 106, 107:
275 s.Style.Background = parts[i] - 100 + 8
276 s.Style.HasBackgroundRGB = false
277 case 38:
278 if i+2 < len(parts) && parts[i+1] == 5 {
279 s.Style.Foreground = parts[i+2]
280 s.Style.HasForegroundRGB = false
281 i += 2
282 } else if i+4 < len(parts) && parts[i+1] == 2 {
283 s.Style.Foreground = -1
284 s.Style.HasForegroundRGB = true
285 s.Style.ForegroundRGB = sgrRGB(parts[i+2], parts[i+3], parts[i+4])
286 i += 4
287 }
288 case 48:
289 if i+2 < len(parts) && parts[i+1] == 5 {
290 s.Style.Background = parts[i+2]
291 s.Style.HasBackgroundRGB = false
292 i += 2
293 } else if i+4 < len(parts) && parts[i+1] == 2 {
294 s.Style.Background = -1
295 s.Style.HasBackgroundRGB = true
296 s.Style.BackgroundRGB = sgrRGB(parts[i+2], parts[i+3], parts[i+4])
297 i += 4
298 }
299 case 58:
300 if i+2 < len(parts) && parts[i+1] == 5 {
301 s.Style.UnderlineColor = parts[i+2]
302 s.Style.HasUnderlineColor = true
303 s.Style.HasUnderlineColorRGB = false
304 i += 2
305 } else if i+4 < len(parts) && parts[i+1] == 2 {
306 s.Style.HasUnderlineColor = false
307 s.Style.HasUnderlineColorRGB = true
308 s.Style.UnderlineColorRGB = sgrRGB(parts[i+2], parts[i+3], parts[i+4])
309 i += 4
310 }
311 case 59:
312 s.Style.HasUnderlineColor = false
313 s.Style.HasUnderlineColorRGB = false
314 }
315 }
316}
317
318func sgrRGB(r, g, b int) renderer.RGB {
319 return renderer.RGB{
320 R: uint8(clamp(r, 0, 255)),
321 G: uint8(clamp(g, 0, 255)),
322 B: uint8(clamp(b, 0, 255)),
323 }
324}
325
326func sgrUnderlineStyle(param int) renderer.UnderlineStyle {
327 switch param {
328 case 0:
329 return renderer.UnderlineNone
330 case 2:
331 return renderer.UnderlineDouble
332 case 3:
333 return renderer.UnderlineCurly
334 case 4:
335 return renderer.UnderlineDotted
336 case 5:
337 return renderer.UnderlineDashed
338 default:
339 return renderer.UnderlineSingle
340 }
341}
342
343func (s *Screen) insertChars(n int) {
344 if s.Row < 0 || s.Row >= s.frame.Height || s.Col >= s.frame.Width || n <= 0 {
345 return
346 }
347 w := s.frame.Width
348 if n > w-s.Col {
349 n = w - s.Col
350 }
351 // A wide left half at Col-1 whose continuation sits at Col will be orphaned
352 // by the shift; its repair falls outside the default damage rect.
353 leftSplit := s.Col > 0 && s.frame.At(s.Col, s.Row).Continuation
354 s.frame.CopyRow(s.Row, s.Col+n, s.Col, w-s.Col-n)
355 s.frame.FillRow(s.Row, s.Col, s.Col+n, renderer.BlankCell())
356 s.repairRow(s.Row)
357 dmgX := s.Col
358 if leftSplit {
359 dmgX = s.Col - 1
360 }
361 s.record(renderer.Damage{Kind: renderer.DamageText, X: dmgX, Y: s.Row, Width: w - dmgX, Height: 1, Count: 1})
362}
363
364func (s *Screen) deleteChars(n int) {
365 if s.Row < 0 || s.Row >= s.frame.Height || s.Col >= s.frame.Width || n <= 0 {
366 return
367 }
368 w := s.frame.Width
369 if n > w-s.Col {
370 n = w - s.Col
371 }
372 // A wide left half at Col-1 whose continuation sits at Col will be orphaned
373 // by the shift; its repair falls outside the default damage rect.
374 leftSplit := s.Col > 0 && s.frame.At(s.Col, s.Row).Continuation
375 s.frame.CopyRow(s.Row, s.Col, s.Col+n, w-s.Col-n)
376 s.frame.FillRow(s.Row, w-n, w, renderer.BlankCell())
377 s.repairRow(s.Row)
378 dmgX := s.Col
379 if leftSplit {
380 dmgX = s.Col - 1
381 }
382 s.record(renderer.Damage{Kind: renderer.DamageText, X: dmgX, Y: s.Row, Width: w - dmgX, Height: 1, Count: 1})
383}
384
385func (s *Screen) insertLines(n int) {
386 if s.Row < s.scrollTop || s.Row > s.scrollBottom || n <= 0 {
387 return
388 }
389 s.scrollDownRegion(s.Row, s.scrollBottom, n)
390}
391
392func (s *Screen) deleteLines(n int) {
393 if s.Row < s.scrollTop || s.Row > s.scrollBottom || n <= 0 {
394 return
395 }
396 s.scrollUpRegion(s.Row, s.scrollBottom, n)
397}
398
399func (s *Screen) clearScreenMode(mode int) {
400 s.clampCursor()
401 switch mode {
402 case 1:
403 for y := range min(s.Row+1, s.frame.Height) {
404 end := s.frame.Width
405 if y == s.Row {
406 end = min(s.Col+1, s.frame.Width)
407 }
408 s.clearRow(y, 0, end)
409 }
410 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})
411 case 2, 3:
412 blank := s.eraseCell()
413 for y := range s.frame.Height {
414 s.frame.FillRow(y, 0, s.frame.Width, blank)
415 }
416 for y := range s.buffer.boundaries {
417 s.buffer.boundaries[y] = LineBound{Soft: s.buffer.boundaries[y].Soft}
418 s.buffer.rowIDs[y] = s.nextRowIDValue()
419 }
420 s.graphicsClearPlacements()
421 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: 0, Width: s.frame.Width, Height: s.frame.Height, Count: 1})
422 default:
423 for y := s.Row; y < s.frame.Height; y++ {
424 start := 0
425 if y == s.Row {
426 start = s.Col
427 }
428 s.clearRow(y, start, s.frame.Width)
429 }
430 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: s.Row, Width: s.frame.Width, Height: s.frame.Height - s.Row, Count: 1})
431 }
432}
433
434func (s *Screen) clearLineMode(mode int) {
435 s.clampCursor()
436 switch mode {
437 case 1:
438 start, width := s.clearRow(s.Row, 0, min(s.Col+1, s.frame.Width))
439 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
440 case 2:
441 start, width := s.clearRow(s.Row, 0, s.frame.Width)
442 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
443 default:
444 start, width := s.clearRow(s.Row, s.Col, s.frame.Width)
445 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
446 }
447}
448
449func (s *Screen) eraseChars(n int) {
450 if n <= 0 {
451 return
452 }
453 s.clampCursor()
454 if remaining := s.frame.Width - s.Col; n > remaining {
455 n = remaining
456 }
457 end := s.Col + n
458 start, width := s.clearRow(s.Row, s.Col, end)
459 if width > 0 {
460 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
461 }
462}
463
464func (s *Screen) clampCursor() {
465 s.Row = clamp(s.Row, 0, s.frame.Height-1)
466 s.Col = clamp(s.Col, 0, s.frame.Width-1)
467}
468
469func (s *Screen) parseCSIInts(params string) []int {
470 s.csiScratch = parseCSIIntsInto(s.csiScratch[:0], params)
471 return s.csiScratch
472}
473
474func parseCSIInts(params string) []int {
475 return parseCSIIntsInto(nil, params)
476}
477
478func parseCSIIntsInto(out []int, params string) []int {
479 if params == "" {
480 return out
481 }
482 params = strings.TrimPrefix(params, "?")
483 params = strings.TrimPrefix(params, ">")
484 start := 0
485 for i := 0; i <= len(params); i++ {
486 if i == len(params) || params[i] == ';' {
487 out = append(out, parseCSIInt(params[start:i]))
488 start = i + 1
489 }
490 }
491 return out
492}
493
494func (s *Screen) parseSGRInts(params string) []int {
495 s.sgrScratch = parseSGRIntsInto(s.sgrScratch[:0], params)
496 return s.sgrScratch
497}
498
499func parseSGRInts(params string) []int {
500 return parseSGRIntsInto(nil, params)
501}
502
503func parseSGRIntsInto(out []int, params string) []int {
504 if params == "" {
505 return out
506 }
507 start := 0
508 for i := 0; i <= len(params); i++ {
509 if i == len(params) || params[i] == ';' {
510 out = appendSGRGroup(out, params[start:i])
511 start = i + 1
512 }
513 }
514 return out
515}
516
517func appendSGRGroup(out []int, group string) []int {
518 colon := false
519 for i := 0; i < len(group); i++ {
520 if group[i] == ':' {
521 colon = true
522 break
523 }
524 }
525 if !colon {
526 return append(out, parseCSIInt(group))
527 }
528
529 parts := countSGRColonFields(group)
530 code := parseCSIInt(sgrColonField(group, 0))
531 if code == 4 {
532 if parts < 2 {
533 return append(out, code)
534 }
535 return append(out, code, sgrUnderlineStyleMarker, parseCSIInt(sgrColonField(group, 1)))
536 }
537 if code != 38 && code != 48 && code != 58 {
538 return append(out, code)
539 }
540 mode := 0
541 if parts > 1 {
542 mode = parseCSIInt(sgrColonField(group, 1))
543 }
544 switch mode {
545 case 5:
546 if parts < 3 {
547 return out
548 }
549 out = append(out, code, mode, parseCSIInt(sgrColonField(group, 2)))
550 case 2:
551 // code:mode::R:G:B and code:mode:cs:R:G:B both put RGB after
552 // the colorspace slot; code:mode:R:G:B omits that slot.
553 start := 2
554 if parts > 2 && (sgrColonField(group, 2) == "" || parts >= 6) {
555 start = 3
556 }
557 if parts < start+3 {
558 return out
559 }
560 out = append(out, code, mode)
561 for i := range 3 {
562 out = append(out, parseCSIInt(sgrColonField(group, start+i)))
563 }
564 }
565 return out
566}
567
568func countSGRColonFields(group string) int {
569 count := 1
570 for i := 0; i < len(group); i++ {
571 if group[i] == ':' {
572 count++
573 }
574 }
575 return count
576}
577
578func sgrColonField(group string, index int) string {
579 start := 0
580 field := 0
581 for i := 0; i <= len(group); i++ {
582 if i == len(group) || group[i] == ':' {
583 if field == index {
584 return group[start:i]
585 }
586 field++
587 start = i + 1
588 }
589 }
590 return ""
591}
592
593func parseCSIInt(param string) int {
594 if param == "" {
595 return 0
596 }
597 v, err := strconv.Atoi(param)
598 if err != nil {
599 return 0
600 }
601 return v
602}