detached 5852a41

no-mistakes(review): Fix VT mode query integration

commit
5852a418d116387c2a7c6ff6ddb2e3d2cc9763cf
author
brice · · 2026-07-09 15:10 UTC
parent
532e4e5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +80 -22
M pkg/vt/query_test.go +5 -0
@@ -21,6 +21,11 @@ func TestScreenQueryResponses(t *testing.T) {
2121 {name: "CPR at home", input: "\x1b[6n", want: "\x1b[1;1R"},
2222 {name: "CPR after CUP", input: "\x1b[3;7H\x1b[6n", want: "\x1b[3;7R"},
2323 {name: "private CPR", input: "\x1b[3;7H\x1b[?6n", want: "\x1b[?3;7R"},
24+ {name: "CPR in DECOM reports origin-relative row", input: "\x1b[2;4r\x1b[?6h\x1b[6n", want: "\x1b[1;1R"},
25+ {name: "DECRQM DECOM reset", input: "\x1b[?6$p", want: "\x1b[?6;2$y"},
26+ {name: "DECRQM DECOM set", input: "\x1b[?6h\x1b[?6$p", want: "\x1b[?6;1$y"},
27+ {name: "DECRQM IRM reset", input: "\x1b[4$p", want: "\x1b[4;2$y"},
28+ {name: "DECRQM IRM set", input: "\x1b[4h\x1b[4$p", want: "\x1b[4;1$y"},
2429 {name: "DECRQM 2026 reset", input: "\x1b[?2026$p", want: "\x1b[?2026;2$y"},
2530 {name: "DECRQM 2026 set", input: "\x1b[?2026h\x1b[?2026$p", want: "\x1b[?2026;1$y"},
2631 {name: "DECRQM 2031 reset", input: "\x1b[?2031$p", want: "\x1b[?2031;2$y"},
@@ -21,6 +21,11 @@ func TestScreenQueryResponses(t *testing.T) {
21 {name: "CPR at home", input: "\x1b[6n", want: "\x1b[1;1R"},21 {name: "CPR at home", input: "\x1b[6n", want: "\x1b[1;1R"},
22 {name: "CPR after CUP", input: "\x1b[3;7H\x1b[6n", want: "\x1b[3;7R"},22 {name: "CPR after CUP", input: "\x1b[3;7H\x1b[6n", want: "\x1b[3;7R"},
23 {name: "private CPR", input: "\x1b[3;7H\x1b[?6n", want: "\x1b[?3;7R"},23 {name: "private CPR", input: "\x1b[3;7H\x1b[?6n", want: "\x1b[?3;7R"},
24+ {name: "CPR in DECOM reports origin-relative row", input: "\x1b[2;4r\x1b[?6h\x1b[6n", want: "\x1b[1;1R"},
25+ {name: "DECRQM DECOM reset", input: "\x1b[?6$p", want: "\x1b[?6;2$y"},
26+ {name: "DECRQM DECOM set", input: "\x1b[?6h\x1b[?6$p", want: "\x1b[?6;1$y"},
27+ {name: "DECRQM IRM reset", input: "\x1b[4$p", want: "\x1b[4;2$y"},
28+ {name: "DECRQM IRM set", input: "\x1b[4h\x1b[4$p", want: "\x1b[4;1$y"},
24 {name: "DECRQM 2026 reset", input: "\x1b[?2026$p", want: "\x1b[?2026;2$y"},29 {name: "DECRQM 2026 reset", input: "\x1b[?2026$p", want: "\x1b[?2026;2$y"},
25 {name: "DECRQM 2026 set", input: "\x1b[?2026h\x1b[?2026$p", want: "\x1b[?2026;1$y"},30 {name: "DECRQM 2026 set", input: "\x1b[?2026h\x1b[?2026$p", want: "\x1b[?2026;1$y"},
26 {name: "DECRQM 2031 reset", input: "\x1b[?2031$p", want: "\x1b[?2031;2$y"},31 {name: "DECRQM 2031 reset", input: "\x1b[?2031$p", want: "\x1b[?2031;2$y"},
M pkg/vt/screen.go +52 -22
@@ -9,10 +9,12 @@ import (
99 )
1010
1111 type cursorState struct {
12- row int
13- col int
14- style renderer.Style
15- saved bool
12+ row int
13+ col int
14+ style renderer.Style
15+ originMode bool
16+ insertMode bool
17+ saved bool
1618 }
1719
1820 type screenState struct {
@@ -843,33 +845,27 @@ func (s *Screen) applyCSI(params string, cmd byte) {
843845 if private {
844846 resp = append(resp, '?')
845847 }
846- resp = strconv.AppendInt(resp, int64(s.Row+1), 10)
848+ resp = strconv.AppendInt(resp, int64(s.cursorReportRow()), 10)
847849 resp = append(resp, ';')
848850 resp = strconv.AppendInt(resp, int64(s.Col+1), 10)
849851 resp = append(resp, 'R')
850852 s.respond(resp)
851853 }
852854 case 'p':
853- if private && strings.HasSuffix(params, "$") {
854- mode, err := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(params, "?"), "$"))
855+ if modeText, ok := strings.CutSuffix(params, "$"); ok {
856+ if private {
857+ modeText = strings.TrimPrefix(modeText, "?")
858+ }
859+ mode, err := strconv.Atoi(modeText)
855860 if err != nil {
856861 return
857862 }
858- state := 0
859- switch mode {
860- case 2026:
861- state = 2
862- if s.syncUpdateActive {
863- state = 1
864- }
865- case 2031:
866- state = 2
867- if s.colorSchemeMode {
868- state = 1
869- }
870- }
863+ state := s.modeReportState(private, mode)
871864 resp := make([]byte, 0, 16)
872- resp = append(resp, "\x1b[?"...)
865+ resp = append(resp, "\x1b["...)
866+ if private {
867+ resp = append(resp, '?')
868+ }
873869 resp = strconv.AppendInt(resp, int64(mode), 10)
874870 resp = append(resp, ';')
875871 resp = strconv.AppendInt(resp, int64(state), 10)
@@ -958,6 +954,38 @@ func firstPositive(parts []int, fallback int) int {
958954 return parts[0]
959955 }
960956
957+func (s *Screen) cursorReportRow() int {
958+ if s.originMode {
959+ return clamp(s.Row, s.cursorMinRow(), s.cursorMaxRow()) - s.cursorMinRow() + 1
960+ }
961+ return s.Row + 1
962+}
963+
964+func (s *Screen) modeReportState(private bool, mode int) int {
965+ if private {
966+ switch mode {
967+ case 6:
968+ return boolModeReportState(s.originMode)
969+ case 2026:
970+ return boolModeReportState(s.syncUpdateActive)
971+ case 2031:
972+ return boolModeReportState(s.colorSchemeMode)
973+ }
974+ return 0
975+ }
976+ if mode == 4 {
977+ return boolModeReportState(s.insertMode)
978+ }
979+ return 0
980+}
981+
982+func boolModeReportState(enabled bool) int {
983+ if enabled {
984+ return 1
985+ }
986+ return 2
987+}
988+
961989 func (s *Screen) cursorMinRow() int {
962990 if s.originMode {
963991 return clamp(s.scrollTop, 0, s.Frame.Height-1)
@@ -1083,7 +1111,7 @@ func sgrRGB(r, g, b int) renderer.RGB {
10831111 }
10841112
10851113 func (s *Screen) saveCursor() {
1086- s.savedCursor = cursorState{row: s.Row, col: s.Col, style: s.Style, saved: true}
1114+ s.savedCursor = cursorState{row: s.Row, col: s.Col, style: s.Style, originMode: s.originMode, insertMode: s.insertMode, saved: true}
10871115 }
10881116
10891117 func (s *Screen) restoreCursor() {
@@ -1093,6 +1121,8 @@ func (s *Screen) restoreCursor() {
10931121 s.Row = clamp(s.savedCursor.row, 0, s.Frame.Height-1)
10941122 s.Col = clamp(s.savedCursor.col, 0, s.Frame.Width-1)
10951123 s.Style = s.savedCursor.style
1124+ s.originMode = s.savedCursor.originMode
1125+ s.insertMode = s.savedCursor.insertMode
10961126 }
10971127
10981128 func (s *Screen) reset() {
@@ -9,10 +9,12 @@ import (
9 )9 )
10 10
11 type cursorState struct {11 type cursorState struct {
12- row int12+ row int
13- col int13+ col int
14- style renderer.Style14+ style renderer.Style
15- saved bool15+ originMode bool
16+ insertMode bool
17+ saved bool
16 }18 }
17 19
18 type screenState struct {20 type screenState struct {
@@ -843,33 +845,27 @@ func (s *Screen) applyCSI(params string, cmd byte) {
843 if private {845 if private {
844 resp = append(resp, '?')846 resp = append(resp, '?')
845 }847 }
846- resp = strconv.AppendInt(resp, int64(s.Row+1), 10)848+ resp = strconv.AppendInt(resp, int64(s.cursorReportRow()), 10)
847 resp = append(resp, ';')849 resp = append(resp, ';')
848 resp = strconv.AppendInt(resp, int64(s.Col+1), 10)850 resp = strconv.AppendInt(resp, int64(s.Col+1), 10)
849 resp = append(resp, 'R')851 resp = append(resp, 'R')
850 s.respond(resp)852 s.respond(resp)
851 }853 }
852 case 'p':854 case 'p':
853- if private && strings.HasSuffix(params, "$") {855+ if modeText, ok := strings.CutSuffix(params, "$"); ok {
854- mode, err := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(params, "?"), "$"))856+ if private {
857+ modeText = strings.TrimPrefix(modeText, "?")
858+ }
859+ mode, err := strconv.Atoi(modeText)
855 if err != nil {860 if err != nil {
856 return861 return
857 }862 }
858- state := 0863+ state := s.modeReportState(private, mode)
859- switch mode {
860- case 2026:
861- state = 2
862- if s.syncUpdateActive {
863- state = 1
864- }
865- case 2031:
866- state = 2
867- if s.colorSchemeMode {
868- state = 1
869- }
870- }
871 resp := make([]byte, 0, 16)864 resp := make([]byte, 0, 16)
872- resp = append(resp, "\x1b[?"...)865+ resp = append(resp, "\x1b["...)
866+ if private {
867+ resp = append(resp, '?')
868+ }
873 resp = strconv.AppendInt(resp, int64(mode), 10)869 resp = strconv.AppendInt(resp, int64(mode), 10)
874 resp = append(resp, ';')870 resp = append(resp, ';')
875 resp = strconv.AppendInt(resp, int64(state), 10)871 resp = strconv.AppendInt(resp, int64(state), 10)
@@ -958,6 +954,38 @@ func firstPositive(parts []int, fallback int) int {
958 return parts[0]954 return parts[0]
959 }955 }
960 956
957+func (s *Screen) cursorReportRow() int {
958+ if s.originMode {
959+ return clamp(s.Row, s.cursorMinRow(), s.cursorMaxRow()) - s.cursorMinRow() + 1
960+ }
961+ return s.Row + 1
962+}
963+
964+func (s *Screen) modeReportState(private bool, mode int) int {
965+ if private {
966+ switch mode {
967+ case 6:
968+ return boolModeReportState(s.originMode)
969+ case 2026:
970+ return boolModeReportState(s.syncUpdateActive)
971+ case 2031:
972+ return boolModeReportState(s.colorSchemeMode)
973+ }
974+ return 0
975+ }
976+ if mode == 4 {
977+ return boolModeReportState(s.insertMode)
978+ }
979+ return 0
980+}
981+
982+func boolModeReportState(enabled bool) int {
983+ if enabled {
984+ return 1
985+ }
986+ return 2
987+}
988+
961 func (s *Screen) cursorMinRow() int {989 func (s *Screen) cursorMinRow() int {
962 if s.originMode {990 if s.originMode {
963 return clamp(s.scrollTop, 0, s.Frame.Height-1)991 return clamp(s.scrollTop, 0, s.Frame.Height-1)
@@ -1083,7 +1111,7 @@ func sgrRGB(r, g, b int) renderer.RGB {
1083 }1111 }
1084 1112
1085 func (s *Screen) saveCursor() {1113 func (s *Screen) saveCursor() {
1086- s.savedCursor = cursorState{row: s.Row, col: s.Col, style: s.Style, saved: true}1114+ s.savedCursor = cursorState{row: s.Row, col: s.Col, style: s.Style, originMode: s.originMode, insertMode: s.insertMode, saved: true}
1087 }1115 }
1088 1116
1089 func (s *Screen) restoreCursor() {1117 func (s *Screen) restoreCursor() {
@@ -1093,6 +1121,8 @@ func (s *Screen) restoreCursor() {
1093 s.Row = clamp(s.savedCursor.row, 0, s.Frame.Height-1)1121 s.Row = clamp(s.savedCursor.row, 0, s.Frame.Height-1)
1094 s.Col = clamp(s.savedCursor.col, 0, s.Frame.Width-1)1122 s.Col = clamp(s.savedCursor.col, 0, s.Frame.Width-1)
1095 s.Style = s.savedCursor.style1123 s.Style = s.savedCursor.style
1124+ s.originMode = s.savedCursor.originMode
1125+ s.insertMode = s.savedCursor.insertMode
1096 }1126 }
1097 1127
1098 func (s *Screen) reset() {1128 func (s *Screen) reset() {
M pkg/vt/screen_test.go +23 -0
@@ -1196,6 +1196,29 @@ func TestESCSequences(t *testing.T) {
11961196 }
11971197 },
11981198 },
1199+ {
1200+ name: "ESC 7 / ESC 8 restore origin and insert modes",
1201+ run: func(t *testing.T) {
1202+ s := NewScreen(6, 5)
1203+ s.Write([]byte("\x1b[2;4r\x1b[?6h\x1b[4h\x1b7"))
1204+ s.Write([]byte("\x1b[?6l\x1b[4l\x1b8"))
1205+
1206+ if !s.originMode {
1207+ t.Fatal("origin mode was not restored")
1208+ }
1209+ if !s.insertMode {
1210+ t.Fatal("insert mode was not restored")
1211+ }
1212+
1213+ s.Write([]byte("\x1b[1;1Habcd\x1b[1;3HXY"))
1214+ if s.Row != 1 || s.Col != 4 {
1215+ t.Fatalf("cursor after restored origin and insert modes = (%d,%d), want (1,4)", s.Row, s.Col)
1216+ }
1217+ if got := lineText(s, 1); got != "abXYcd" {
1218+ t.Fatalf("line after restored insert mode = %q, want %q", got, "abXYcd")
1219+ }
1220+ },
1221+ },
11991222 {
12001223 name: "ESC D (index), ESC E (next line), ESC M (reverse index)",
12011224 run: func(t *testing.T) {
@@ -1196,6 +1196,29 @@ func TestESCSequences(t *testing.T) {
1196 }1196 }
1197 },1197 },
1198 },1198 },
1199+ {
1200+ name: "ESC 7 / ESC 8 restore origin and insert modes",
1201+ run: func(t *testing.T) {
1202+ s := NewScreen(6, 5)
1203+ s.Write([]byte("\x1b[2;4r\x1b[?6h\x1b[4h\x1b7"))
1204+ s.Write([]byte("\x1b[?6l\x1b[4l\x1b8"))
1205+
1206+ if !s.originMode {
1207+ t.Fatal("origin mode was not restored")
1208+ }
1209+ if !s.insertMode {
1210+ t.Fatal("insert mode was not restored")
1211+ }
1212+
1213+ s.Write([]byte("\x1b[1;1Habcd\x1b[1;3HXY"))
1214+ if s.Row != 1 || s.Col != 4 {
1215+ t.Fatalf("cursor after restored origin and insert modes = (%d,%d), want (1,4)", s.Row, s.Col)
1216+ }
1217+ if got := lineText(s, 1); got != "abXYcd" {
1218+ t.Fatalf("line after restored insert mode = %q, want %q", got, "abXYcd")
1219+ }
1220+ },
1221+ },
1199 {1222 {
1200 name: "ESC D (index), ESC E (next line), ESC M (reverse index)",1223 name: "ESC D (index), ESC E (next line), ESC M (reverse index)",
1201 run: func(t *testing.T) {1224 run: func(t *testing.T) {

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 12

Tags 11

↑↓ moveenter openesc close