diff --git a/docs/changelog.rst b/docs/changelog.rst index b933729c1..2f4cfa0ff 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -188,6 +188,8 @@ Detailed list of changes - Preserve user-set tab stops across window resizes instead of resetting to 8 column default +- Add support for the DECST8C escape sequence (``CSI ? 5 W``) to reset tab stops to every 8 columns + 0.47.0 [2026-05-19] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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 43ec474a8..db4c403d5 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)