Today — tab stops are fixed at every eighth column. Setting one (HTS, ESC H), clearing one (TBC, CSI g) and moving between them (CHT, CSI I; CBT, CSI Z) are all parsed and ignored. Measured against 0.9.0 on a 24-column terminal, with · marking where the text lands:
$ inspect --size 24x2 sh -c "printf 'a\011b'" # default stops
a·······b # column 8, correct
$ inspect --size 24x2 sh -c "printf '\033[4G\033H\033[1Ga\011b'" # set a stop at column 4, then tab to it
a·······b # xterm: a··b, at column 4
$ inspect --size 24x2 sh -c "printf '\011\011X\033[1Zy'" # two tabs, then one back-tab
················Xy # xterm: y at column 8
The default stops are right because they are the backend's hardcoded eight; nothing else about tab stops exists. The escape sequences reach a dispatch table that has no entry for them and vanish.
Why it is worth fixing — all four capabilities are in the terminfo entry termlens hands every child by default:
$ infocmp -1 xterm-256color | grep -E '^\s+(hts|tbc|cbt)='
cbt=\E[Z,
hts=\EH,
tbc=\E[3g,
An application that lays a table out by setting its own stops and tabbing between them — which is what the capabilities are for, and what ncurses uses when a program calls them — renders with every column in the wrong place. The failure is a silent misalignment: the characters are all present, so contains still passes and only a column assertion or a whole-screen snapshot catches it, and a snapshot taken before the tab stops were set will happily bless the wrong layout.
Back-tab (CBT) has a second life worth naming: it is what Shift-Tab sends, and an application echoing or forwarding it emits CSI Z on its output side. Today that moves nothing.
Fix — the backend has no notion of tab stops beyond its fixed eight, so the state belongs where the other state it does not model already lives: emu/seq.rs, beside the character sets, focus reporting and the cursor shape.
A tab stop set is a bitmap one bit per column, cols wide, initialised to every eighth column and reset there by RIS — and DECSTR too, whose list already lives in soft_reset. Then:
ESC H (HTS) sets the stop at the current column, so the tracker needs the cursor column, which the emulator has.
CSI g / CSI 0 g clears the stop at the cursor; CSI 3 g clears all.
\t, CSI I (CHT, forward n stops) and CSI Z (CBT, back n stops) move the cursor to the computed column, which the emulator applies as a CSI n G (CHA) on the rewritten stream — the backend dispatches G, so this is a rewrite rather than a new grid operation, the same way DEC Special Graphics became a glyph substitution.
Resize is the one interesting case: a grid that grows needs stops for the new columns. Take the simple answer — extend with the default every-eighth pattern past the old width, leaving existing stops alone — and write it in the rustdoc as a decision rather than leaving a reader to infer it.
Scope note for the PR: \t currently reaches the backend and is handled by its fixed eight, so the rewrite must take over \t as well or the two will disagree the moment a custom stop exists. That is the part to get right first, and a test that sets one stop and then uses a plain \t is the one that proves it.
Done when — HTS sets a stop that a following \t honours; TBC clears one stop and CSI 3 g clears all; CHT and CBT move by whole stops; RIS and DECSTR restore the default every-eighth set; a resize extends the set in a documented way; and a test in the emulation suite covers each, including a plain \t after a custom stop.
Today — tab stops are fixed at every eighth column. Setting one (
HTS,ESC H), clearing one (TBC,CSI g) and moving between them (CHT,CSI I;CBT,CSI Z) are all parsed and ignored. Measured against 0.9.0 on a 24-column terminal, with·marking where the text lands:The default stops are right because they are the backend's hardcoded eight; nothing else about tab stops exists. The escape sequences reach a dispatch table that has no entry for them and vanish.
Why it is worth fixing — all four capabilities are in the terminfo entry termlens hands every child by default:
An application that lays a table out by setting its own stops and tabbing between them — which is what the capabilities are for, and what ncurses uses when a program calls them — renders with every column in the wrong place. The failure is a silent misalignment: the characters are all present, so
containsstill passes and only a column assertion or a whole-screen snapshot catches it, and a snapshot taken before the tab stops were set will happily bless the wrong layout.Back-tab (
CBT) has a second life worth naming: it is whatShift-Tabsends, and an application echoing or forwarding it emitsCSI Zon its output side. Today that moves nothing.Fix — the backend has no notion of tab stops beyond its fixed eight, so the state belongs where the other state it does not model already lives:
emu/seq.rs, beside the character sets, focus reporting and the cursor shape.A tab stop set is a bitmap one bit per column,
colswide, initialised to every eighth column and reset there byRIS— andDECSTRtoo, whose list already lives insoft_reset. Then:ESC H(HTS) sets the stop at the current column, so the tracker needs the cursor column, which the emulator has.CSI g/CSI 0 gclears the stop at the cursor;CSI 3 gclears all.\t,CSI I(CHT, forward n stops) andCSI Z(CBT, back n stops) move the cursor to the computed column, which the emulator applies as aCSI n G(CHA) on the rewritten stream — the backend dispatchesG, so this is a rewrite rather than a new grid operation, the same way DEC Special Graphics became a glyph substitution.Resize is the one interesting case: a grid that grows needs stops for the new columns. Take the simple answer — extend with the default every-eighth pattern past the old width, leaving existing stops alone — and write it in the rustdoc as a decision rather than leaving a reader to infer it.
Scope note for the PR:
\tcurrently reaches the backend and is handled by its fixed eight, so the rewrite must take over\tas well or the two will disagree the moment a custom stop exists. That is the part to get right first, and a test that sets one stop and then uses a plain\tis the one that proves it.Done when —
HTSsets a stop that a following\thonours;TBCclears one stop andCSI 3 gclears all;CHTandCBTmove by whole stops;RISandDECSTRrestore the default every-eighth set; a resize extends the set in a documented way; and a test in the emulation suite covers each, including a plain\tafter a custom stop.