detached ab6686c

screen_csi.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close