Commit Graph
10 Commits
Author SHA1 Message Date
LGUG2Z 01179af18d ci(github): publish releases
The Komorebi License lets anyone distribute the source code but not binaries, so
the flake has no public binary cache and the binaries of komorebi come only from
this repository. The `build` job builds `komorebi` and `komorebic` with nix on
every run, on ubuntu-24.04 and on ubuntu-24.04-arm, and keeps them as an
artifact for 14 days; the arm64 job is `continue-on-error`.

As nix builds them, the binaries load libc, libm and libgcc_s through the
dynamic loader of the store and an RPATH into it, which only a machine with the
same nixpkgs revision in its store has. The job points them at the system's
loader at its standard path and drops the RPATH, with patchelf from the nixpkgs
flake.lock pins, then runs `--version` on the Ubuntu runner to check that they
start. Built from the locked nixpkgs (d3b13ad), the newest glibc symbol version
they need is 2.39, the glibc of Ubuntu 24.04.

The `publish` job runs on a `v*` tag, and on a scheduled or manual run on
master, once lint, vm-fast and build have passed. It zips the two binaries of
each architecture as `komorebi-<version>-<target>.zip` (an architecture whose
build failed is left out with a warning), adds schema.json, the schema of
komorebi.json, when the repository has one (it does), and writes checksums.txt
with sha256sum. The release notes are kokai's, from the commit messages since
the previous version tag, as komorebi for Mac writes them; kokai is not in
nixpkgs, so cargo installs it, pinned at 0.3.1. A tag becomes a release. master
replaces the rolling `nightly` prerelease and its tag; the old nightly tag is
moved to the commit first, so that the notes list the commits since the last
version and not since the last nightly. There is no Homebrew, winget or second
repository.

The cache key of the composite action gets the runner's architecture: the arm64
runner has the same `runner.os` as the x86_64 one, and would restore and save
the other's store.

The steps were run as written on aarch64 and x86_64 machines: the builds, whose
binaries then started in Ubuntu 24.04 root filesystems of both architectures,
and a first nightly, a later nightly and a tag release against a fake gh. GitHub
Actions has not run them yet. CONTRIBUTING.md says how a release is made, the
README where binaries come from and what the license allows, and DESIGN.md why.
The pages that said there were no prebuilt binaries (README, installation.md and
the entry on the binary cache in DESIGN.md) now say that the releases are the
only ones.
2026-10-28 07:39:49 -07:00
LGUG2Z 04aa311f2e build(nix): the KWin version the module supports
komorebi relies on the per-output virtual desktops of KWin 6.8, and the
repository follows the tip of nixpkgs-unstable, so a system can have a KWin that
is too old, or one that no VM suite has run against yet.

nix/supported-kwin.nix records both ends: min, the oldest KWin komorebi
supports, and testedMax, the newest one every VM suite has passed against
(6.8.1, with nixpkgs d3b13ad). It is a plain attribute set, so a CI job can read
it with `nix eval --file`. The module asserts that pkgs.kdePackages.kwin is min
or newer, and warns when it is newer than testedMax;
services.komorebi.kwin.untestedIsError makes that an error.

nixos-module-eval gives the system a KWin of a version of its own, so that no
check depends on the KWin of the lock, which moves with the tip, and checks the
assertion, the warning and the option in ten more checks. CONTRIBUTING.md,
PLATFORM.md and vm-tests.md say how testedMax is raised: by a commit, once every
suite has passed against the newer KWin, that says which suites ran.
2026-10-28 07:39:49 -07:00
LGUG2Z 9cb26daf38 build(nix): keybindings in the module
On NixOS the key bindings belong in the configuration, with the rest of
komorebi. services.komorebi.keybindings takes the format (skhd, whkd or sxhkd),
the bindings as text or as a file (exactly one of the two, asserted), the mode
(auto, engine or static; engine by default, as PlanMode::default() is),
chordTimeout (3000 ms), layout (us, the only one), strict and appAliases. skhd's
.load is refused by an assertion: the file it reads would not be in the store.

At build time the module runs komorebic hotkeys compile on the file in the
package komorebi-keybindings, whose share/kglobalaccel goes into the system
profile, where KWin finds the static shortcuts when the session starts. Its
diagnostics are in the build log and in diagnostics.txt, and strict passes
--strict, which fails the build on a warning or an error. execStart gains
--keybindings with the same store file, its format and mode, --chord-timeout and
an --app-alias per alias, so the daemon plans the file as the build did and runs
the bindings the build left to the engine. The unit already releases the keys
after the daemon stops.

