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 row := s.Frame.Row(s.Row)
346 // A wide left half at Col-1 whose continuation sits at Col will be orphaned
347 // by the shift; its repair falls outside the default damage rect.
348 leftSplit := s.Col > 0 && row[s.Col].Continuation
349 for x := w - 1; x >= s.Col+n; x-- {
350 row[x] = row[x-n]
351 }
352 for x := s.Col; x < s.Col+n; x++ {
353 row[x] = renderer.BlankCell()
354 }
355 s.repairRow(s.Row)
356 dmgX := s.Col
357 if leftSplit {
358 dmgX = s.Col - 1
359 }
360 s.record(renderer.Damage{Kind: renderer.DamageText, X: dmgX, Y: s.Row, Width: w - dmgX, Height: 1, Count: 1})
361}
362
363func (s *Screen) deleteChars(n int) {
364 if s.Row < 0 || s.Row >= s.Frame.Height || s.Col >= s.Frame.Width || n <= 0 {
365 return
366 }
367 w := s.Frame.Width
368 if n > w-s.Col {
369 n = w - s.Col
370 }
371 row := s.Frame.Row(s.Row)
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 && row[s.Col].Continuation
375 for x := s.Col; x < w-n; x++ {
376 row[x] = row[x+n]
377 }
378 for x := w - n; x < w; x++ {
379 row[x] = renderer.BlankCell()
380 }
381 s.repairRow(s.Row)
382 dmgX := s.Col
383 if leftSplit {
384 dmgX = s.Col - 1
385 }
386 s.record(renderer.Damage{Kind: renderer.DamageText, X: dmgX, Y: s.Row, Width: w - dmgX, Height: 1, Count: 1})
387}
388
389func (s *Screen) insertLines(n int) {
390 if s.Row < s.scrollTop || s.Row > s.scrollBottom || n <= 0 {
391 return
392 }
393 s.scrollDownRegion(s.Row, s.scrollBottom, n)
394}
395
396func (s *Screen) deleteLines(n int) {
397 if s.Row < s.scrollTop || s.Row > s.scrollBottom || n <= 0 {
398 return
399 }
400 s.scrollUpRegion(s.Row, s.scrollBottom, n)
401}
402
403func (s *Screen) clearScreenMode(mode int) {
404 s.clampCursor()
405 switch mode {
406 case 1:
407 for y := range min(s.Row+1, s.Frame.Height) {
408 end := s.Frame.Width
409 if y == s.Row {
410 end = min(s.Col+1, s.Frame.Width)
411 }
412 s.clearRow(y, 0, end)
413 }
414 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})
415 case 2, 3:
416 blank := s.eraseCell()
417 for i := range s.Frame.Cells {
418 s.Frame.Cells[i] = blank
419 }
420 for y := range s.buffer.boundaries {
421 s.buffer.boundaries[y] = LineBound{Soft: s.buffer.boundaries[y].Soft}
422 s.buffer.rowIDs[y] = s.nextRowIDValue()
423 }
424 s.graphicsClearPlacements()
425 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: 0, Width: s.Frame.Width, Height: s.Frame.Height, Count: 1})
426 default:
427 for y := s.Row; y < s.Frame.Height; y++ {
428 start := 0
429 if y == s.Row {
430 start = s.Col
431 }
432 s.clearRow(y, start, s.Frame.Width)
433 }
434 s.record(renderer.Damage{Kind: renderer.DamageClear, X: 0, Y: s.Row, Width: s.Frame.Width, Height: s.Frame.Height - s.Row, Count: 1})
435 }
436}
437
438func (s *Screen) clearLineMode(mode int) {
439 s.clampCursor()
440 switch mode {
441 case 1:
442 start, width := s.clearRow(s.Row, 0, min(s.Col+1, s.Frame.Width))
443 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
444 case 2:
445 start, width := s.clearRow(s.Row, 0, s.Frame.Width)
446 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
447 default:
448 start, width := s.clearRow(s.Row, s.Col, s.Frame.Width)
449 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
450 }
451}
452
453func (s *Screen) eraseChars(n int) {
454 if n <= 0 {
455 return
456 }
457 s.clampCursor()
458 if remaining := s.Frame.Width - s.Col; n > remaining {
459 n = remaining
460 }
461 end := s.Col + n
462 start, width := s.clearRow(s.Row, s.Col, end)
463 if width > 0 {
464 s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1})
465 }
466}
467
468func (s *Screen) clampCursor() {
469 s.Row = clamp(s.Row, 0, s.Frame.Height-1)
470 s.Col = clamp(s.Col, 0, s.Frame.Width-1)
471}
472
473func (s *Screen) parseCSIInts(params string) []int {
474 s.csiScratch = parseCSIIntsInto(s.csiScratch[:0], params)
475 return s.csiScratch
476}
477
478func parseCSIInts(params string) []int {
479 return parseCSIIntsInto(nil, params)
480}
481
482func parseCSIIntsInto(out []int, params string) []int {
483 if params == "" {
484 return out
485 }
486 params = strings.TrimPrefix(params, "?")
487 params = strings.TrimPrefix(params, ">")
488 start := 0
489 for i := 0; i <= len(params); i++ {
490 if i == len(params) || params[i] == ';' {
491 out = append(out, parseCSIInt(params[start:i]))
492 start = i + 1
493 }
494 }
495 return out
496}
497
498func (s *Screen) parseSGRInts(params string) []int {
499 s.sgrScratch = parseSGRIntsInto(s.sgrScratch[:0], params)
500 return s.sgrScratch
501}
502
503func parseSGRInts(params string) []int {
504 return parseSGRIntsInto(nil, params)
505}
506
507func parseSGRIntsInto(out []int, params string) []int {
508 if params == "" {
509 return out
510 }
511 start := 0
512 for i := 0; i <= len(params); i++ {
513 if i == len(params) || params[i] == ';' {
514 out = appendSGRGroup(out, params[start:i])
515 start = i + 1
516 }
517 }
518 return out
519}
520
521func appendSGRGroup(out []int, group string) []int {
522 colon := false
523 for i := 0; i < len(group); i++ {
524 if group[i] == ':' {
525 colon = true
526 break
527 }
528 }
529 if !colon {
530 return append(out, parseCSIInt(group))
531 }
532
533 parts := countSGRColonFields(group)
534 code := parseCSIInt(sgrColonField(group, 0))
535 if code == 4 {
536 if parts < 2 {
537 return append(out, code)
538 }
539 return append(out, code, sgrUnderlineStyleMarker, parseCSIInt(sgrColonField(group, 1)))
540 }
541 if code != 38 && code != 48 && code != 58 {
542 return append(out, code)
543 }
544 mode := 0
545 if parts > 1 {
546 mode = parseCSIInt(sgrColonField(group, 1))
547 }
548 switch mode {
549 case 5:
550 if parts < 3 {
551 return out
552 }
553 out = append(out, code, mode, parseCSIInt(sgrColonField(group, 2)))
554 case 2:
555 // code:mode::R:G:B and code:mode:cs:R:G:B both put RGB after
556 // the colorspace slot; code:mode:R:G:B omits that slot.
557 start := 2
558 if parts > 2 && (sgrColonField(group, 2) == "" || parts >= 6) {
559 start = 3
560 }
561 if parts < start+3 {
562 return out
563 }
564 out = append(out, code, mode)
565 for i := range 3 {
566 out = append(out, parseCSIInt(sgrColonField(group, start+i)))
567 }
568 }
569 return out
570}
571
572func countSGRColonFields(group string) int {
573 count := 1
574 for i := 0; i < len(group); i++ {
575 if group[i] == ':' {
576 count++
577 }
578 }
579 return count
580}
581
582func sgrColonField(group string, index int) string {
583 start := 0
584 field := 0
585 for i := 0; i <= len(group); i++ {
586 if i == len(group) || group[i] == ':' {
587 if field == index {
588 return group[start:i]
589 }
590 field++
591 start = i + 1
592 }
593 }
594 return ""
595}
596
597func parseCSIInt(param string) int {
598 if param == "" {
599 return 0
600 }
601 v, err := strconv.Atoi(param)
602 if err != nil {
603 return 0
604 }
605 return v
606}