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).
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;mainsetsTERM_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:
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_cellwrites 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 atx+2andx+3are 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_Terminalat the client).