From c68a1654d36c966320bee1d434f7d6804eb28af2 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 26 May 2026 07:54:24 -0400 Subject: [PATCH] feat(vt): add support for DECST8C escape sequence Recognize CSI ? 5 W as DECST8C, which resets the active screen's tab stops to the default of every 8 columns. Other CSI W variants continue to produce a parse error. Signed-off-by: Ayman Bagabas --- docs/changelog.rst | 2 ++ kitty/screen.c | 7 +++++++ kitty/screen.h | 1 + kitty/vt-parser.c | 8 ++++++++ kitty_tests/screen.py | 16 ++++++++++++++++ 5 files changed, 34 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 174da49ce..f358e110f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -176,6 +176,8 @@ Detailed list of changes 0.47.1 [future] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- Add support for the DECST8C escape sequence (``CSI ? 5 W``) to reset tab stops to every 8 columns + - Preserve user-set tab stops across window resizes (previously they were reset to every 8 columns on every resize) - Fix a regression in the previous release that caused :ac:`copy_or_noop` to stop working correctly (:pull:`10041`) diff --git a/kitty/screen.c b/kitty/screen.c index 4f84d3df5..cc5673518 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -2075,6 +2075,11 @@ screen_set_tab_stop(Screen *self) { self->tabstops[self->cursor->x] = true; } +void +screen_reset_tab_stops(Screen *self) { + init_tabstops(self->tabstops, self->columns); +} + void screen_cursor_move(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/, bool allow_move_to_previous_line) { if (count == 0) count = 1; @@ -4978,6 +4983,7 @@ WRAP0(reverse_index) WRAP0(reset) WRAP0(set_tab_stop) WRAP1(clear_tab_stop, 0) +WRAP0(reset_tab_stops) WRAP0(backspace) WRAP0(tab) WRAP0(linefeed) @@ -6260,6 +6266,7 @@ static PyMethodDef methods[] = { MND(carriage_return, METH_NOARGS) MND(set_tab_stop, METH_NOARGS) MND(clear_tab_stop, METH_VARARGS) + MND(reset_tab_stops, METH_NOARGS) MND(start_selection, METH_VARARGS) MND(update_selection, METH_VARARGS) {"clear_selection", (PyCFunction)clear_selection_, METH_NOARGS, ""}, diff --git a/kitty/screen.h b/kitty/screen.h index dee5402aa..56903d122 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -245,6 +245,7 @@ void screen_set_tab_stop(Screen *self); void screen_tab(Screen *self); void screen_backtab(Screen *self, unsigned int); void screen_clear_tab_stop(Screen *self, unsigned int how); +void screen_reset_tab_stops(Screen *self); void screen_set_mode(Screen *self, unsigned int mode); void screen_reset_mode(Screen *self, unsigned int mode); void screen_decsace(Screen *self, unsigned int); diff --git a/kitty/vt-parser.c b/kitty/vt-parser.c index f6ab307dc..012b8f6f2 100644 --- a/kitty/vt-parser.c +++ b/kitty/vt-parser.c @@ -1246,6 +1246,14 @@ dispatch_csi(PS *self) { } REPORT_ERROR("Unknown CSI R sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params); break; + case 'W': + if (start_modifier == '?' && !end_modifier && num_params == 1 && params[0] == 5) { + REPORT_COMMAND(screen_reset_tab_stops); + screen_reset_tab_stops(self->screen); + break; + } + REPORT_ERROR("Unknown CSI W sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params); + break; case ECH: CALL_CSI_HANDLER1(screen_erase_characters, 1); case DA: diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 38d265147..f2a4534a5 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -530,6 +530,22 @@ class TestScreen(BaseTest): s = self.create_screen(cols=4, lines=2) s.draw('aaaX\tbbbb') self.ae(str(s.line(0)) + str(s.line(1)), 'aaaXbbbb') + # DECST8C: reset tab stops to every 8 columns + s = self.create_screen(cols=20, lines=2) + s.clear_tab_stop(3) + s.reset_tab_stops() + s.cursor_position(1, 1) + s.tab() + self.ae(s.cursor.x, 8) + s.tab() + self.ae(s.cursor.x, 16) + # Verify the DECST8C escape sequence + s = self.create_screen(cols=20, lines=2) + s.clear_tab_stop(3) + parse_bytes(s, b'\x1b[?5W') + s.cursor_position(1, 1) + s.tab() + self.ae(s.cursor.x, 8) # Custom tab stops survive a window resize s = self.create_screen(cols=20, lines=2)