Config conventions#
Binding spec — read before touching config. This is the config-layer counterpart to
mshell-crates/mshell-frame/DESIGN.md(which governs UI). It covers who owns which file, what is machine-written vs hand-edited, where a new knob goes, and what must never be merged. When a task touches config — adding a setting, a Settings page, a managed fragment, or reorganising~/.config/margo— follow these rules without re-deriving them.User-facing docs are separate:
configuration.md(a walkthrough) andconfig-reference.md(every knob). This file is the conventions, not the knob list.
0. Two config worlds — pick the right one#
There are two unrelated config systems, both exporting types named
Config / Menu / Position. Always verify the import path.
margo-config |
mshell-config |
|
|---|---|---|
| Owner | compositor (margo) |
shell (mshell) |
| Format | plain-text key = value .conf (PCRE2 window rules) |
YAML profiles |
| File | ~/.config/margo/config.conf (+ sourced fragments) |
~/.config/margo/mshell/profiles/<name>.yaml |
| Live edit | mctl reload re-parses |
reactive_stores store, hot |
| Add a knob | §4 | §5 |
A compositor behaviour → margo-config. A shell/bar/menu behaviour →
mshell-config. They never share a struct.
1. Who owns each file in ~/.config/margo#
Most files are not interchangeable clutter — each is owned by a specific binary and several are machine-written. Do not move or merge across owners.
| Path | Owner | Written by | Hand-edit? |
|---|---|---|---|
config.conf |
margo | user (often a dotfiles symlink) | yes |
conf.d/colors.conf |
margo (sourced) | matugen (wallpaper palette) | no — regenerated |
conf.d/taglayouts.conf |
margo (sourced) | mshell (Settings → Tiling Layout) | tolerated |
conf.d/mlayout.conf |
margo (sourced) | mlayout (active-layout symlink → ../layouts/…) |
no — runtime state |
binds.d/*.conf |
margo (sourced) | plugin manager (plugin keybinds) | tolerated |
layouts/layout_*.conf |
mlayout |
mlayout (+ user-saved) |
yes |
config.conf also has one machine-writer of its own: mctl config
rollback ($XDG_STATE_HOME/margo/config-generations/ holds the
history it restores from — see the config-generations-rollback spec).
Unlike the fragments above, it overwrites config.conf itself — a
rollback can clobber hand-edits made since the last successful boot or
mctl reload — and, per §2, it writes through the symlink when
config.conf is one, landing the change in whatever file the symlink
points at.
Margo's own sourced fragments are grouped under conf.d/ (matugen
colors, mshell taglayouts, the mlayout active-layout symlink); plugin
keybinds keep their own binds.d/; monitor-arrangement snapshots live in
layouts/. config.conf itself stays at the config-dir root (that's what
margo -c reads). mlock.conf / mlogind-variables.toml / mpower.toml are
not margo config — they're separate tools' files that happen to share the
dir, so they stay at the root, not in conf.d/.
| mlock.conf | mlock | user | yes |
| mlogind-variables.toml | mlogind | user | yes |
| mpower.toml | mpower | mshell (Settings → Power → Automatic Power Profile) + user | yes — re-read every tick |
| twilight/ | twilight | twilight + user presets | presets yes |
| mshell/profiles/*.yaml | mshell | mshell | tolerated |
| mshell/plugins.toml | mshell | mshell (plugin manager) | no |
| mshell/plugins/<key>/ | mshell | plugin manager (install) | no |
Rule: the file count reflects this multi-tool design. Reducing it by
merging machine-written fragments into config.conf breaks the writer
(matugen, mshell, the plugin manager hardcode their output paths). Don't.
2. Hand-edited vs machine-written fragments#
config.conf pulls fragments in with source = <relative-path>:
source = conf.d/colors.conf # matugen-written
source = conf.d/taglayouts.conf # mshell-written (Settings → Tiling Layout)
source = conf.d/mlayout.conf # mlayout active-layout symlink → ../layouts/
source = binds.d/<plugin>.conf # plugin-manager-written
- Machine-written fragments are append-or-overwrite targets for their tool.
Their path is hardcoded in the writer. Never inline them into
config.confand never relocate them without changing the writer too. config.confis frequently a symlink into a dotfiles repo (e.g.arch-config/…/margo/config.conf). Treat it as possibly-not-writable-in-place: editing it edits the dotfiles repo, and the paths itsources are part of that repo's contract. Don't restructure sourced paths without coordinating.
3. The "managed fragment + mctl reload" pattern#
When the shell needs to drive compositor config (a Settings page that
writes a .conf), do NOT reach into the compositor — they are separate worlds
(§0). Instead:
- Write a managed fragment under
~/.config/margo/conf.d/<name>.conf(mshell owns it; createconf.d/if missing). - Ensure
config.confsources it once (source = conf.d/<name>.conf, append-if-missing, whitespace-tolerant check — seeconfig_sources_us). - Run
mctl reload.
Examples: taglayouts.conf (Tiling Layout), binds.d/ (plugin keybinds),
the Keybinds editor. This is the same rule DESIGN.md §8b states for Settings
pages; shell-owned settings instead go through config_manager() (the
reactive store), never a managed .conf.
mctl reload re-parses config in the running binary — it does not
pick up new compositor code. A compositor behaviour change needs a margo
rebuild + relogin; only config values are live via reload.
4. Adding a compositor knob (margo-config)#
Touch all four or the knob silently no-ops / fails validation:
margo-config/src/types.rs— field on theConfigstruct and itsDefault.margo-config/src/parser.rs— amatcharm inparse_lineand add the key to theKNOWNkeys array.margo-config/src/validator.rs— add the key to the known-keysmatches!list (elsemctl check-configflags it andreload_configreturns early before applying anything — a real failure mode).- Consume it in
margo/src/…(and re-apply onreload_configif it should be live, not only at startup — see §6).
5. Adding a shell knob (mshell-config)#
mshell-config/src/schema/…— field on the struct. It derivesStore/Patch, so reactive accessors (config.menus().x_menu().field()) are generated automatically.#[serde(default)](container) fills a missing field from the struct'sDefault— not from a per-field intent. For a non-false/non-zero default on a bool/scalar, add#[serde(default = "fn")], or older saved profiles won't pick it up. (This is why a default-value flip can't reach existing profiles by editingDefaultalone.)- Read it via
config_manager(); writes go throughupdate_config. There is no migration layer — design new fields to default safely.
6. Precedence & live-apply rules#
- Per-tag tiling layout precedence:
taglayout>tagrule layout_name>default_layout. Enforced inapply_tag_rules_to_monitor(skip a tag's rule layout when it has ataglayout) +reload_config(seed taglayouts, then applydefault_layoutto tags with neither an override nor a live user-picked layout). A manualsetlayoutsetsuser_picked_layoutand is never clobbered.floatingis a valid value fortaglayout/tagrule layout_name/default_layout— it routes throughLayoutId::from_namelike any other, but bypasses the tiler entirely: the compositor auto-floats the tag's windows and cascades their placement, and re-tiles them when the tag switches back to a tiling layout (issue #1). - If a setting should take effect on
mctl reload, re-apply it inreload_config— seeding only at output/Pertag creation means it lands at the next start, not on reload (the bug behind "Apply does nothing"). - Before adding a knob, check for an existing mechanism on the same axis.
Two ways to set one thing (e.g.
tagrule layout_namevstaglayout, or the reverted menu min/max vs auto vs plugin-panel min/max) need an explicit precedence or they fight. Define it up front.
7. mlayout (monitor arrangement) convention#
- Named layouts are
layout_<slug>.conffiles in~/.config/margo/layouts/;mlayoutscans that subdir (gather_layouts). - The active layout is the
conf.d/mlayout.confsymlink → the chosen../layouts/layout_<slug>.conf(relative target, dotfiles-portable);config.confsource = conf.d/mlayout.confpicks it up. The symlink is the runtime selection; thelayouts/layout_*files are the catalogue. - This is idiomatic —
mlayout.conf → layout_*.confis the design, not a redundant chain. Distinct from §6's tiling layout (taglayout), which is a different concept (per-tag tiling algorithm, not monitor arrangement).
8. Plugin install = runtime files only#
Installed plugins live in ~/.config/margo/mshell/plugins/<key>/. The shell
runs only the manifest.toml, the wasm entry, and any scripts/assets the
manifest references (*.sh, sounds/, …). The install copy
(mshell-plugins/src/git.rs::copy_dir_all) skips plugin source (src/,
Cargo.toml/lock, README, .gitignore, target/, .git) — the source
lives in the plugin repo, not the config dir. Keep it that way; don't
reintroduce a whole-dir copy.
9. Settings pages: changed-settings + Defaults scope#
Every Settings page can show "N changed", reset itself with Defaults,
and highlight changed rows (mshell-settings/src/page_defaults.rs, KDE
System Settings style). What a page owns is generated, not hand-kept:
mshell-settings/build.rs scans the page sources listed in its PAGES
table —
- shell: field paths written in
update_config(|c| c.a.b = …)closures andreactive_settings! { group: g, … => key }bindings → JSON pointers/a/b,/g/key; - compositor: string literals that are margo options
(
margo-configOPTION_KEYS) in a page that editsconfig.conf.
Rules:
- New page with settings → add
(route, &[files])toPAGES. Routes are the stack routes (bar,theme/fonts,widgets/clipboard). - Data the user built is never a "default". Lists like bar widgets,
autostart scripts, subscriptions, blocklists, rules, per-tag layouts go in
USER_DATA(shell) or the page stays out ofPAGES(compositor rule blocks, managed fragments). Live state (game_mode.active) too. - Tag each row with the setting it edits so it can be highlighted:
set_widget_name: "setting:/toasts/battery"(shell) or"setting:conf:borderpx"(compositor). A test checks every tag against the page's generated scope. - Defaults on a compositor key comments the line out in
config.conf(never deletes it — §2, it may be a dotfiles symlink) and runsmctl reload; margo's built-in default applies. Shell keys are set back toConfig::default().
10. Quick checklist#
- Compositor setting → §4 (types + parser + KNOWN + validator). Shell setting → §5. Never the wrong world (§0).
- Shell driving compositor config → managed fragment +
source+mctl reload(§3); shell-only →config_manager(). - Should it be live on reload? Re-apply in
reload_config(§6). - New per-tag / per-menu knob → is there already a mechanism on that axis? Define precedence (§6).
- Don't merge/relocate machine-written fragments (§1/§2);
config.confmay be a dotfiles symlink (§2). - New Settings page / row →
build.rsPAGESentry +setting:row tags; user-built lists stay out of Defaults (§9).