gitops 49d5f8b20e cosmic-conf: a fixed set of workspaces, with no compositor change
COSMIC's workspaces are dynamic and there is no setting that turns that
off. ensure_last_empty keeps exactly one trailing empty workspace and
collects every other empty one the moment its last window closes, so
"workspace 4 is the browser" is only true while the browser is open.

The primitive that fixes it already exists. can_auto_remove is

    is_empty() && !has_activation_token() && !pinned

and pinned_workspaces is an existing persisted CosmicCompConfig key that
Workspaces::add_output drains into the first output to appear. So this is
a projection and nothing else: no cosmic-comp patch, no new config key,
and the fast cosmic-conf CI job covers all of it rather than a 2.5 hour
package build per distro.

    workspace = 1, name:term
    workspace = 2, name:web
    workspace = 3, name:code

Three things about the restore path shaped the module.

Restore is positional. PinnedWorkspace has no index field -- the order of
the Vec becomes the order of the workspaces -- so `workspace = 4` cannot
emit one entry. It emits four, with 1..3 unnamed, or the browser
workspace comes back as workspace 1. That is why there is a cap: without
one, `workspace = 1000` is a typo that silently creates a thousand
workspaces rather than a diagnostic.

The dynamic workspace survives. Pinned workspaces are pushed into an
empty WorkspaceSet and ensure_last_empty then appends the usual empty
one, so declaring four gives 1-4 always present and a fifth appearing
when you use it. That is Hyprland's behaviour and not a compromise
either way.

It lands at the next login. Workspaces::new reads the key once at
compositor start and there is no reload path for it, while cosmic-conf
watch is started from the autostart file after COSMIC's own components.
Every other key in this file is live, so the one that is not is worth
saying out loud in cosmic.conf rather than leaving to be discovered.

Ids are generated rather than random

random_workspace_id is format!("{:x}", rand(0..2<<24)), at most seven hex
digits. Ours are FNV-1a of the index with the high nibble forced on, so
always eight -- a collision with a compositor-generated id is impossible
by construction rather than unlikely. FNV is written out rather than
reaching for DefaultHasher, which is explicitly not stable across Rust
releases: the id is what ties a window's saved workspace to the
workspace it reappears on, so it changing under a toolchain bump is a
bug that would be very hard to attribute.

monitor: is refused, not ignored

Hyprland's monitor: is the parameter a user reaches for first and the one
COSMIC cannot honour. output_matches compares the EDID before the name,
so an OutputMatch with edid: None is rejected outright against any output
that reports one, and only falls through to the name when neither side
has an EDID. Every real panel reports one, so a name-only match would
work on a VM and nowhere else. cosmic.conf cannot supply an EDID -- it is
a manufacturer triple, product id, serial and manufacture date read off
the wire by the DRM backend.

So the parameter fails with that explanation, the same way follow_mouse =
2 does. Accepting it and quietly doing nothing was the third option and
the worst of the three. The emitted OutputMatch is empty, which is not a
placeholder: an unmatchable match is how a workspace says it has no
output preference, which is the only thing this file can truthfully say.

tiling: defaults to general:autotile

A PinnedWorkspace carries its own tiling_enabled. Defaulting it to false
would have meant that turning workspaces on quietly turned tiling off for
someone whose whole reason for editing the file was autotile = true, so
resolve reads the folded autotile write and passes it through.

CI

The assets job syntax-checked two of the four shell scripts;
hyprcosmic-fan and hyprcosmic-keybinds were added in fc8c2a7 and never
registered. Both are now checked and shellchecked, and each is checked
with the shell its shebang names -- hyprcosmic-fan is POSIX sh on
purpose, and `bash -n` would accept a bashism that fails where /bin/sh is
dash. All four are clean today, so the list stays a ratchet.

Verified

The RON shape was taken from the struct definitions rather than guessed:
PinnedWorkspace has no serde defaults, so all four fields are emitted,
and the (field: value) / None / Some(x) spelling matches what COSMIC
itself wrote to this machine's xkb_config. 19 unit tests in the new
module and 6 in resolve cover the gap-filling, the id range, the
inherited tiling default, the duplicate-index diagnostic, and that a
quote in a name cannot break out of the RON.

