-
-
Notifications
You must be signed in to change notification settings - Fork 2
fix: preserve grapheme clusters (combining marks) in cell emission #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,10 +5,16 @@ | |
|
|
||
| #include <stdint.h> | ||
|
|
||
| /* 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 | ||
|
Comment on lines
+8
to
+10
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| typedef struct { | ||
| uint32_t ch; | ||
| uint32_t fg; /* 0xAARRGGBB — upper byte: attribute flags */ | ||
| uint32_t bg; /* 0xAARRGGBB — upper byte: attribute flags */ | ||
| uint32_t combining[CELL_MAX_COMBINING]; /* zero-terminated combining-mark | ||
| codepoints */ | ||
| } Cell; | ||
|
|
||
| /* Attribute flags (packed into high byte of fg) */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -109,7 +109,9 @@ struct Clayterm { | |
| * Output buffer is sized at 64 bytes per cell — enough for worst-case | ||
| * full-screen redraws with truecolor SGR sequences on every cell. | ||
| */ | ||
| #define OUT_BYTES_PER_CELL 64 | ||
| /* 128 bytes per cell: ~84 bytes worst-case for CUP + SGR sequences, plus up | ||
| * to 4 (base SMP char) + 8×4 (combining marks) = 36 bytes of cluster text. */ | ||
| #define OUT_BYTES_PER_CELL 128 | ||
|
|
||
| /* ── Cell buffer ops ──────────────────────────────────────────────── */ | ||
|
|
||
|
|
@@ -133,6 +135,23 @@ static void setcell(struct Clayterm *ct, int x, int y, uint32_t ch, uint32_t fg, | |
| if (!(bg & ATTR_DEFAULT)) { | ||
| c->bg = bg; | ||
| } | ||
| for (int i = 0; i < CELL_MAX_COMBINING; i++) | ||
| c->combining[i] = 0; | ||
| } | ||
|
|
||
| /* Append a combining-mark codepoint to the cell at (x, y) in the back buffer. | ||
| * Marks beyond CELL_MAX_COMBINING are silently dropped (truncation from end). | ||
| */ | ||
| static void append_combining(struct Clayterm *ct, int x, int y, uint32_t cp) { | ||
| if (x < 0 || x >= ct->w || y < 0 || y >= ct->h) | ||
| return; | ||
| Cell *c = cell_at(ct, ct->back, x, y); | ||
| for (int i = 0; i < CELL_MAX_COMBINING; i++) { | ||
| if (c->combining[i] == 0) { | ||
| c->combining[i] = cp; | ||
| return; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /* ── Escape sequence generation ───────────────────────────────────── */ | ||
|
|
@@ -238,6 +257,8 @@ static void present_cups(struct Clayterm *ct, int row) { | |
| emit_ch(ct, i, y, row, ' '); | ||
| } else { | ||
| emit_ch(ct, x, y, row, back->ch); | ||
| for (int ci = 0; ci < CELL_MAX_COMBINING && back->combining[ci]; ci++) | ||
| buf_char(&ct->out, back->combining[ci]); | ||
| /* mark trailing cells of wide char as invalid in front | ||
| * so they'll diff when overwritten by narrow chars */ | ||
| for (int i = 1; i < w; i++) { | ||
|
|
@@ -298,6 +319,8 @@ static void present_lines(struct Clayterm *ct) { | |
| if (!iswprint(ch)) | ||
| ch = 0xfffd; | ||
| buf_char(&ct->out, ch); | ||
| for (int ci = 0; ci < CELL_MAX_COMBINING && back->combining[ci]; ci++) | ||
| buf_char(&ct->out, back->combining[ci]); | ||
| for (int i = 1; i < w; i++) { | ||
| Cell *fw = cell_at(ct, ct->front, x + i, y); | ||
| fw->ch = 0xffffffff; | ||
|
|
@@ -391,6 +414,7 @@ static void render_text(struct Clayterm *ct, int x0, int y0, | |
| const char *p = slice; | ||
| int rem = slice_len; | ||
| int x = x0; | ||
| int last_x = -1; /* column of the most-recently written base cell */ | ||
|
|
||
| while (rem > 0) { | ||
| /* Check at the top of each iteration: if the pointer we are about to | ||
|
|
@@ -415,7 +439,11 @@ static void render_text(struct Clayterm *ct, int x0, int y0, | |
| cw = 1; | ||
| if (cw > 0) { | ||
| setcell(ct, x, y0, cp, fg, bg); | ||
| last_x = x; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not unconditionally set the if (ct->clipping) {
if (x < ct->clipx || x >= ct->clipx + ct->clipw)
return;
if (y < ct->clipy || y >= ct->clipy + ct->cliph)
return;
} |
||
| x += cw; | ||
| } else if (last_x >= 0) { | ||
| /* combining mark: attach to the preceding base cell */ | ||
| append_combining(ct, last_x, y0, cp); | ||
| } | ||
| p += n; | ||
| rem -= n; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this result in a warning being issued? It seems like if it was, then it should be issued only once.