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.
`rofi -show drun` came up unstyled. Three separate reasons, all of them
invisible: `import-theme` wrote ~/.config/rofi/theme.rasi, but rofi only ever
auto-loads config.rasi and nothing imported theme.rasi; theme.rasi holds a
HyDE palette and nothing else, no widget geometry at all; and no layout was
shipped anywhere for it to colour.
The layering mirrors what config/waybar already does, for the same reason --
a theme supplies colours, and the layout has to survive being handed a theme
that defines only some of them:
1. palette.rasi defaults, under the exact names HyDE themes use
2. theme.rasi the installed theme's rofi.theme, written by cosmic-conf
3. rules.rasi geometry and layout, with no colour literals at all
4. local.rasi per-machine paths: sidebar wallpaper, icon theme
Later imports win, so a theme recolours the launcher without rules.rasi
knowing a theme exists, and a machine points at its own wallpaper without
either of them knowing the path. Unlike waybar there is no bridge step: a HyDE
rofi.theme defines the names rules.rasi already references.
1 and 3 are shared and go under /usr/share/hyprcosmic/rofi. 2 and 4 must be
per-user, and config.rasi with them: it imports those two relatively, and rofi
resolves a relative @import against the importing file's directory.
rules.rasi is HyDE's style_1 with three deliberate departures, documented in
its header. HyDE computes the border width, radius and font in rofilaunch.sh
before invoking rofi; there is no launcher script here -- the keybinding runs
`rofi -show drun` bare -- so those are baked in. The sidebar image moves to
local.rasi because HyDE's ~/.cache/hyde/wall.thmb does not exist outside HyDE.
And dummywall gets `background-color: @main-bg` rather than transparent, so a
machine with no wallpaper set shows a panel instead of a hole.
The display-* labels are Nerd Font glyphs copied byte-for-byte out of
style_1.rasi. They are Private Use Area codepoints and do not survive being
retyped; verified as U+F303, U+F120, U+F07B, U+F2D0, each followed by a thin
space, and confirmed present in JetBrainsMono Nerd Font via
`fc-list :charset=`.
Verified with `rofi -dump-theme` and `-dump-config`: both exit 0 with empty
stderr, and the merged dump shows the Tokyo Night theme's main-bg beating the
palette default.
Without it "the file wins" only held at the moment someone last ran `apply` by
hand. Now cosmic.conf is compiled at login and recompiled on every edit to it
or to anything it sources, so whatever COSMIC's settings UI has stored since
the last login is overwritten before the desktop settles.
First in the file for that reason. The bar does not read cosmic-config, so the
ordering is for the compositor's benefit rather than waybar's.
No `--config`: the default path is derived from XDG_CONFIG_HOME inside the
process, so unlike the waybar line this needs no hardcoded home directory --
the one part of this file that is not portable as written.
A malformed edit stays non-fatal. It goes to the session log and the last good
configuration remains in place, so a typo cannot strand you at a broken
desktop; fix the file and the next save applies.
Requires cosmic-conf on PATH, which it now is (/usr/bin/cosmic-conf, release
build). `waybar` and `awww-daemon` are already named bare here, so the profile
resolves argv[0] through PATH.
`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.
The hyprcosmic profile does not start cosmic-bg, so until now nothing was
drawing a background at all.
HyDE calls this swww, and it is packaged for Fedora after all -- the
alebastr/sway-extras COPR carries it. Upstream renamed the project to awww at
0.12 and the package Obsoletes swww < 0.12.0, so `dnf install swww` lands
awww-0.12.1. /usr/bin/swww still exists as a shim, but it prints a deprecation
warning on every invocation and its own help says it will be removed in a
future update, so the autostart line uses the real name.
`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.
The profile disables cosmic-panel, so without these a HyprCosmic session is a
compositor with no bar, no clock and no tray. Waybar's shipped default is not
a substitute: /etc/xdg/waybar/config.jsonc is built entirely from sway/*
modules and renders nothing under cosmic-comp.
Module choices are pinned to what cosmic-comp actually advertises on the
Wayland registry, checked against a live session rather than assumed:
ext/workspaces ext_workspace_manager_v1, present in stock cosmic-comp.
wlr/taskbar zwlr_foreign_toplevel_management_v1, absent from stock
cosmic-comp and supplied by this fork's Patch A. Under a
stock compositor the module stays empty and the bar
otherwise works, so the config is not fork-only.
tray cosmic-panel normally hosts the StatusNotifierWatcher via
cosmic-applet-status-area; with the panel disabled waybar
hosts it instead.
The stylesheet keeps every colour behind an @define-color so that a HyDE
theme's waybar.theme -- which is only a list of such declarations, the bespoke
CSS being HyDE's own rather than any theme's -- can recolour the bar by being
sourced ahead of it.
Configs install to /usr/share/hyprcosmic/waybar so the autostart file needs no
per-user paths, which matters because the profile parser is deliberately not a
shell and cannot expand ~ or $HOME.
Verified: config.jsonc parses as JSONC and every compositor module placed on
the bar has a matching config block.
Running a second cosmic-session on the development machine turns out to be
genuinely dangerous, and twice it logged the developer out mid-session and
destroyed open work. Two distinct causes, both encoded here as guards:
- Name-based process selection cannot distinguish the fork, the system
install, or a stand-in binary; they all answer to `cosmic-session`. The
second logout came from `pgrep -x cosmic-session | head -1` inside the
test written to demonstrate that name matching is unsafe, because `head
-1` favours the oldest match, which is always the live desktop. The
harness therefore never selects a process by name: it spawns under
setsid and signals `-$PGID`, with the group ID taken from `$!`.
- A nested cosmic-session takes the well-known D-Bus name
com.system76.CosmicSession away from the running session on a shared bus
("Connection `:1.3` lost name ..." in the journal), destabilising the
outer desktop before anything is killed. The harness always runs under
dbus-run-session. Nesting cosmic-comp alone does not need this.
It also refuses to start without WAYLAND_DISPLAY, since the winit backend
would otherwise fall back to DRM and seize the real display, and it reaps
IPC socket directories whose owning PID is gone.
Verified: shellcheck-clean syntax; the no-WAYLAND_DISPLAY guard fires; an
audit confirms every `kill` targets the script's own process group and no
code path matches a process by name. NOT verified: the harness has never
been run against the real binaries. Session-level runtime testing is now
deferred to a VM or to logging into hyprcosmic.desktop directly, rather than
nesting inside the developer's live desktop.
cosmic-session joins cosmic-comp in .gitignore; both are forks that become
submodules under the topology in the design spec.
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.
HyDE-style desktop on cosmic-comp: single-file Hyprland-idiom config
compiler, wlr-foreign-toplevel + Hyprland-compatible IPC patches, waybar
in place of cosmic-panel.