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