Skip to content

fix: apply terminal semantics to carriage returns - #278

Open
Thorsrud22 wants to merge 1 commit into
pycontribs:mainfrom
Thorsrud22:carriage-return
Open

Thorsrud22 wants to merge 1 commit into
pycontribs:mainfrom
Thorsrud22:carriage-return

Conversation

@Thorsrud22

@Thorsrud22 Thorsrud22 commented Sep 11, 2026

Copy link
Copy Markdown

Fixes #79.

A carriage return in the middle of a line, which is how progress bars, spinners and status lines rewrite themselves, currently ends up as a line break. The CLI's stdin wrapper translates \r to \n (universal newlines), and via the API a raw \r reaches the HTML, where browsers render it as a newline anyway. A terminal moves the cursor back to column 0 and overwrites.

Using the example from the issue:

$ printf 'ONE\nTWO\nTHREEEEEE\rFOUR\rFIVE\nSIX\n' | ansi2html --inline
output
before ONE TWO THREEEEEE FOUR FIVE SIX (each on its own line)
after ONE TWO FIVE SIX

Change

  • A small pre-pass in apply_regex keeps only the text after the last \r on each line. \r\n is treated as a plain line ending and a trailing \r is ignored.
  • Escape sequences in the overwritten part (CSI, the VT100 box-drawing switch, OSC 8 link open/close) are carried over, since they change the state the visible text is rendered with. So ESC[32m[###] 30%\r[######] 100%ESC[0m renders as a green [######] 100%, and a hyperlink that is still open when the line is rewritten applies to the new text. A link whose text was wholly overwritten is dropped rather than left as an empty anchor.
  • The CLI opens stdin with newline="" so carriage returns reach the converter untranslated. CRLF input still produces the same output as before.

Known approximation, stated in the docstring too: when the rewritten text is shorter than what it replaces, a real terminal would still show the tail of the old text (THREEEEEE\rFOUR shows FOUREEEEE). This PR shows FOUR. Reproducing the tail would need per-column tracking of characters and their styles, which is a much bigger change; the whole-line rewrite that progress bars do is rendered exactly as the terminal shows it. Happy to go the "document it" route from the issue instead if you'd rather not change behaviour.

Verification

  • Four tests added: the issue's example, CRLF and trailing CR, styling and link carry-over, and a CLI round-trip through a real subprocess. All 45 tests pass.
  • isort, black, flake8, pylint and mypy pre-commit hooks pass on the changed files.

@github-actions github-actions Bot added the fix This issue/PR relates to a bug. label Sep 11, 2026
A carriage return in the middle of a line (progress bars, spinners,
status lines) used to end up as a line break: the CLI's stdin wrapper
translated it to \n, and via the API a raw \r reached the HTML where
browsers render it the same way. A terminal instead moves the cursor
back to the start of the line and overwrites.

Add a pre-pass that keeps only the text after the last \r on each
line, treats \r\n as a plain line ending, ignores a trailing \r, and
carries over escape sequences from the overwritten part so colour and
link state still apply to what stays visible. The CLI now opens stdin
with newline="" so the converter, not the wrapper, decides what a
carriage return means.

This is an approximation: a rewrite shorter than the text it replaces
would leave a tail visible in a terminal, which is not reproduced. The
common whole-line rewrite renders exactly as the terminal shows it.

Fixes pycontribs#79
@Thorsrud22
Thorsrud22 force-pushed the carriage-return branch 2 times, most recently from bc8fa85 to 8036aae Compare September 16, 2026 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix This issue/PR relates to a bug.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Carriage return ignored/treated like line feed

1 participant