The check keybindings-fixtures runs every file of the fixture corpus through
komorebic hotkeys compile --mode auto --dry-run, the command the module runs,
and diffs what it prints against nix/tests/keybindings.expected; it also checks
that docs/{skhdrc,whkdrc,sxhkdrc}.sample are the corpus's sample case.
nixos-module-eval gains the key binding scenarios: text, file, both, no format,
.load and a layout that is not us, and one with the typed config together with
key bindings, whose execStart has the checked file and then the keys. The corpus
gains the case vm, in the three formats: a binding a shortcut file can hold, and
one on the comma key, which X-KDE-Shortcuts cannot, so that the engine runs it
in auto mode. The VM suites vm-keys-skhd, vm-keys-whkd and vm-keys-sxhkd
configure the module with that case in their format and press both keys; they
share one script through the generator's script attribute.

For strict, nixos-module-eval has two scenarios with the same skhd text, which
has a line komorebic cannot read: strict = true and the default. Evaluation
never reads the key bindings, so the checks read the script of the build: with
strict it runs hotkeys compile --strict and exits 1 when that fails, without it
there is no --strict and the diagnostics are only printed to the build log. A
third check says the daemon gets the same command line either way. ARCHITECTURE
names them in the row for strict.

komorebic quickstart writes the sample skhdrc next to komorebi.json and
applications.json, from docs/skhdrc.sample, where the daemon and hotkeys compile
read it; the flake's source filter keeps that file for the build.

Docs: the user guide docs/usage/keybindings.md, the three sample files,
docs/example-configurations.md for KDE and NixOS, the options in
docs/installation.md, and the module's section of ARCHITECTURE. Whether the
engine or static shortcuts should be the default stays open in DESIGN: a static
shortcut took 233 ms to run its command at the median on KWin 6.8.1, the engine
3 ms, but only the static shortcut survives a dead daemon.
2026-10-28 07:39:49 -07:00
LGUG2Z 2f61b62276 feat(kwin): survive restarts of the daemon and KWin
kglobalaccel keeps the actions of a client that died, and their keys: a daemon
that is killed leaves its keys swallowed and running nothing until they are
unregistered. And kglobalaccel runs inside KWin, so a KWin that restarts forgets
every key komorebi registered. Neither should leave a user without keys.

kwin::hotkeys::release unregisters every action of the component, whatever
registered it, and komorebic hotkeys release runs it, with or without a daemon.
The NixOS module's unit runs it after the daemon stops (ExecStopPost), however
it stops, and a start already takes over what is left. The KWin backend's
NameOwnerChanged watch now also follows org.kde.kglobalaccel: when it has an
owner again, kwin::hotkeys::rebind loads the key bindings into the new one,
trying for 15 seconds.

Loading moves from the daemon's main into kwin::hotkeys::load, which keeps its
settings (Keybindings: file, format, plan mode, engine options), so that reload
can read the file again: the message ReloadHotkeys, komorebic hotkeys reload. A
new engine replaces the running one without unregistering its keys, and takes
them over as a start does, so the keys both files have stay grabbed. The message
HotkeysState answers with the engine's mode, the combos it holds and whether a
chain is pending (null without an engine); komorebic hotkeys state prints it
with the actions kglobalaccel has for the component. komorebic now depends on
komorebi for the two calls to kglobalaccel, and on serde_json for the state.

Tests: the socket messages without key bindings, state and release without a
session bus, reload without a daemon, and a nixos-module-eval check of
ExecStopPost. The VM test vm-restart-survival runs the module's unit in a Plasma
session: a key works, hotkeys state shows it; with the daemon stopped by SIGSTOP
the key runs nothing until hotkeys release gives it back; after SIGKILL the
unit's journal shows the release and the restarted daemon has the key; after the
KWin process is killed, the daemon registers the key with the new KWin; and a
reload of a changed file swaps the key.

