Skip to main content

normalize_csi_overwrites

Function normalize_csi_overwrites 

Source
fn normalize_csi_overwrites(
    chunk: &mut Vec<u8>,
    still_on_first_line: &mut bool,
    at_col0: &mut bool,
)
Expand description

Normalize CSI cursor-repositioning idioms that map cleanly onto the existing \r-based overwrite model, converting them to a literal \r byte so collapse_cr (which already understands \r) collapses them too, with no new cases needed there. Runs on the raw chunk BEFORE strip_ansi, which otherwise deletes CSI sequences unconditionally with no overwrite-awareness — the “Phase γ” gap strip_ansi’s own doc comment flags (“Cursor positioning escapes within the same line”). See docs/specs/SPEC_TOOL_LOG_UNIVERSAL_ANIMATION_COLLAPSE_2026_07_27.md §A2.

still_on_first_line and at_col0 are both caller-owned state that persists across calls for the lifetime of one reader loop — this function is invoked once per PTY read, and a chunk boundary is not a terminal state boundary: a read ending mid-line (no trailing \n/\r) must carry “cursor is NOT at column 0” into the next call, otherwise a standalone EL split across two reads ("prefix" in one chunk, "\x1b[2Ksuffix" in the next) would see a freshly-reset at_col0 == true and wrongly convert that EL to \r, reproducing the exact mid-line truncation bug this function exists to prevent, just moved from a mid-chunk split to a cross-chunk-read split (reagent P1, PR #2330, caught after the first, chunk-internal-only version of this fix).

still_on_first_line: true until the first \n is seen in this tool call’s output, false forever after. Needed because Windows ConPTY re-serializes a literal \r as CUP-to-home (\x1b[H) rather than passing it through unchanged — verified empirically via a1_e2e_static_label_then_delayed_cr_overwrite_collapses_to_one_line capturing the raw PTY bytes: a child process writing \rdone produces \x1b[?25l\x1b[Hdone...\x1b[?25h on the wire, not a literal \r. CUP “home” (row 1, col 1) is unambiguous evidence of an overwrite ONLY when nothing has scrolled the visual line off row 1 yet; once real multi-line output exists, \x1b[H genuinely means “go back to the first of several lines” (a multi-line redraw, spec §A3, deliberately out of scope) and must NOT be treated as “restart the current line”.

Cases handled, chosen because each means exactly “move to the start of the (only) line so far” without needing true per-column tracking:

  • CUP to row 1 col 1 (\x1b[H, \x1b[1H, \x1b[1;1H, and the omitted-field variants of each), gated on still_on_first_line per above — the ConPTY-observed idiom.
  • CHA to column 1 (\x1b[1G / \x1b[G, “cursor to column 1”) is literally “move to start of line” — identical to \r. Unix PTYs (and any tool not going through ConPTY’s re-serialization) may use this form directly.
  • EL (\x1b[2K erase whole line, \x1b[K/\x1b[0K erase to end of line) is treated as “reset to start of line” too, but ONLY when the cursor is already known to be at column 0 (*at_col0) — i.e. the dominant real-world \r + EL idiom (\r\x1b[2K<text>), where it’s genuinely redundant with the \r already just emitted. A STANDALONE EL, mid-line (cursor NOT at column 0 — e.g. prefix\x1b[Ksuffix), is a real, different case: EL only erases from the cursor onward and never moves it, so collapsing it to \r there would make collapse_cr truncate prefix even though EL never asked for that. That standalone case is left unconverted (falls through to the “unrecognized” branch below, so strip_ansi removes the escape byte-for-byte without touching prefix) — a bug reagent caught on this exact line (PR #2330): the prior, unconditional version silently discarded prefix in prefix\x1b[Ksuffix\n.

CUB (\x1b[<n>D, “cursor back n columns”) is intentionally NOT handled here — a faithful implementation needs true per-column tracking (a raw byte position isn’t a faithful proxy once n varies per frame), which collapse_cr’s line-offset model doesn’t have. Left as a documented follow-up alongside CUU/multi-line redraw (spec §A3) rather than guessed at with an approximation that could truncate the wrong amount.