fix: preserve grapheme clusters (combining marks) in cell emission - #114
fix: preserve grapheme clusters (combining marks) in cell emission#114natemoo-re wants to merge 3 commits into
Conversation
| /* 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 |
There was a problem hiding this comment.
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.
commit: |
|
Size Increased — +1.0 KB 101.1 KB unpacked |
07a84e9 to
d7c2d69
Compare
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.
d7c2d69 to
1a1f83d
Compare
| combining marks stores zero in every slot. When a text string produces more than | ||
| 8 combining marks for a single base codepoint, the excess marks are silently | ||
| truncated from the end (marks 1–8 are kept, marks 9+ are discarded), ensuring |
There was a problem hiding this comment.
Should this result in a warning being issued? It seems like if it was, then it should be issued only once.
| cw = 1; | ||
| if (cw > 0) { | ||
| setcell(ct, x, y0, cp, fg, bg); | ||
| last_x = x; |
There was a problem hiding this comment.
We should not unconditionally set the last_x because setcell() could be a no-op in the event that it is outside the clip region:
if (ct->clipping) {
if (x < ct->clipx || x >= ct->clipx + ct->clipw)
return;
if (y < ct->clipy || y >= ct->clipy + ct->cliph)
return;
}There was a problem hiding this comment.
Can we make the tests visual as in other test cases? It would involve enhancing our print() helper (yet again!)
expect(trim(print(ansi, 12, 4))).toEqual(`
┌──────────┐
│café │
│ │
└──────────┘`.trim());We really ought to see if we can use libghostty for this (not now ofc, but sometime in the unspecified future)
cells with combining marks (zero-width codepoints) were silently dropped when emitted because
render_textskipped codepoints withcw == 0. the cell model stored only a singleuint32_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 trackuint32_t combining[8]and haverender_textappend zero-width codepoints to the current column before cursor output. this requires updatingOUT_BYTES_PER_CELLfrom64to128to 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