Not verified: nothing was compiled here. cargo test, clippy -D warnings
and the release build run in the cosmic-conf job.
2026-08-11 08:02:41 +07:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-06-23 13:50:45 -06:00
2026-02-17 14:17:02 -07:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-07-14 16:17:56 -06:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-02-17 14:17:02 -07:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-07-29 09:15:17 -06:00
2026-02-17 14:17:02 -07:00
2026-07-14 16:17:56 -06:00
2026-07-29 09:15:17 -06:00
2024-06-03 11:10:30 -06:00

HyprCosmic

COSMIC's compositor, driven the way Hyprland is configured, wearing a HyDE shell.

It is a fork of cosmic-epoch, the meta-repository that names every COSMIC component and builds the desktop out of them. Two of its 29 submodules point at forks; the other 27 are System76's, unchanged. So this is not a re-implementation of COSMIC and not a theme pack sitting beside it — it is COSMIC, built from source, with a different shell on top and a different way of telling it what to do.

Three things distinguish a HyprCosmic session from a COSMIC one:

  • Hyprland's configuration idiom. A single ~/.config/hyprcosmic/cosmic.conf with general { } blocks, bind = lines and $variables is compiled into COSMIC's config tree. The file wins: what it names, it owns.
  • HyDE's shell. waybar instead of cosmic-panel, rofi instead of cosmic-launcher, awww instead of cosmic-bg. HyDE themes are imported directly, palette and wallpapers and all.
  • It replaces COSMIC rather than sitting next to it. The binaries install as /usr/bin/cosmic-comp and /usr/bin/cosmic-session, the paths a cosmic-comp and a cosmic-session go to, and the packages conflict with the distribution's accordingly. Both session entries are installed, so the greeter still offers a stock COSMIC shell for the day the HyDE one does not start — now served by these binaries rather than by a second copy on disk.

Repository layout

Everything in cosmic-epoch, plus:

Path What it is
cosmic-comp/ submodule → outbackdingo/hyprcosmic-comp
cosmic-session/ submodule → outbackdingo/hyprcosmic-session
cosmic-conf/ the config compiler and HyDE theme importer. A crate in this repository, not a submodule
config/ the shipped cosmic.conf, autostart, waybar and rofi assets, and the power menu
tools/install-assets.sh installs the parts of config/ that live outside $HOME, and --checks them for drift
docs/ the design spec, a debugging guide, and one written-up bug that is still open

The other 27 submodules stay on pop-os. Nothing about them needs to change, and pinning them to copies nobody maintains would be a promise to keep 27 forks current.

What the two forks change

