Skip to content

Emoji sequences spill their background over the two cells after them in Terminal.app; the spill is never repainted #4600

Description

@mellonis

Environment: herdr 0.9.1 (Homebrew), macOS 27.0, host terminal Terminal.app. In this build panes get the host's TERM_PROGRAM=Apple_Terminal; main sets TERM_PROGRAM=herdr (src/pane.rs), which does not change what follows. Not reproducible in a bare Terminal.app tab, and as far as I can tell not in hosts that draw emoji sequences two columns wide.

What happens

Inside a herdr pane on Terminal.app, a multi-code-point emoji — a skin tone (👍🏽), a ZWJ sequence (👩‍👧), a keycap (1️⃣) — is drawn four columns wide by the host, while herdr's grid (correctly) gives it two. Its background colour spills over the two cells to its right. When the app later rewrites those two cells with the same content, nothing changes on screen: the spill stays until something writes different bytes there.

An app that scrolls such text (a marquee, a log line with emoji under a coloured band) accumulates a growing coloured tail to the right of its box.

Repro

In a pane, a magenta band ending on the emoji, then two kinds of rewrite of the cells after it:

printf '\e[2J\e[H'
printf '0123456789  the magenta must stop at column 2\n'
printf '\e[45m👍🏽\e[0m    |  F1  left alone\n'
printf '\e[45m👍🏽\e[0m    |  F2  cells 2-3 rewritten with the same spaces\n'
printf '\e[45m👍🏽\e[0m    |  F3  cells 2-3 rewritten as "xx", then as spaces\n'
sleep 1
printf '\e[3;3H  '                 # F2: same bytes again
printf '\e[4;3Hxx\e[4;3H  '        # F3: different bytes, then the spaces
sleep 5

Observed: on all three rows the magenta first covers columns 0–3. After the rewrites, F3 is clean (columns 0–1); F1 and F2 still show four magenta columns. (My recordings were made with an equivalent Node script that writes the same bytes; a moving band shows the tail growing with every emoji that enters it.)

Expected: the background ends at column 2 on every row, or at least is cleaned when the cells after the emoji are repainted.

Where it seems to come from

src/protocol/render_ansi.rs: write_cell writes the whole cluster and skips the covered cell (to_skip = cell_width(cell) - 1); the diff path (write_changed_cells / cells_visually_equal) repaints the following cells only when their resolved content changed. After a wide cell whose glyph the host draws wider than two columns, the cells at x+2 and x+3 are therefore never rewritten unless the app changes them — and a rewrite with identical bytes makes no difference to the host.

Suggested fix

When a changed cell holds a multi-code-point cluster (more than one scalar), extend the repainted run by the two cells after its covered cell even when they compare equal. The F3 row shows the host does clean the spill on such a rewrite; the cost is two extra cells per such change. It could be limited to hosts known to draw sequences wide (TERM_PROGRAM=Apple_Terminal at the client).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    macosaffects macOS-specific behaviormaintainer-neededrequires maintainer judgment or maintainer-only reproductionp2valid narrow or ordinary defect with limited impact or a practical workaroundrenderingterminal emulation, reflow, layout, graphics, or output renderingtriagedScreened; left open for investigation or maintainer judgment. Not proof of a bug or reproduction.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions