aboutsummaryrefslogtreecommitdiffstats
path: root/src/config.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-15 18:44:07 +0200
committeruser <user@clank>2026-08-15 18:44:07 +0200
commit93ce0b645babd0851edb34701bbfffa5565daf65 (patch)
treeb402f934e5624a731fef4988d5d4e640766817fa /src/config.c
parentsl0ppty: watch the config directory even when it is not there yet (diff)
downloadslosh-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.c27
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) {