Skip to content

fix: preserve grapheme clusters (combining marks) in cell emission - #114

Open
natemoo-re wants to merge 4 commits into
mainfrom
nm/cell-grapheme
Open

fix: preserve grapheme clusters (combining marks) in cell emission#114
natemoo-re wants to merge 4 commits into
mainfrom
nm/cell-grapheme

Conversation

@natemoo-re

@natemoo-re natemoo-re commented Aug 23, 2026

Copy link
Copy Markdown
Member

cells with combining marks (zero-width codepoints) were silently dropped when emitted because render_text skipped codepoints with cw == 0. the cell model stored only a single uint32_t, meaning combining accents, ZWJ emoji, skin-tone modifiers, flag variation emoji, and kitty graphics were broken.

the fix here is to update Cell's model to track uint32_t combining[8] and have render_text append zero-width codepoints to the current column before cursor output. this requires updating OUT_BYTES_PER_CELL from 64 to 128 to cover worst-case scenario (base char + 8 combining marks, silently dropping marks beyond 8).

spec §8.3.3 has been updated with a grapheme-cluster preservation requirement. §13 now describes cells as "a grapheme cluster" rather than "a Unicode codepoint" and defines the capacity and truncation behavior.

semi-related to #84

The Cell struct stored only a single uint32_t codepoint, so render_text
silently dropped every combining mark (wcwidth ≤ 0): the emitted ANSI
stream contained bare base codepoints with no continuations.

Root cause: kitty-graphics Unicode placeholder cells require two combining
diacritics (row/col index from the rowcolumn-diacritics table) to follow
the base U+10EEEE codepoint. Because those marks were dropped, every
placeholder cell emitted as row 0 — producing the N-repeated-top-band
banding artifact in multi-row placements.

The same drop affected any combining-mark or ZWJ content: accented
characters (e + U+0301), flag pairs, skin-tone modifiers.

Fix:
- Cell gains `uint32_t combining[8]` (zero-terminated). Marks beyond 8
  are silently truncated from the end; the first marks always survive.
- cells_fill uses a designated-init template so combining[] is zeroed
  on every back-buffer reset, and setcell clears it on every base write.
- render_text tracks the last-written column; zero-width codepoints go
  to append_combining() instead of being discarded.
- present_cups / present_lines emit combining[] bytes immediately after
  the base character, before any cursor repositioning.
- OUT_BYTES_PER_CELL bumped 64→128 to cover base + 8 combining marks.
- cell_cmp checks combining[] so a changed mark triggers a diff/rediff.
- Spec updated: §8.3.3 normative preservation requirement, §13 cluster
  cell representation with truncation semantics, §13 measurement note.

Four new tests: kitty placeholder (U+10EEEE + 2 marks), combining accent
(e + U+0301), ZWJ family emoji (ZWJ preserved per-cell; following emoji
start new cells — inherent cell-model constraint, documented), and
truncation-from-end pinned at 8 marks.
Comment thread src/cell.h
Comment on lines +8 to +10
/* Maximum combining marks stored per cell. Marks beyond this limit are
* silently truncated from the end; the first CELL_MAX_COMBINING are kept. */
#define CELL_MAX_COMBINING 8

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this gives us plenty of headroom for the common case and is unlikely to be a problem in the short-term, but I have a slight concern that it will be a long-term issue if any terminal protocols introduce complex metadata via ZWJ that requires >8 codepoints

an alternative design would be bumping this cieling and keeping a dynamic map of combining size per cell rather than reserving a flat 8 per cell.

@natemoo-re
natemoo-re marked this pull request as ready for review August 23, 2026 11:49
@pkg-pr-new

pkg-pr-new Bot commented Aug 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@bomb.sh/tty@114

commit: 07a84e9

@github-actions

Copy link
Copy Markdown

Size Increased — +1.1 KB

119.9 KB unpacked

@codspeed-hq

codspeed-hq Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will degrade performance by 15.74%

❌ 2 regressed benchmarks
✅ 8 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime render steady diff 36.7 ms 44 ms -16.6%
WallTime render mixed frames 39 ms 45.8 ms -14.87%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing nm/cell-grapheme (07a84e9) with main (5889be9)

Open in CodSpeed

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