v0.4.0 0b9cfda

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			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}

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