diff options
| author | user <user@clank> | 2026-08-15 18:44:07 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-15 18:44:07 +0200 |
| commit | 93ce0b645babd0851edb34701bbfffa5565daf65 (patch) | |
| tree | b402f934e5624a731fef4988d5d4e640766817fa /src/config.c | |
| parent | sl0ppty: watch the config directory even when it is not there yet (diff) | |
| download | slosh-93ce0b645babd0851edb34701bbfffa5565daf65.tar.gz slosh-93ce0b645babd0851edb34701bbfffa5565daf65.tar.xz slosh-93ce0b645babd0851edb34701bbfffa5565daf65.zip | |
sl0ppty: measure chrome in columns, not characters
Good catch: a two-column bell mark shifted the pane title row. Everything in
app.c that positions chrome -- title budgets, button placement, the hit rects
that have to agree with them -- came from a `cells()` that counted codepoints,
and a glyph drawn in two columns was booked as one, so every glyph after it
landed a column early.
Widths now come from lib-vt's own table, via ghostty_unicode_grapheme_width.
That is deliberately the same table the terminal uses for pane content, so
chrome and content cannot disagree about how wide something is, and it is
cluster-aware: an emoji with a variation selector is two codepoints and one
glyph. A wide cell claims its right half as a tail (width 0), which the
renderer already knew how to skip.
HANDOVER said this was a known trap and to dump the actual cells before
touching arithmetic, which is exactly how this went: the first assertion I
wrote passed against a deliberately broken width function, because it measured
the frame's right corner -- drawn at an absolute position, so it does not move
-- rather than the columns the mark claims. The test now reads the mark's own
colour runs and demands 1 column for a narrow mark and 2 for a wide one, and it
fails when the width function is stubbed back to 1.
Two things fell out on the way:
ghostty_unicode_grapheme_width measures only the *first* cluster and returns
the codepoints it consumed. My first loop ignored that and asked "did the width
change when I added the next codepoint?", which concludes that an entire ASCII
string is a single cluster -- the cheatsheet modal collapsed to ten columns
wide with the text clipped. Use the return value.
A cluster can also be longer than a cell holds: a ZWJ family emoji is 18 bytes
against 16. Both the cell writer and the config's mark fields (now 32 bytes)
clamp on a codepoint boundary, because snprintf cuts at a byte and half a
codepoint is invalid UTF-8 -- which showed up as a decoder error in a test far
from the cause, not as a short mark.
Diffstat (limited to 'src/config.c')
| -rw-r--r-- | src/config.c | 27 |
1 files changed, 21 insertions, 6 deletions
diff --git a/src/config.c b/src/config.c index 9473e34..d1b4eb6 100644 --- a/src/config.c +++ b/src/config.c @@ -268,6 +268,21 @@ static void bind_add(config_t *c, int key, uint16_t mods, action_t action, * `declared` is whether the config named `unfocused` itself, in which case it * has said what it wants -- including `states { unfocused { } }`, which says * "nothing", and must not be quietly refilled. */ +/* Copy a mark, cutting only where a codepoint ends. + * + * snprintf truncates at a byte, and a mark is a grapheme cluster: cutting one + * mid-codepoint puts bytes on the wire that are not UTF-8 at all, which shows + * up far from here as a garbled screen rather than a too-short mark. */ +static void set_mark(char *dst, size_t cap, const char *src) { + size_t n = strlen(src); + if (n >= cap) { + n = cap - 1; + while (n && ((unsigned char)src[n] & 0xC0) == 0x80) n--; + } + memcpy(dst, src, n); + dst[n] = 0; +} + static void apply_dim_unfocused(config_t *c, bool declared) { if (declared) return; if (!c->dim_unfocused) { @@ -923,15 +938,15 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) { c->pane_buttons = kdl_arg_bool(kdl_child(root, "pane_buttons"), 0, c->pane_buttons); const char *zm = kdl_arg(kdl_child(root, "zoom_mark"), 0, NULL); - if (zm) snprintf(c->zoom_mark, sizeof c->zoom_mark, "%s", zm); + if (zm) set_mark(c->zoom_mark, sizeof c->zoom_mark, zm); const char *zo = kdl_arg(kdl_child(root, "zoom_on_mark"), 0, NULL); - if (zo) snprintf(c->zoom_on_mark, sizeof c->zoom_on_mark, "%s", zo); + if (zo) set_mark(c->zoom_on_mark, sizeof c->zoom_on_mark, zo); const char *cm = kdl_arg(kdl_child(root, "close_mark"), 0, NULL); - if (cm) snprintf(c->close_mark, sizeof c->close_mark, "%s", cm); + if (cm) set_mark(c->close_mark, sizeof c->close_mark, cm); const char *mm = kdl_arg(kdl_child(root, "min_mark"), 0, NULL); - if (mm) snprintf(c->min_mark, sizeof c->min_mark, "%s", mm); + if (mm) set_mark(c->min_mark, sizeof c->min_mark, mm); const char *nt = kdl_arg(kdl_child(root, "newtab_mark"), 0, NULL); - if (nt) snprintf(c->newtab_mark, sizeof c->newtab_mark, "%s", nt); + if (nt) set_mark(c->newtab_mark, sizeof c->newtab_mark, nt); { /* `commands` (the default), `all`, or `none`. `true`/`false` are taken as * `all`/`none`, because that is what they used to mean here and a config @@ -954,7 +969,7 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) { c->bell_indicator = kdl_arg_bool(kdl_child(root, "bell_indicator"), 0, c->bell_indicator); const char *bm = kdl_arg(kdl_child(root, "bell_mark"), 0, NULL); - if (bm) snprintf(c->bell_mark, sizeof c->bell_mark, "%s", bm); + if (bm) set_mark(c->bell_mark, sizeof c->bell_mark, bm); const kdl_node_t *mins = kdl_child(root, "min_split"); if (mins) { |
