Skip to content

Cursor movement from the pending-wrap state: CUU carries x==cols (CUD normalizes), CUB computes from the phantom column #565

Description

@hendo12

Description

After a character prints in the last column, buffer.x == cols represents the pending-wrap state. Some handlers normalize it back into the grid and some don't, producing divergence from xterm — and internal inconsistency between siblings:

  • cmdCursorDown and cmdLineFeedBasic normalize (if buffer.x >= cols { buffer.x -= 1 }, with the comment "If the end of the line is hit, prevent this action from wrapping around to the next line").
  • cmdCursorUp does not — a vertical-only CUU carries the phantom upward, so the next printed character autowraps onto the row below the one the cursor visibly occupies. xterm clears the pending flag on cursor movement and prints at the last column of the CUU target row.
  • cmdBackspace calls restrictCursor() first; cursorBackward (CUB) does not — CUB from the phantom lands one column right of xterm.
  • AppleTerminalView.updateCursorPosition renders the caret at cellWidth * buffer.x unclamped — at x == cols the caret view draws one cell past the grid's right edge.

This looks like the class the Terminal.swift header TODO already tracks ("audit every location to use restrictCursor").

Reproduction (headless engine, no app code)

final class Sink: TerminalDelegate {
    func send(source: Terminal, data: ArraySlice<UInt8>) {}
}
let sink = Sink()
let term = Terminal(delegate: sink, options: TerminalOptions(cols: 10, rows: 5))

term.feed(text: "\r\n")                              // row 1
term.feed(text: String(repeating: "a", count: 10))   // fill row 1 → x == 10 (pending)

term.feed(text: "\u{1b}[A")                          // CUU 1
term.getCursorLocation()   // actual (10, 0) — expected (9, 0): CUD/LF normalize, CUU doesn't

term.feed(text: "Z")
term.getCharacter(col: 9, row: 0)   // actual "\0" — expected "Z"
term.getCharacter(col: 0, row: 1)   // actual "Z"  — spuriously wrapped a row low

CUB variant: from the same pending state, ESC[D lands at x=9; xterm (clear pending → move from col 9) lands at x=8.

Expected: vertical moves clear pending-wrap like CUD/LF already do; CUB restricts before subtracting like cmdBackspace already does; the caret view clamps to the last column.

Found while running zsh-autosuggestions under heavy repaints; zsh's own xenl handling masks most of it in practice, but the caret-view displacement is user-visible. Happy to open a PR with the two-line normalizations + regression tests if that's welcome.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions