Files
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

219 lines
8.8 KiB
YAML

# Build and check the parts of HyprCosmic that are not one of the two forks:
# cosmic-conf, the shared waybar/rofi assets, and the installer that places them.
#
# Two jobs with different shapes on purpose. cosmic-conf is compiled code and
# gets the same per-distribution matrix the forks do. The assets are text, and
# text does not care which distribution it is on -- what matters there is whether
# generated files are still in step with their generator, which is a single
# question with a single answer.
# The name is not "CI", which is upstream's ci.yml. Two workflows sharing a name
# share `${{ github.workflow }}`, and the concurrency group below is built from
# it -- they would cancel each other on every push, and whichever started second
# would be the only one that ever reported.
name: HyprCosmic
on:
push:
branches: [master]
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
cosmic-conf:
name: cosmic-conf (${{ matrix.distro }})
runs-on: ubuntu-latest
container: ${{ matrix.image }}
strategy:
fail-fast: false
matrix:
include:
- distro: fedora
image: fedora:latest
- distro: debian
image: debian:bookworm
- distro: arch
image: archlinux:latest
steps:
# Before checkout: actions/checkout needs git and these images are bare.
- name: Install build dependencies (fedora)
if: matrix.distro == 'fedora'
run: dnf -y install --setopt=install_weak_deps=False git curl gcc
- name: Install build dependencies (debian)
if: matrix.distro == 'debian'
run: |
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
git curl ca-certificates build-essential
- name: Install build dependencies (arch)
if: matrix.distro == 'arch'
run: pacman -Syu --noconfirm --needed git curl base-devel
- uses: actions/checkout@v4
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain stable --profile minimal \
--component clippy
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- uses: Swatinem/rust-cache@v2
with:
workspaces: cosmic-conf
key: ${{ matrix.distro }}
- name: Test
working-directory: cosmic-conf
run: cargo test --locked
# Warnings are errors here because the alternative is a build log nobody
# reads and a lint that has been failing for six months.
- name: Clippy
working-directory: cosmic-conf
run: cargo clippy --all-targets --locked -- -D warnings
- name: Build
working-directory: cosmic-conf
run: cargo build --release --locked
# The end-to-end question the unit tests cannot ask: does the cosmic.conf
# this repository actually ships still parse and resolve? A schema change
# that invalidates the shipped config would pass every test in the crate
# and break every user on first login.
#
# HOME is redirected so the resolution reports against an empty config
# tree rather than the runner's own. The binary is run directly instead of
# through `cargo run`: cargo keys its registry cache on HOME, so moving
# HOME would send it off to re-download every dependency into a directory
# the cache action does not know about.
#
# --diff writes nothing, and the last two lines hold it to that.
- name: The shipped cosmic.conf still resolves
working-directory: cosmic-conf
run: |
set -eux
fake="$RUNNER_TEMP/fakehome"
rm -rf "$fake"
mkdir -p "$fake/.config"
env HOME="$fake" XDG_CONFIG_HOME="$fake/.config" \
./target/release/cosmic-conf apply --diff --config ../config/cosmic.conf
test -z "$(find "$fake" -type f -print -quit)"
- uses: actions/upload-artifact@v4
with:
name: cosmic-conf-${{ matrix.distro }}
path: cosmic-conf/target/release/cosmic-conf
retention-days: 14
assets:
name: assets and installer
runs-on: ubuntu-latest
steps:
# Not `submodules: recursive`. This repository names 29 of them, 27 of
# which are the rest of COSMIC and none of which this job touches -- the
# only one needed is cosmic-session, because tools/install-assets.sh also
# places the session entry point that lives there. Cloning the other 27 to
# lint some CSS would cost several gigabytes per run.
#
# The script degrades gracefully when that checkout is absent -- it warns
# and skips those two files -- so a failure to fetch would quietly reduce
# what is covered rather than fail. Hence the assertion after it.
- uses: actions/checkout@v4
- name: Fetch only the session submodule
run: |
set -eux
git submodule update --init --depth 1 cosmic-session
test -f cosmic-session/data/start-hyprcosmic
# config.jsonc is generated, and the repository's rule is that it is never
# hand-edited: every Nerd Font glyph in it comes from a codepoint table in
# generate-config.py, because Private Use Area characters are destroyed by
# being retyped and indistinguishable in a diff. That rule is currently
# enforced by remembering it. This enforces it instead -- regenerate, and
# the file must not move.
- name: The generated waybar config is in step with its generator
run: |
set -eux
python3 config/waybar/generate-config.py \
config/waybar/config.jsonc.in config/waybar/config.jsonc
git diff --exit-code -- config/waybar/config.jsonc
# No PUA character may reach the template or the generator's own source.
# The generator refuses to emit non-ASCII, but nothing stopped one being
# pasted into its inputs until here.
#
# Written as an `if` rather than `! grep ...` because grep exits 1 for "no
# match" and 2 for "no such file", and negating it would turn a vanished
# file into a pass -- the check would quietly stop checking anything.
- name: The template and generator stay pure ASCII
run: |
set -eu
for f in config/waybar/config.jsonc.in config/waybar/generate-config.py; do
test -f "$f"
if LC_ALL=C grep -Pn '[^\x00-\x7F]' "$f"; then
echo "$f: non-ASCII above. Glyphs belong in the codepoint table," \
"not in the template." >&2
exit 1
fi
done
# A login-time script with a syntax error is a black screen with nowhere to
# print the reason.
# Each script is checked with the shell its shebang names rather than with
# bash across the board: hyprcosmic-fan is POSIX sh on purpose, and `bash
# -n` would happily accept a bashism that fails on a system where /bin/sh
# is dash.
- name: Syntax-check the shell scripts
run: |
set -eux
sh -n config/bin/hyprcosmic-fan
bash -n config/bin/hyprcosmic-keybinds
bash -n config/bin/hyprcosmic-powermenu
bash -n tools/install-assets.sh
# Every script is shellcheck-clean today, so this starts as a ratchet
# rather than a backlog. The runner image ships shellcheck; the install is
# there so that stopping to be true is a slow step and not a broken job.
- name: Shellcheck
run: |
set -eux
command -v shellcheck >/dev/null || {
sudo apt-get update
sudo apt-get install -y --no-install-recommends shellcheck
}
shellcheck config/bin/hyprcosmic-fan \
config/bin/hyprcosmic-keybinds \
config/bin/hyprcosmic-powermenu \
tools/install-assets.sh
# A round trip. Installing into a staging root and then asking --check to
# confirm it exercises both halves of the script against each other, and
# the audit it runs first refuses to proceed at all unless every file under
# config/ is classified as shared, per-user or a generator input. That
# audit is the real test: it is what stops a new file being silently left
# out of the install.
- name: Install into a staging root, then verify it
run: |
set -eux
DESTDIR="$PWD/stage" ./tools/install-assets.sh
DESTDIR="$PWD/stage" ./tools/install-assets.sh --check
find stage -type f -printf '%M %10s %P\n'
- uses: actions/upload-artifact@v4
with:
name: hyprcosmic-assets
path: stage/
retention-days: 14