diff options
| author | user <user@clank> | 2026-08-16 20:23:03 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-16 20:23:03 +0200 |
| commit | 2182ef505c6f12545cf156cca85a629256c28c72 (patch) | |
| tree | 75a53a5fe4715b65172a33df633dd382e3944528 /src | |
| parent | sl0ppty: docs that match the program (diff) | |
| download | slosh-2182ef505c6f12545cf156cca85a629256c28c72.tar.gz slosh-2182ef505c6f12545cf156cca85a629256c28c72.tar.xz slosh-2182ef505c6f12545cf156cca85a629256c28c72.zip | |
sl0ppty: layouts are `*.layout.kdl`, and each loader says which it got
`config/layout.example.kdl` becomes `config/example.layout.kdl`, and the
convention for the files people write is `project.layout.kdl`.
Not a new extension, and the reason matters: the syntax genuinely *is* the same
hand-rolled KDL subset for both documents -- one parser reads them -- so calling
layouts something other than KDL would trade one wrong signal for another and
lose every editor that highlights it. The confusion is about *schema*, so the
schema goes in the stem: the extension says how to read the file, `.layout` says
what is in it. `docker-compose.yml` and `tsconfig.json` settled this convention
long ago.
**But a filename cannot enforce anything, and that turned out to be the real
bug.** `sl0ppty --check` on a layout file printed `ok` and reported "bindings 53"
-- it had read a document containing nothing it understood and said so was fine.
A config that `include`d a layout linted clean for the same reason. So did
`rouded true`. The loader reads a document by *asking it for the keys it knows*,
which is fast and clean and never notices what it was given.
So it notices now. `KNOWN_TOP` is every node the loader reads, and anything else
at the top level is a complaint with a file and line:
s.kdl:2: unknown setting: rouded
example.layout.kdl:14: this is a layout, not a config: `sl0ppty --layout` reads those
sl0ppty: config.kdl: this is a config, not a layout: `gap` is a setting
The `layout` node gets its own message because it is the mistake people actually
make, and the reverse direction is the same list read backwards -- the layout
loader asks `config_is_setting()` rather than keeping a guess of its own, so the
two answers cannot disagree about which file is which. A layout whose own tabs
are misspelled still gets "layout declares no tabs", which is a different
mistake.
D2 records it, since it is the decision that two schemas share one syntax.
Verified: `tests/test_config.py` gained thirteen checks -- the unknown setting
with its line, both wrong-schema messages, the misspelled-tabs case keeping its
own, the shipped config linting clean and the shipped layout loading as one, and
a drift guard that greps `kdl_child(root, ...)` out of the loader and fails if a
setting is missing from `KNOWN_TOP` (verified by removing `gap_aspect` and
watching it fail). Full suite green; my own config and the reference config both
still lint clean, which is the thing this change could most easily have broken.
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.c | 14 | ||||
| -rw-r--r-- | src/config.c | 80 | ||||
| -rw-r--r-- | src/config.h | 5 |
3 files changed, 99 insertions, 0 deletions
@@ -2265,6 +2265,20 @@ bool app_apply_layout(app_t *a, const kdl_node_t *root, bool replace, char *err, } if (!made) { + /* The other half of telling the two documents apart. A file with `theme` or + * `keys` at the top of it is somebody's config, and "declares no tabs" is true + * of it in the least useful way -- the same message a layout gets when its own + * tabs are misspelled. `config_is_setting` answers from the loader's own list, + * so this cannot drift from what a config actually holds. */ + for (size_t i = 0; i < root->nkids; i++) { + const kdl_node_t *n = root->kids[i]; + if (n && n->name && config_is_setting(n->name)) { + if (err) + snprintf(err, errcap, + "this is a config, not a layout: `%s` is a setting", n->name); + return false; + } + } if (err) snprintf(err, errcap, "layout declares no tabs"); return false; } diff --git a/src/config.c b/src/config.c index d0cd347..d540dd5 100644 --- a/src/config.c +++ b/src/config.c @@ -1387,6 +1387,83 @@ const char *config_default_path(void) { * mistake, and the message says which one you have. */ #define INCLUDE_MAX_DEPTH 8 +/* Every node this file reads at the top level, so that one it does *not* read can + * be said out loud. Without this a whole document of the wrong shape -- a layout + * handed to `--check`, or `include`d into a config -- passes silently, because a + * loader that only asks for what it knows never notices what it was given. A + * mistyped setting did the same thing quietly. + * + * Kept honest by `tests/test_config.py`, which greps this file for the names it + * actually asks `kdl_child(root, ...)` for and fails if one is missing here. */ +static const char *const KNOWN_TOP[] = { + "include", /* read before the rest, but a name this file understands */ + "anim_ms", + "bell_indicator", + "bell_mark", + "close_mark", + "dim_unfocused", + "double_click_ms", + "editor", + "focus_follows_mouse", + "gap", + "gap_aspect", + "hints", + "hover_delay_ms", + "in_band_shaders", + "keep_dead", + "keys", + "min_mark", + "min_pane", + "min_split", + "modal_scrim", + "newtab_mark", + "padding", + "pane_buttons", + "rounded", + "scroll_lines", + "shader_dir", + "shaders", + "shell", + "states", + "status_bar", + "status_line", + "status_pad", + "theme", + "title_align", + "title_inset", + "toast_ms", + "version_banner", + "zoom_mark", + "zoom_on_mark", +}; + +/* Whether a name is one of this file's settings. Public so that the *layout* loader + * can tell somebody they handed it a config -- the two documents share a syntax, so + * telling them apart has to come from one list rather than two guesses. */ +bool config_is_setting(const char *name) { + if (!name) return false; + for (size_t i = 0; i < sizeof KNOWN_TOP / sizeof *KNOWN_TOP; i++) + if (strcmp(name, KNOWN_TOP[i]) == 0) return true; + return false; +} + +/* Named for what a stray node probably *is* rather than only for what it is not: a + * `layout` at the top of a document is somebody's session file, and the mistake + * worth naming is the one people make. */ +static void complain_unknown_top(config_t *c, const kdl_node_t *root, char *err, + size_t errcap) { + for (size_t i = 0; i < root->nkids; i++) { + const kdl_node_t *n = root->kids[i]; + if (!n || !n->name) continue; + if (config_is_setting(n->name)) continue; + if (strcmp(n->name, "layout") == 0) + complain(c, err, errcap, n->line, + "this is a layout, not a config: `sl0ppty --layout` reads those"); + else + complain(c, err, errcap, n->line, "unknown setting: %s", n->name); + } +} + static bool load_into(config_t *c, const char *path, int depth, char *err, size_t errcap); @@ -1507,6 +1584,9 @@ static bool load_into(config_t *c, const char *path, int depth, char *err, /* After the includes, because each of those set it to its own file while it * was being read. From here the complaints belong to this file. */ c->loading = path; + /* ...which includes "I have no idea what this document is", so it has to be + * said after the file is known and not before. */ + complain_unknown_top(c, root, err, errcap); c->gap = (uint16_t)kdl_arg_int(kdl_child(root, "gap"), 0, c->gap); c->gap_aspect = diff --git a/src/config.h b/src/config.h index 16c4218..365559c 100644 --- a/src/config.h +++ b/src/config.h @@ -420,6 +420,11 @@ size_t config_parse_chain_file(const char *path, color_t default_color, expr_prog_t **exprs, size_t *nexprs, char *err, size_t errcap); +/* Whether a name is one of the config's own settings. For the layout loader, which + * shares this syntax and needs to tell a config apart from a session file -- one + * list, so the two answers cannot disagree. */ +bool config_is_setting(const char *name); + /* What a chord does after the leader. Direct bindings answer here too, so * pressing the leader first never makes a binding stop working. */ action_t config_lookup(const config_t *c, int key, uint16_t mods); |
