Skip to content

display: end an escape sequence where the specification says it ends - #756

Merged
berrym merged 1 commit into
masterfrom
sweep/item6-composition-width
Aug 16, 2026
Merged

display: end an escape sequence where the specification says it ends#756
berrym merged 1 commit into
masterfrom
sweep/item6-composition-width

Conversation

@berrym

@berrym berrym commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Second slice of duplicate-path item 6, and the more interesting one: consolidating onto the shared function exposed a bug in the shared function.

What the copy did

composition_engine.c carried its own width walk beside a call to the canonical one 300 lines away in the same file. The copy:

  • enumerated terminators m K J H A B C D G f s u, so a sequence ending in h or l never terminated — and \033[?25l / \033[?25h is the cursor hide/show pair a shell emits constantly. Everything after one measured as zero width.
  • carried a standing TODO: Proper UTF-8 width calculation and counted one column per character.

Deleted. Both sites in that file now call the shared computation.

What auditing the terminators then found

The shared walk had the same class of error. It ended a sequence at the first letter — which looks equivalent, because most final bytes are letters. But ECMA-48 defines the final byte as 0x40–0x7E, and \033[3~ ends in ~.

So the shared function measured the remainder of such text as zero width too. Consolidating onto it without checking would have spread that case rather than fixing it.

The core now implements the specified rule: consume the introducer, and for CSI/OSC run to the first byte in 0x40–0x7E; a two-character escape ends at once. This is exactly what completion_menu_renderer already did — the correct implementation existed, in the copy nobody had consolidated onto.

Curation

No reference shell is the authority on how lush measures its own prompt, so this is not a consensus question. The question is whether lush can beat the heuristic it had — and it can, because the specification defines the answer exactly. Every hand-rolled enumeration in this tree disagreed with it in a different way, which is the argument for having one.

Verification

tests/unit/test_screen_buffer.c pins the terminators that broke each copy: SGR colour, cursor hide/show (l, h), erase-line K, clear-screen J, cursor-position H, multi-parameter SGR, and \033[3~ — the non-letter final byte the shared function itself failed until this change.

Full suite 197/197. ASan 65/65.

composition_engine carried its own width walk beside a call to the canonical
one 300 lines away in the same file. The copy enumerated ANSI terminators --
m K J H A B C D G f s u -- so a sequence ending in `h` or `l` never
terminated, and `\033[?25l` / `\033[?25h` is the cursor hide/show pair a shell
emits constantly. Everything after one measured as ZERO width. It also carried
a standing `TODO: Proper UTF-8 width calculation` and counted one column per
character. Deleted; both sites in that file now call the shared computation.

Auditing the terminator sets then found the same class of error in the SHARED
walk. It ended a sequence at the first letter, which looks equivalent because
most final bytes are letters -- but ECMA-48 defines the final byte as the
range 0x40-0x7E, and `\033[3~` ends in `~`. The shared function measured the
whole remainder of such a text as zero width, so consolidating onto it would
have spread that one case rather than fixing it.

The core now implements the rule the specification states: consume the
introducer, and for a CSI or OSC run to the first byte in 0x40-0x7E. A
two-character escape ends at once. This is what the completion menu renderer
already did -- the correct implementation existed, in the copy nobody had
consolidated onto.

Curation, recorded: no reference shell is the authority on how lush measures
its own prompt, so this is not a consensus question. The question is whether
lush can do better than the heuristic it had, and it can -- the specification
defines the answer exactly, and every hand-rolled enumeration in this tree
disagreed with it in a different way.

tests/unit/test_screen_buffer.c pins the terminators that broke each copy: SGR
colour, cursor hide/show ending in `l` and `h`, erase-line `K`, clear-screen
`J`, cursor-position `H`, multi-parameter SGR, and `\033[3~` -- the final byte
that is not a letter, which the shared function itself failed until now.
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/display/screen_buffer.c 75.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@berrym
berrym merged commit 0a60976 into master Aug 16, 2026
7 checks passed
@berrym
berrym deleted the sweep/item6-composition-width branch August 16, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant