Focus follows mouse was already there and already off. cosmic-comp has
supported it for as long as this fork has existed -- focus_follows_cursor and
focus_follows_cursor_delay, both live-watched -- and the schema already exposed
them under general. What was missing was the Hyprland spelling: there was no
input section at all, so a config written the way a Hyprland user would write
it named a setting that did not exist.
So this is an alias, not a new setting. input.follow_mouse and
input.follow_mouse_delay resolve to exactly the same cosmic-config keys as
general.focus_follows_cursor and its delay, and both spellings stay. Setting
both in one file is not an error; the last assignment wins, which is the rule
the rest of the file already follows. A test pins the two pairs to the same
targets, because a rebase that renames a target key would otherwise leave one
spelling working and the other quietly dead.
follow_mouse needs its own type. Hyprland writes it as a number and COSMIC
stores a bool, so Ty::FollowMouse maps 0 to false and 1 to true -- the same
trick Ty::Mode already plays for dark/light. Hyprland's 2 and 3 split pointer
focus from keyboard focus, which cosmic-comp cannot express: it has one focus
and either moves it or does not. They are rejected with a diagnostic that says
why, rather than rounded up to 1, because silently handing click-to-focus to
someone who asked for the opposite is worse than telling them the mode does not
exist here. Anything else gets the ordinary "expected 0 or 1" error.
There is no autoraise key because autoraise is not a separate feature.
raise_with_children runs inside update_active, which is what the
focus-follows-cursor timer ends up calling, so a floating window under the
pointer comes to the front as part of being focused. Tiled windows do not
overlap, so raising one is a no-op.
The delay is left at COSMIC's 250ms rather than shortened. It is what stops
focus from skating across every window the pointer crosses on its way
somewhere else, and that failure is more irritating than the wait.
Installing the binary before editing the config is the required order, not a
preference: resolution is transactional, so the old binary meeting an unknown
input section would refuse to write the whole file, not just that block.
`general.preserve_split` maps straight to the compositor key Patch C adds,
so opening a third window gives three panes in a row instead of quadrants.
Every other key under `general` exists upstream; this one is the fork's
own field on `CosmicCompConfig`. If a rebase ever loses that field the
compositor would ignore the key without saying anything, so the target is
pinned in a test of its own rather than left to the generic registry
checks.
The four-layer rofi chain needs config.rasi and local.rasi to exist, and
neither has an upstream file to copy: a HyDE theme has no equivalent of
either. They were hand-written for this machine, which meant a second machine
got a launcher that reported a missing @import instead of opening.
config.rasi is `include_str!`d from config/rofi/ rather than kept as a string
literal, so it stays a real .rasi file -- highlightable, diffable, editable
without a rebuild to see the result -- and there is one copy of it rather than
two that can disagree.
local.rasi is composed per machine from two things the repo cannot know: the
theme's $ICON_THEME, and a wallpaper path. Each half is omitted entirely when
there is nothing to say, because an empty `icon-theme:` list is something rofi
would honour.
$ICON_THEME comes back as a field on Import rather than being re-parsed out of
the conf text that the same function just rendered.
The wallpaper needed a stable name. local.rasi cannot hardcode a filename
without going stale at the next theme import, so plan_wallpapers now also
maintains ~/.local/share/wallpapers/hyprcosmic/current as a symlink to one of
the copies it made. HyDE has this problem too and solves it the same way, with
~/.cache/hyde/wall.thmb. The launcher sidebar and the autostart's `awww img`
line both name the link, so they cannot drift apart.
Which wallpaper it points at is the first in *sorted* order. read_dir returns
whatever the filesystem feels like, and an arbitrary choice is fine where an
unrepeatable one is not: re-running the import would otherwise change the
wallpaper at random. The link is repointed even when every wallpaper was
skipped as already installed -- the copies are theme-specific and unchanged,
but the link is global and has to follow the theme just imported.
Repointing has to handle a *dangling* link, which is exactly what a previous
import leaves behind once its theme directory is gone: symlink(2) fails with
EEXIST rather than replacing, and Path::exists follows the link, so it answers
false for the one case that needs removing. symlink_metadata asks about the
link itself.
Values reaching a generated config come from a theme directory that may have
been downloaded from anywhere, so quote_rasi_string strips quotes, backslashes
and control characters -- .rasi has no escape syntax worth relying on.
Four existing tests counted actions and broke once every plan carried two more.
Fixed with a theme_assets() filter rather than by bumping the numbers, so what
they are actually asserting stays visible and the next generated file does not
break them again.
125 unit, 5 bin and 4 integration tests pass; clippy --all-targets and
`cargo fmt --check` are clean. A real import of Tokyo Night installed all 13
paths, and `cosmic-conf apply --diff` reports no changes afterwards.
`watch::watch` has been written, tested and unreachable from the CLI since it
landed. It now has a command: `cosmic-conf watch [--config <path>]`, sharing
`--config` with `apply` and refusing `--diff`, which means nothing for a
daemon whose whole job is to notice a change and write it.
Exposing it made an existing wart user-visible: a single bad save reported
itself three or four times. One write arrives as several inotify events --
modify, close_write, and a rename when the editor writes atomically -- and
they do not all land inside one 250ms debounce window, so each produced its
own compile and its own copy of the same diagnostic. Consecutive identical
errors are now printed once, reset on any successful compile so the same
error after a good one is still news.
Verified against an isolated XDG_CONFIG_HOME, driving a real daemon rather
than calling `compile` directly, since none of this is reachable from the unit
tests: applies at startup, recompiles on edit, notices edits to sourced files,
picks up a `source` line added at runtime, survives a malformed edit with the
last good value intact, reports it exactly once, does not suppress a
*different* error, and resumes after a fix. Ten checks, all passing.
Mostly mechanical -- writeln! for format strings ending in a newline,
sort_by_key, slice::from_ref, &Path over &PathBuf, a stray &mut in a test.
Two were worth more than the lint that found them.
`parse_color` sliced `hex[i..i + 2]` after checking `hex.len()`. Both are byte
counts, so a multi-byte character inside `rgb(...)` split a char boundary and
panicked: `rgb(€abc)` is six bytes and aborted the compiler with "end byte
index 2 is not a char boundary". A typo in a config file must produce a
diagnostic, not a crash. Clippy did not see this -- it flagged the duplicated
`rgb(`/`rgba(` arms as foldable into `?`, and folding them is what put the two
length assumptions next to each other where the mismatch was visible. Now
guarded by is_ascii, with a test that panics without the guard.
`plan_verbatim` tripped too_many_arguments at 8. Three of them were the
`&mut Vec<Action>`, `&mut Vec<Note>` and `&mut Vec<AssetError>` threaded
through both plan helpers -- a Plan under construction, so `Draft` now names
it and `finish()` owns the errors-are-fatal rule that was previously inline.
118 unit tests plus 4 integration tests pass; `cargo fmt --check` and
`cargo clippy --all-targets` are both clean.
rustfmt and clippy were installed all along; I had wrongly recorded them as
missing and never ran either. This is the mechanical half -- files rustfmt
reformatted and nothing else touched, committed separately so the real fixes
in the next commit are readable.
`cosmic-conf apply --diff-only ~/.config/hyprcosmic/cosmic.conf` did exactly
the wrong thing twice over: the misspelt flag was ignored, so it wrote instead
of diffing, and the path was ignored too, so it wrote to whatever the *default*
config compiles to. It reported success either way.
Both arguments are now errors with exit 2. An argument parser that silently
skips the unknown is a bad fit for a command whose job is to overwrite
settings; the surface here is six flags, so the check is fifteen lines rather
than a dependency.
Keeping the imported theme in its own file, sourced from cosmic.conf, is what
stops a re-import from clobbering the keybindings. But a sourced file only
does anything if something sources it, and until now `import-theme --out`
reported "Wrote ..." whether or not anything did -- which looks like success
while the desktop stays exactly as it was.
It now checks the sibling cosmic.conf and prints the line to add when the file
is unreachable. The match is by filename and deliberately loose: it is looking
for evidence the user already knows about the file, not parsing the config.
The shipped template carries that `source` line commented out rather than
live, because `source` naming a file that does not exist is a hard error, and
a fresh checkout has no theme.conf yet. Copying the template and running
`apply` has to work before any theme is imported.
Three things stood between `assets.rs` and a themed desktop.
`import-theme` never called it. The module was written, tested and unreachable;
`--assets` now wires it up, with `--source`, `--overwrite` and `--dry-run`, and
finds the theme repo's Source/ directory by searching upward rather than
assuming HyDE's exact nesting depth.
The archive guard rejected every real icon theme. Refusing any `..` in a link
target is right for an entry path but wrong for a symlink: icon themes are
built out of relative links into sibling directories, and Tela ships thousands
of `../devices/network-wireless.svg`. What matters is whether the target
resolves inside the destination, which `stays_within_root` now decides
lexically -- no canonicalize, since the tree does not exist at plan time and
following real links during validation would be a TOCTOU window. Absolute
targets and links that climb past the root are still refused; the existing
escape tests still pass.
`apply` silently ignored `source`. It parsed and resolved inline while `watch`
went through `compile`, and `flatten` drops `Item::Source` -- so an include
that worked under `watch` vanished under `apply`. `apply` now uses `compile`
too. This matters immediately: the generated theme lives in its own
theme.conf, sourced from cosmic.conf, so re-importing a theme cannot clobber
the keybindings.
The waybar stylesheet claimed a theme could be dropped in ahead of it to
recolour the bar. It could not -- HyDE names its colours main-bg/wb-act-bg and
the rules referenced bar-bg/accent. Split into palette + theme + bridge +
rules, imported in that order, so the claim is now true. Verified by loading
the result through GTK's own CSS parser: with Tokyo Night installed main-bg
resolves to #24283b and wb-act-bg to #bb9af7; with an empty theme.css the
defaults stand. Both parse without error.
Two deliberate departures, both commented where they are made: the theme's
near-transparent bar-bg is composited at 0.85 because cosmic-comp has no blur
to put behind it, and theme.css is copied next to style.css rather than
imported from HyDE's own path, because a missing @import is fatal in GTK and
would break the bar on any machine without a theme.
`bind = SUPER, D, exec, rofi -show drun` is the most recognisable line in a
hyprland.conf, and the hyprcosmic profile makes it necessary rather than just
idiomatic: with cosmic-launcher and cosmic-app-library not running, COSMIC's
stock Super, Super+/ and Super+A bindings point at nothing.
Binds are the one repeatable key in the language -- many lines fold into a
single map instead of the last one winning -- so they bypass the schema, which
is built around one conf key naming one value. They land in the Shortcuts
`custom` key, which cosmic-comp merges over `defaults`, so the system file is
untouched and reverting means deleting the lines and re-applying.
Actions are rendered as RON text rather than modelled as an enum: COSMIC's
Action has forty-odd variants, this crate deliberately does not link the cosmic
crates, and the mapping table only ever needs a handful. Dispatchers without a
genuine equivalent are refused rather than approximated, since a keybinding
that silently does the wrong thing is worse than one that fails to compile.
Verified the emitted file deserializes into cosmic-settings-config's own
`Shortcuts` type: five bindings, keysyms XK_a/XK_slash/XK_Return, Spawn actions.
watch: source-include expansion by textual splicing so diagnostic spans stay
correct across merged files; debounced inotify; a bad edit prints diagnostics
and keeps watching rather than killing the daemon.
assets: plan/apply split mirroring emit.rs. Tarball entries and symlink/
hardlink targets are validated before extraction, sharing one routine between
plan and apply so the check cannot drift.
Added tests/archive_escape.rs as independent verification of that boundary.
The tar crate refuses to build hostile archives through its safe API, so the
fixtures write GNU header name/linkname bytes directly — the same thing a
malicious archiver does. Asserts on the filesystem afterwards rather than on
returned errors, and covers symlink indirection, where neither entry path
contains '..' yet a later write still escapes.
99 tests.
import-theme translates a HyDE hypr.theme into cosmic.conf, reusing the
Phase 1 parser — which is the payoff for choosing Hyprland-style syntax.
Nothing is dropped silently. Every source key either lands in the output or
carries a Note explaining why not, classified as NoEquivalent,
NeedsCompositorPatch, DifferentProgram or Lossy. Gradient borders contribute
their first stop as the accent and say so.
Handles real-world quirks found in actual theme files: HyDE's |> destination
header (no '=', would otherwise be a parse error), colon-keys like
shadow:enabled, and nested blur blocks.
Tests run against the verbatim Catppuccin-Mocha theme, and assert that the
generated conf both parses and resolves against the registry. Verified
end-to-end on Tokyo-Night, a theme absent from the tests: import -> apply
produced 8 correct cosmic-config files with 20 settings reported.
64 tests.
emit writes RON directly rather than linking libcosmic. Spike 2 showed
cosmic-config is a filesystem KV store whose notify watcher keys off file
paths (lib.rs:377), so an atomic write is observed identically to the typed
API — for the cost of ron instead of the whole libcosmic graph.
Two-stage: plan renders without touching disk, apply writes. Composites are
read-modify-write against verified upstream defaults — gaps (0,8) at
theme.rs:939, CornerRadii at corner.rs:20-31 — so setting one field never
drops its siblings. Unmodelled composites error rather than write blind.
52 tests. CLI verified end-to-end: apply, --diff (writes nothing), idempotent
rerun, partial update preserving siblings, and multi-diagnostic failure with
exit 1 and zero writes.
Spike results corrected the spec: ThemeBuilder.gaps is (outer, inner) at
cosmic-theme/src/model/theme.rs:895, lives under CosmicTheme.{Dark,Light}.Builder
rather than CosmicTk, and fans out to two components. Entry therefore carries
targets: &[Target].
34 tests, including the folding property that keeps gaps_out from clobbering
gaps_in. Bare #rrggbb colours rejected: # begins a comment, as in Hyprland.