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 <aymanbagabas@gmail.com>
This commit is contained in:
Ayman Bagabas 2026-05-26 07:54:24 -04:00 committed by Ayman Bagabas
parent 3138ae4aad
commit c68a1654d3
5 changed files with 34 additions and 0 deletions

View file

@ -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`)

View file

@ -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, ""},

View file

@ -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);

View file

@ -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:

View file

@ -530,6 +530,22 @@ def test_tab_stops(self):
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)