vm-keys-engine asks the engine and does not sleep. A key that is not grabbed yet
reaches the focused window, which showed as a binding that does not run, so the
suite presses one once komorebic hotkeys state answers, which it does when the
engine has made the calls of its last event: for a mode, for a chord chain
beginning and ending, and for the keys of the application that takes the focus.
It waits for the old daemon to exit before it starts the next (a daemon started
meanwhile failed to load its KWin script), and activates a window until KWin has
it (KWin lists a window before it can have the focus). The last check reloads
the key bindings with komorebic hotkeys reload while wev has the focus and every
binding is for the terminal, a start with no key to grab.
2026-10-28 07:39:49 -07:00
LGUG2Z 4fa8108110 feat(kwin): animation through the Geometry Change effect
Windows that jump to their new places are hard to follow. komorebi animates
nothing itself: KWin's Geometry Change effect animates a window that a script
moves, and komorebi tells it how long that takes. The look the window manager
asks the backend for now has an animation, a duration that the settings give for
every kind of animation or, when one is set for the movement of windows, for
that one. The KWin backend writes the effect's settings into the user's kwinrc
(a duration, and no cross-fading, since a window going to its place changes
where it is and not what it shows), loads the effect when animation is on, and
unloads it and puts the settings back when it is off or komorebi exits.

komorebic gets animation, animation-duration and animation-fps. The module
installs the effect (effects.animation), and the session node sets the variable
that makes KWin load effects that animate under software rendering. vm-look
reads the screen a moment after a flip of the layout, with a 30 s animation and
without one.

The commands that change a stack turn the animation off while they run, as
komorebi does elsewhere, and komorebi.json takes an animation key (enabled,
duration and fps, each for every kind of animation or for one).
2026-10-28 07:20:49 -07:00
LGUG2Z 2c35e4bb6f feat(kwin): borders drawn by the compositor
komorebi draws no border. KWin's KDE Rounded Corners effect does, outside each
window in the gap the layout leaves, and komorebi says how it looks. The window
manager describes a Look (a border with a width, a radius and two colors) and
asks the backend for it with Backend::apply_look after every event, message and
retile. The backend answers whether borders are being drawn, and the layout
leaves room around the windows only while they are: a border with no effect to
draw it takes none.

