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.
Description
After a character prints in the last column,
buffer.x == colsrepresents 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:cmdCursorDownandcmdLineFeedBasicnormalize (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").cmdCursorUpdoes 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.cmdBackspacecallsrestrictCursor()first;cursorBackward(CUB) does not — CUB from the phantom lands one column right of xterm.AppleTerminalView.updateCursorPositionrenders the caret atcellWidth * buffer.xunclamped — atx == colsthe caret view draws one cell past the grid's right edge.This looks like the class the
Terminal.swiftheader TODO already tracks ("audit every location to use restrictCursor").Reproduction (headless engine, no app code)
CUB variant: from the same pending state,
ESC[Dlands 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.