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