cosmic-comp — four patches, each independent:

  • zwlr_foreign_toplevel_management_v1, which is the protocol waybar's window list and rofi's window mode read. Without it the taskbar is empty.
  • A Hyprland-compatible IPC socket (.socket.sock and the .socket2.sock event stream) under the names Hyprland clients actually open, so HyDE's scripts and waybar's hyprland/* modules work unmodified. The write surface is deliberately small: dispatch exec and dispatch killactive are rejected, because this is the surface any process that can open the socket gets.
  • New windows open beside the focused window rather than inside it.
  • The install goes to /usr/bin/cosmic-comp, at upstream's paths and alongside upstream's two .ron defaults files, which are carried unmodified.

cosmic-session — profiles. HYPRCOSMIC_PROFILE=hyprcosmic (set by hyprcosmic.desktop) skips cosmic-panel, cosmic-launcher, cosmic-app-library, cosmic-workspaces, cosmic-bg and cosmic-files-applet, then starts whatever ~/.config/hyprcosmic/autostart names. cosmic-greeter is deliberately not skippable — a display manager is the easiest thing to lock yourself out of. The fork installs three files where upstream installs seven; the four it drops are owned by the distribution's own cosmic-session package and writing them would make the two conflict.

Building

git clone --recurse-submodules https://github.com/outbackdingo/hyprcosmic
cd hyprcosmic
just build

Build dependencies are COSMIC's — see upstream's list, which is long and distribution-specific. rustup is recommended over the distribution's rustc: cosmic-comp is edition 2024 and pins Rust 1.93 in its rust-toolchain.toml, which is newer than several stable distributions ship — Debian bookworm's rustc is 1.63. just is likewise absent before Debian trixie; cargo install just --locked covers it.

Installing

The easiest route is a package. Every tag builds one for Fedora, Arch and Debian and attaches it to a draft release; workflow_dispatch on Packages builds them at any other time and leaves them as run artifacts.

sudo dnf install ./hyprcosmic-*.rpm          # Fedora
sudo pacman -U   ./hyprcosmic-*.pkg.tar.zst  # Arch
sudo dpkg -i     ./hyprcosmic_*_amd64.deb    # Debian

Expect this to fail the first time, and read what it says when it does. These packages provide /usr/bin/cosmic-comp and /usr/bin/cosmic-session, so they conflict with the distribution's cosmic-comp and cosmic-session and your package manager will refuse until those are removed. That refusal is the design: installing HyprCosmic replaces the machine's desktop, and it should take a deliberate dnf remove cosmic-comp cosmic-session to say so rather than a resolver deciding on your behalf. Both session entries survive the swap, so the greeter still offers a stock COSMIC shell afterwards.

Building it yourself instead:

sudo just install '' /usr

The two positional arguments are rootdir (a staging root, for packaging) and prefix. Use /usr, not the /usr/local default. Several files name /usr/share/hyprcosmic as a literal because they have no way to interpolate a prefix — a rofi .rasi has no variables, hyprcosmic.desktop has no way to expand one into Exec=, and autostart is deliberately not a shell. install-assets.sh prints the exact list when you use another prefix.

To stage instead of install:

just install /tmp/stage /usr

This installs all of COSMIC — the 27 unmodified components as well — plus cosmic-conf at $prefix/bin/cosmic-conf, the shared waybar and rofi assets under $prefix/share/hyprcosmic/, and hyprcosmic-powermenu.

install depends on build, which is upstream's arrangement and means sudo just install compiles as root. That is inherited, not chosen; if you would rather not, build into a staging root as your own user and copy it into place.

Then log out. HyprCosmic appears on the greeter's session menu next to COSMIC; both work.

Per-user setup

just install places nothing in a home directory — under sudo the only home directory it could see is root's. Four files are yours to place:

mkdir -p ~/.config/hyprcosmic/waybar
cp config/cosmic.conf config/autostart ~/.config/hyprcosmic/
cp config/waybar/style.css ~/.config/hyprcosmic/waybar/

style.css is per-user rather than shared for one reason: it @imports a sibling theme.css holding the installed HyDE theme's palette, and a relative @import resolves against the importing file. That sibling is written by import-theme --assets, so the bar is unstyled until you have imported a theme.

The fourth file, ~/.config/rofi/config.rasi, is written by import-theme --assets too, because it names per-machine paths.

Runtime dependencies of the shell itself are not COSMIC's and are not built here: waybar, rofi (wayland build), awww (formerly swww), and a Nerd Font for the bar's glyphs.

Configuration

~/.config/hyprcosmic/cosmic.conf, in Hyprland's idiom, compiled into cosmic-config by:

cosmic-conf apply           # once
cosmic-conf apply --diff    # show what would change, write nothing
cosmic-conf watch           # recompile on every edit, for the whole session

watch is the first line of the shipped autostart, which is what makes "the file wins" true at login and not only when you last ran apply by hand: whatever COSMIC's settings UI stored since then is overwritten before the desktop settles. A malformed edit is reported to the session log and the last good configuration stays in place, so a typo cannot leave you at a broken desktop.

The rule is one-way and deliberate. Keys this file names are overwritten from it on every login; keys it does not name are left entirely alone, so cosmic-settings remains the right place to change anything the file is silent about. There is no write-back — the GUI never edits cosmic.conf.

bind lines go to the Shortcuts custom key, which cosmic-comp merges over defaults, so the system defaults file is never touched and reverting is a matter of deleting the lines and re-applying. Hyprland spellings and COSMIC spellings are both accepted for the same setting (input:follow_mouse and general:focus_follows_cursor), and the last assignment wins. Where a Hyprland value has no COSMIC equivalent — follow_mouse = 2 and 3, which separate pointer focus from keyboard focus — it is rejected with an explanation rather than quietly rounded.

What the shipped file sets up, since the components those keys used to reach are no longer running:

Binding Does
Super (tap), Super+/, Super+A rofi -show drun
Super+W rofi -show window, in place of the workspace overview
Super+Return cosmic-term (Super+T still works — cosmic-comp handles that one itself)
Super+Shift+E hyprcosmic-powermenu: lock, suspend, log out, reboot, shut down

The power menu is there because cosmic-panel hosts COSMIC's power applet, and without the panel a session had no way out short of systemctl reboot from a terminal. The same script backs waybar's power button, so the two cannot drift apart, and it confirms before anything that ends the session.

See config/cosmic.conf; it is commented at length and is the reference for what is supported.

Theming

cosmic-conf import-theme ~/.config/hyde/themes/'Tokyo Night'/hypr.theme \
    --out ~/.config/hyprcosmic/theme.conf --report --assets

This translates a HyDE theme into conf keys, and with --assets also installs the wallpapers, GTK and icon themes, and the waybar/rofi/kitty theme files that sit beside hypr.theme. --report prints everything that did not translate cleanly, which is the honest half of the output.

theme.conf is written as a separate file and sourced from cosmic.conf rather than pasted into it. That keeps re-importing from touching your keybindings, and anything you want to override can simply be repeated later in cosmic.conf, since the last assignment to a key wins. The source line ships commented out — a source naming a file that does not exist is a hard error, and no theme is imported on a fresh install. Uncomment it once you have run the command above; import-theme says so as well.

Change the wallpaper by repointing the current symlink that --assets maintains, not by editing autostart:

ln -sfn ~/".local/share/wallpapers/hyprcosmic/<theme>/<image>" \
        ~/.local/share/wallpapers/hyprcosmic/current

Continuous integration

Two workflows, on purpose:

  • .github/workflows/ci.yml is upstream's, unmodified. It builds the entire desktop on Arch via just sysext, which is exactly the check a meta-repo wants and is not made less useful by forking.
  • .github/workflows/hyprcosmic.yml covers what upstream's does not: cosmic-conf built, tested and clippy-clean on Fedora, Debian and Arch; a check that the shipped cosmic.conf still parses and resolves against the current schema; that config/waybar/config.jsonc is still in step with the generator that produces it; that the template and generator stay pure ASCII; and an install-assets.sh round trip into a staging root, verified with --check.

The two forks carry a hyprcosmic.yml of the same shape, each building on Fedora, Debian and Arch and asserting that its install landed at upstream's paths — and that nothing landed in the private /usr/libexec/hyprcosmic/ this fork used to use, which is the assertion that would otherwise rot quietly.

packages.yml in this repository builds installable packages for the same three distributions: an RPM, a .pkg.tar.zst and a .deb, each compiled inside a container of the distribution it targets so the sonames it records are the ones the installing machine will have. It runs on tags and on demand, not on every push — three full desktop builds is hours of runner time. Tags additionally open a draft release with the packages attached; drafts rather than published, because installing one of these replaces the machine's desktop.

The waybar generator deserves its own note. config.jsonc is generated from config.jsonc.in and a codepoint table in generate-config.py, and is never hand-edited: Nerd Font glyphs live in the Private Use Area, where they are destroyed by being retyped and indistinguishable from each other in a diff. CI regenerates the file and fails if it moves.

Known gaps

  • The /usr/share/hyprcosmic literals described under Installing.
  • docs/unreproducible-dead-input-2026-08-10.md records a session that came up without input and has not been reproduced since. It is written down rather than closed.

Trademark

COSMIC is a System76 trademark. This fork is not affiliated with or endorsed by System76. See TRADEMARK.md, which is upstream's policy and applies here.

Upstream

For COSMIC itself — the component list, packaging status, translations, and how to install it on your distribution rather than building it — see pop-os/cosmic-epoch.

S
Description
Next generation Cosmic desktop environment
Readme
645 KiB
Languages
Rust 80.2%
Shell 12%
CSS 3.3%
Python 2.5%
Just 2%