The KWin backend writes the effect's settings into the user's kwinrc, with every
inner outline off (the effect's default draws one over the title bar), loads the
effect when borders are on and has it read them again. It unloads the effect and
puts the user's settings back when the borders go or komorebi exits. The module
installs the effect (effects.borders), which is built to stay unloaded until
komorebi asks, and vm-look reads the pixels around two windows in a real
session.

The session library of the VM tests also waits for the panel of the Plasma shell
now: a window komorebi tiles before the panel is made is tiled under it, which
hides the bottom of its border from a screenshot.

Kde.wait_for_panel_room waits until the daemon's work area is not the whole of
its monitor, which is the daemon having learned that the panel takes room, and
vm-look calls it where the daemon starts, before the windows open. The shell
makes its panel and KWin takes room for it a while later, which the daemon
learns of up to a second after, so a daemon that tiles in between puts the
windows under the panel and tiles them again, and a screenshot taken between the
two shows frames as high as the screen. It is a wait and not a check, since a
session that is too slow, as TCG often is, may never take the room.

komorebic gets border, border-width and border-offset, and visible-windows.

komorebi.json takes the same settings as border, border_width and border_offset,
and border_radius, which has no command. The module warns when config asks for
borders and the effect is not installed.
2026-10-28 07:20:49 -07:00
LGUG2Z c6874ff5db feat(cli): quickstart, autostart and the config commands
komorebic quickstart writes an example komorebi.json and applications.json into
the config home, asking before it writes over a file, so a new user has a
configuration to start from. komorebic enable-autostart writes the XDG autostart
entry that has Plasma start komorebi at login, with the komorebi.json it is
given if any, and disable-autostart takes it away; the NixOS module does the
same job for a machine that has it.

komorebic replace-configuration makes the running daemon start over from another
komorebi.json: it pauses its window manager and brings back the windows it hid,
the back of a stack, so that none is out of sight for the new one to miss, then
makes the window manager again from the file, with the monitors and the windows
the backend has, and watches the new file in place of the old one. A file that
does not read is logged and the daemon carries on as it was.
2026-10-28 07:20:37 -07:00
LGUG2Z 3a3a0f8f24 feat(wm): JSON schemas, and the module's typed options
komorebi.json is described as a JSON Schema, made from the Rust types that read
it: komorebic prints the schema of komorebi.json, of an applications.json, of a
notification and of a message, and the daemon answers the same four questions on
its socket. schema.json and schema.asc.json are committed (just jsonschema).

The module's config option is typed from the schema: schemars-nixgen makes
nix/komorebi-options.nix from schema.json (just nixgen), so a layout komorebi
does not have, or a value of the wrong kind, does not evaluate. The module
writes config as a komorebi.json and gives it to the daemon with --config.
configFile and the file the module writes are checked against the schema when
the system is built. applicationSpecificConfiguration takes files from GitHub,
and there are none by default, since no community file for Plasma is known.

Two checks keep the generated files honest without the tools that make them.
schema-fresh compares the schemas with what komorebic prints, and the hash on
the first line of the options with the schema. nix-options-validation evaluates
the options against the example configuration, written in Nix and in JSON.

nixos-module-eval also evaluates the module with the sources turned off: with
applicationSpecificConfiguration.enable = false the sources are left out of the
config that is written, and with no config either there is nothing to write and
the unit passes no --config. ARCHITECTURE has the guarantee row that names the
two checks.
2026-10-28 07:20:37 -07:00
LGUG2Z 5eaa613f50 feat(wm): configuration in komorebi.json
The daemon takes its settings from a komorebi.json: the file --config names, or
the one in the config home ($KOMOREBI_CONFIG_HOME, else ~/.config/komorebi).
Without a file it runs on its defaults, as before.

StaticConfig reads the file in three steps, because the monitors are known only
after the backend has been asked. preload makes the window manager and applies
the settings that are process-wide, postload gives each monitor and workspace
the entry of its place (by the index display_index_preferences gives a monitor's
serial number id, else in order), and main asks KWin for a desktop for each
workspace the file lists. A watcher on the file sends the daemon a
ReloadStaticConfiguration about two seconds after it changes, whether an editor
wrote the file in place or renamed a new one over it, and a file that does not
parse changes nothing.

A workspace can change its layout, its work area offset and its layout options
with the number of its containers, from the file or with komorebic
(workspace-layout-rule, its named form and the two that clear), and
layout_defaults gives every workspace of a layout the options it sets none of.
One file is meant to serve every platform, so a key that does nothing on Plasma
is accepted and ignored. config_parity checks the example and the files of other
platforms; the keys of features that are not in yet are listed there, and the
test fails when a list names a key that is read.

The file also sets what the stacks, the float override, the floating layer and
monocle set while the daemon runs: window_container_behaviour, with its rules by
container count in a workspace, float_override, float_override_placement,
floating_layer_behaviour (for a monitor and a workspace too),
floating_layer_placement and monocle_focus_behaviour. Their features come before
this commit, but a file to put the keys in does not, so they are part of this
one.

komorebic gains configuration, generate-static-config and start --config. The
NixOS module gets configFile, which the unit gives the daemon as --config.
2026-10-28 07:20:37 -07:00
LGUG2Z a86e2bdc63 build(nix): a NixOS module for komorebi
On NixOS, running komorebi should take one flake input and one module line. The
flake exports `services.komorebi` as nixosModules.default (and .komorebi). It
adds the flake's overlay, which gains komorebi-full, installs komorebi and
komorebic, and runs the daemon in every Plasma session as a systemd user unit,
wanted by plasma-workspace.target and ordered after plasma-core.target, which
Plasma orders after KWin. systemd starts a daemon that fails again; the exit
status of a SIGTERM (130) counts as success, so a stopped unit is not failed.
The unit's PATH is the one a terminal in the session has, instead of the few
store paths NixOS gives a unit.

The options are enable, package, logLevel and a read-only execStart, the command
line the unit runs. There is no configFile: the daemon reads no configuration
file yet, and an option that linked one would do nothing.

nixos-module-eval evaluates seven scenarios as real NixOS systems and checks
their assertions, the unit and execStart, with builtins.tryEval for the two that
must not evaluate. Nothing in it builds a Linux package, so it runs on a Mac as
well.

The VM nodes import the module, so every suite runs komorebi as the module
installs it. The headless node's komorebi@NAME runs execStart, which has
--log-level, so the nodes set logLevel to debug and the suites stop passing it;
latency, which times the daemon, keeps the default level. The session node
starts the module's unit at login; render-smoke turns it off, since a tiled
terminal would cover the desktop it looks at. A new fast suite, vm-module, logs
in to Plasma and checks that the unit runs the daemon with the module's log
level and PATH, that a window opened in the session is tiled, that a killed
daemon is started again, and that a stop is not a failure.

docs/installation.md covers NixOS first, and building from source on other
distributions. There is no binary cache: the Komorebi license does not allow
third parties to redistribute binaries.
2026-10-28 07:20:37 -07:00