no-mistakes(review): Fix VT mode query integration
- commit
- 5852a418d116387c2a7c6ff6ddb2e3d2cc9763cf
- author
- brice · · 2026-07-09 15:10 UTC
- parent
- 532e4e5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 21 | 21 | {name: "CPR at home", input: "\x1b[6n", want: "\x1b[1;1R"}, | |
| 22 | 22 | {name: "CPR after CUP", input: "\x1b[3;7H\x1b[6n", want: "\x1b[3;7R"}, | |
| 23 | 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 | 29 | {name: "DECRQM 2026 reset", input: "\x1b[?2026$p", want: "\x1b[?2026;2$y"}, | |
| 25 | 30 | {name: "DECRQM 2026 set", input: "\x1b[?2026h\x1b[?2026$p", want: "\x1b[?2026;1$y"}, | |
| 26 | 31 | {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 ( | |||
| 9 | 9 | ) | |
| 10 | 10 | ||
| 11 | 11 | 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 | |
| 16 | 18 | } | |
| 17 | 19 | ||
| 18 | 20 | type screenState struct { | |
| @@ -843,33 +845,27 @@ func (s *Screen) applyCSI(params string, cmd byte) { | |||
| 843 | 845 | if private { | |
| 844 | 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 | 849 | resp = append(resp, ';') | |
| 848 | 850 | resp = strconv.AppendInt(resp, int64(s.Col+1), 10) | |
| 849 | 851 | resp = append(resp, 'R') | |
| 850 | 852 | s.respond(resp) | |
| 851 | 853 | } | |
| 852 | 854 | 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) | |
| 855 | 860 | if err != nil { | |
| 856 | 861 | return | |
| 857 | 862 | } | |
| 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) | |
| 871 | 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 | 869 | resp = strconv.AppendInt(resp, int64(mode), 10) | |
| 874 | 870 | resp = append(resp, ';') | |
| 875 | 871 | resp = strconv.AppendInt(resp, int64(state), 10) | |
| @@ -958,6 +954,38 @@ func firstPositive(parts []int, fallback int) int { | |||
| 958 | 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 | 989 | func (s *Screen) cursorMinRow() int { | |
| 962 | 990 | if s.originMode { | |
| 963 | 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 | 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 | 1117 | func (s *Screen) restoreCursor() { | |
| @@ -1093,6 +1121,8 @@ func (s *Screen) restoreCursor() { | |||
| 1093 | 1121 | s.Row = clamp(s.savedCursor.row, 0, s.Frame.Height-1) | |
| 1094 | 1122 | s.Col = clamp(s.savedCursor.col, 0, s.Frame.Width-1) | |
| 1095 | 1123 | s.Style = s.savedCursor.style | |
| 1124 | + | s.originMode = s.savedCursor.originMode | |
| 1125 | + | s.insertMode = s.savedCursor.insertMode | |
| 1096 | 1126 | } | |
| 1097 | 1127 | ||
| 1098 | 1128 | func (s *Screen) reset() { | |
| @@ -9,10 +9,12 @@ import ( | |||||
| 9 | ) | 9 | ) | ||
| 10 | 10 | ||||
| 11 | type cursorState struct { | 11 | type cursorState struct { | ||
| 12 | - | row int | 12 | + | row int |
| 13 | - | col int | 13 | + | col int |
| 14 | - | style renderer.Style | 14 | + | style renderer.Style |
| 15 | - | saved bool | 15 | + | 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 | return | 861 | return | ||
| 857 | } | 862 | } | ||
| 858 | - | state := 0 | 863 | + | 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.style | 1123 | 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) { | |||
| 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 | 1223 | name: "ESC D (index), ESC E (next line), ESC M (reverse index)", | |
| 1201 | 1224 | 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) { | ||