fix(screen): preserve tab stops across window resizes

Previously, every window resize reinitialised the tab stops to the
default of every 8 columns, discarding any stops set via HTS or cleared
via TBC. ECMA-48 only treats RIS, DECSTR, and DECCOLM as events that
reset tab stops, and other terminal emulators all preserve user-set
stops across an interactive resize.

Copy the surviving prefix of the previous tab stops into the freshly
allocated array on both main and alt screens. Newly added columns when
growing the window keep the default every 8 columns pattern. Also point
the active tabstops pointer at the alt screen's array when a resize
happens while the alt screen is active, instead of unconditionally
resetting it to the main screen's array.

Signed-off-by: Ayman Bagabas <aymanbagabas@gmail.com>
This commit is contained in:
Ayman Bagabas 2026-05-26 07:55:16 -04:00 committed by Ayman Bagabas
parent e257e5695f
commit 3138ae4aad
3 changed files with 47 additions and 7 deletions

View file

@ -176,6 +176,8 @@ Detailed list of changes
0.47.1 [future]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 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`)
- macOS: Fix a regression in the previous release that caused URLs to be quoted when dropping into shells (:iss:`10054`)

View file

@ -575,16 +575,26 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) {
grman_resize(self->alt_grman, self->lines, lines, self->columns, columns, num_content_lines_before, num_content_lines_after);
#undef setup_cursor
/* printf("\nold_size: (%u, %u) new_size: (%u, %u)\n", self->columns, self->lines, columns, lines); */
index_type old_columns_for_tabs = self->columns;
self->lines = lines; self->columns = columns;
self->margin_top = 0; self->margin_bottom = self->lines - 1;
PyMem_Free(self->main_tabstops);
self->main_tabstops = PyMem_Calloc(2*self->columns, sizeof(bool));
if (self->main_tabstops == NULL) { PyErr_NoMemory(); return false; }
self->alt_tabstops = self->main_tabstops + self->columns;
self->tabstops = self->main_tabstops;
init_tabstops(self->main_tabstops, self->columns);
init_tabstops(self->alt_tabstops, self->columns);
bool *old_tabstops = self->main_tabstops;
bool *new_tabstops = PyMem_Calloc(2 * self->columns, sizeof(bool));
if (new_tabstops == NULL) { PyErr_NoMemory(); return false; }
bool *new_main = new_tabstops;
bool *new_alt = new_tabstops + self->columns;
init_tabstops(new_main, self->columns);
init_tabstops(new_alt, self->columns);
if (old_tabstops && old_columns_for_tabs) {
index_type to_copy = MIN(old_columns_for_tabs, self->columns);
memcpy(new_main, old_tabstops, to_copy * sizeof(bool));
memcpy(new_alt, old_tabstops + old_columns_for_tabs, to_copy * sizeof(bool));
}
PyMem_Free(old_tabstops);
self->main_tabstops = new_main;
self->alt_tabstops = new_alt;
self->tabstops = is_main ? self->main_tabstops : self->alt_tabstops;
self->is_dirty = true;
clear_all_selections(self);
self->last_visited_prompt.is_set = false;

View file

@ -531,6 +531,34 @@ class TestScreen(BaseTest):
s.draw('aaaX\tbbbb')
self.ae(str(s.line(0)) + str(s.line(1)), 'aaaXbbbb')
# Custom tab stops survive a window resize
s = self.create_screen(cols=20, lines=2)
s.clear_tab_stop(3) # clear all
s.cursor_position(1, 5)
s.set_tab_stop() # stop at column index 4
s.cursor_position(1, 13)
s.set_tab_stop() # stop at column index 12
# Grow: existing stops preserved, new columns get default every-8 stops
s.resize(s.lines, 30)
s.cursor_position(1, 1)
s.tab(); self.ae(s.cursor.x, 4)
s.tab(); self.ae(s.cursor.x, 12)
s.tab(); self.ae(s.cursor.x, 24) # default stop in newly added columns
# Shrink: stops within new width are preserved
s.resize(s.lines, 15)
s.cursor_position(1, 1)
s.tab(); self.ae(s.cursor.x, 4)
s.tab(); self.ae(s.cursor.x, 12)
# Resize on alt screen also preserves alt-screen tab stops
s = self.create_screen(cols=20, lines=2)
parse_bytes(s, b'\x1b[?1049h') # switch to alt screen
s.clear_tab_stop(3)
s.cursor_position(1, 5)
s.set_tab_stop()
s.resize(s.lines, 30)
s.cursor_position(1, 1)
s.tab(); self.ae(s.cursor.x, 4)
def test_backspace(self):
s = self.create_screen()
q = 'a'*s.columns