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