Commit Graph
21 Commits
Author SHA1 Message Date
LGUG2Z 0e8e25957c build(nix): the demo guest as a VM suite
The demo guest is what the command `run-kde` starts, so it should not stop
working between the times somebody runs it by hand. The suite `run-kde` puts the
guest module, nix/dev/run-kde.nix, on the session node, as the rdp suite does
with krdp.nix, and checks what only a booted session can say: the demo opens two
Konsole windows, Kate and Dolphin and komorebi tiles them clear of the panel,
the cheat sheet is a window that is not a tile, what it shows of `komorebic
check` has no error, and KWin says what it draws with.

The node makes the directory the host shares as the guest finds it once it has
mounted it: owned by a user the guest does not have, with a password and a
public key in it. The suite checks that the guest takes them as root before the
login, that ssh lets in that key and no other, that KRDP starts by itself and
listens, and that a client with the password is let in and shows the display.

The client of those RDP checks moves from the rdp suite into the library as
RdpClient, with unit tests against the fake machine, so that both suites run the
same checks; the machine protocol gains copy_from_machine for it.
2026-10-28 14:20:08 -07:00
LGUG2Z e37f275b9b feat(nix): run-kde, the command
Starting the demo guest by hand takes a build, a state directory, a password, a
key, the sizes of the machine and the right arguments for its display, and a
mistake shows up as an error from QEMU a minute later. `run-kde` does all of it:
`nix run .#run-kde` (or `just run-kde`) checks the machine, builds the guest
when its runner is not in the store, with the build log shown, makes the state
directory with a sparse disk, a random RDP password and an ssh key (all mode
0600, none of it in the Nix store), and starts the runner with KVM, the memory
and cores the machine has, the repository at /repo, and the state directory at
/run/run-kde. It prints a banner with the RDP address and password, the ssh
command, how to quit, and which graphics mode it chose and why.

`--doctor` prints every prerequisite as a tick, a cross or a warning with the
exact fix: WSL 2 or not, KVM and its group, nested virtualization, virtiofs (a
virtiofsd started as the user with the options of the guest's runner must serve
a directory, since the guest mounts its /nix/store through one; /dev/fuse is
only a warning), memory, disk, WSLg, Windows interop, the GPU and the OpenGL of
the host, and the lines of .wslconfig. It exits with 1 when something is a
cross, which includes a missing KVM unless `--tcg` allows software emulation.
The GPU is used when the display is a window, the host has a GPU device and its
OpenGL is not llvmpipe, and `--gpu on|off` overrides it; on WSL 2, `--rdp` opens
mstsc with a credential that lasts as long as the run.

The script reads the machine through variables, so `--dry-run` and `--doctor`
can be tested against machines that a check makes up: `run-kde-dry-run` and
`run-kde-doctor` compare their output with the texts in nix/tests/run-kde, and
the package, which shellchecks the script when it is built, is a check too.
2026-10-28 14:20:08 -07:00
LGUG2Z 041dd35224 build(nix): the demo guest
A person who wants to see komorebi tile windows should not have to configure a
machine first. `run-kde-x86_64` and `run-kde-aarch64` are the session guest with
a demo on it: the NixOS module configures komorebi from a typed `config` (eight
workspaces, borders, animation) and the sample skhd bindings, and at login the
guest opens two Konsole windows, Kate and Dolphin, which komorebi tiles, and a
cheat sheet window that it leaves alone, with the key bindings, `komorebic`
commands to try, what KWin draws with, and the output of `komorebic check`.

The closure is the same for everyone. No password or key is in it: the host
shares a directory, which the guest mounts at /run/run-kde, and a unit copies
the RDP password and the ssh public key from it, since the files can belong to a
user of the host who is not the guest user. krdp.nix reads the password from the
file that `komorebi.krdp.passwordFile` names when it exists. The display device,
the port forwards, the memory and whether to insist on KVM are left to the
environment of the runner, so that one closure and one disk run with a software
virtio-vga or with a virtio-vga-gl that renders on the host GPU. The module is a
separate file that does not import plasma-vm.nix, so that a VM suite can put it
on the session node.

plasma-guests-eval asserts the same of the demo guests.
2026-10-28 13:27:11 -07:00
LGUG2Z 146e794b7b build(nix): guests for a Linux host
The Plasma guests were made for an Apple silicon Mac only: the flake fixed the
guest to aarch64-linux and the host of QEMU to the Mac. `mkPlasmaGuest` now
takes the system of the guest and the packages of the machine that runs QEMU,
and the flake has the two QEMU guests for a Linux machine with KVM as well:
plasma-vm-x86_64 and plasma-vm-session-x86_64, and the same pair for aarch64
(plasma-vm-aarch64, plasma-vm-session-aarch64). The three guests of a Mac keep
their names and are built from the same instructions.

An x86_64 machine has a VGA that QEMU adds unless it is told not to, which would
be a second display next to the virtio-gpu that KWin draws on. An x86_64 guest
therefore gets virtio-vga, a virtio-gpu that is also the VGA, and `-vga none`;
an aarch64 guest has no VGA and keeps virtio-gpu-pci.

plasma-guests-eval evaluates every guest and asserts what each is for: the
architecture of the guest, the system of its host, KVM or HVF, and one display
device. docs/dev/utm.md says which guest is for which host and how to build and
run one on a Linux machine; the README and CONTRIBUTING say that there are
guests for both.
2026-10-28 13:27:11 -07:00
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 70c2affa9d docs(repo): compare the three komorebi programs and fix what the docs got wrong
docs/dev/COMPARE.md says what komorebi for KDE shares with komorebi for Windows
and komorebi for Mac, what differs, what KDE does by itself and what is not
supported, for a reader who knows komorebi on one of them. Its tables are
generated by scripts/compare-gen.py from this checkout and from checkouts of the
other two, because a count or a name typed by hand is wrong after the next
change to a command, a key or an event. The script also checks the notes that
say why a command has no counterpart, and the sentences of the document that
depend on a table, and stops when one of them is no longer true. CI has neither
checkout and cannot run it; CONTRIBUTING says when to run it. The tables have no
column for the commit that added a row: the plan that numbered the commits is
not in the repository, and a number the script cannot derive for the next commit
would stop it.

The other documents had places that said something the repository does not carry
or does not do:

- DESIGN cited docs/kde.md, which no commit carries, said spike 8 "asked
  whether" one komorebi.json could be shared, and described the default of the
  key binding mode as a plan. Each entry now states the rule and the evidence.
  The first entry no longer says that the Windows implementation is the
  reference: both it and the macOS one were read, and the native thing is
  preferred where the other two need a workaround.
- "The spikes" was used in PLATFORM, DESIGN and COMPARE and defined nowhere.
  PLATFORM's vocabulary defines it, and PLATFORM has a list of what was not
  measured, which COMPARE links to.
- ARCHITECTURE said a maximized window is hidden with its workspace and shown
  with it, and that the code has a TODO for cycling the focus. A workspace
  switch hides nothing, since KWin shows and hides the desktop's windows, and
  the cycle leaves a maximized window as it is; the TODO and the commented-out
  code under it in window_manager.rs are replaced by a comment that says so. The
  Key bindings section no longer repeats the headings of the sections before it.
- README listed the subcommands that work without the daemon, and the list was
  short. It gives the rule and one example of each kind.
- The page on changing monitors opened with "nothing to configure" and then
  named display_index_preferences.
- COMPARE said KWin waits 300 ms for the pointer to settle before it moves the
  focus. That is KWin's default (DelayFocusInterval, in KWin 6.8.1), and
  komorebi sets it to 0 while focus follows mouse is on; the page now says so.
- ARCHITECTURE's opening list of what the page describes stopped at the borders
  and had no word of animation, themes, wallpapers, transparency, focus follows
  mouse or key bindings. Two link definitions that it had twice are there once.
- PLATFORM's list of what was not measured has the progress of the stock
  animation effect against the clock: one capture showed none four seconds into
  a 30 s animation, where the curve gives about 60 %.
- README, docs/index.md and ARCHITECTURE link COMPARE.md.
- COMPARE's list of the methods of Backend has drag_end, which the end of a
  resize is read through, and compare-gen.py has the row and the counts.
2026-10-28 07:39:49 -07:00
LGUG2Z 8b9cde08d0 build(nix): Plasma guests to develop against
The VM tests boot a machine, run a scenario and power it off. Trying a change by
hand needs a Plasma session that stays up, and on a Mac, one that renders on its
GPU.

nix/dev/plasma-vm.nix is a guest for QEMU on an Apple silicon Mac, headless or
with a Plasma session that logs in by itself, and nix/dev/plasma-utm.nix is that
session as a disk image for UTM, whose QEMU renders on the Mac's GPU. They are
nixosConfigurations of the flake, and install komorebi with services.komorebi
from the flake's packages. They carry no key, password or configuration of
anyone's: the console logs in by itself, ssh takes the keys a user puts in the
guest, and the RDP password is a file in the guest. plasma-guests-eval evaluates
the three on every system.

nix/dev/krdp.nix runs KRDP in the session, and the slow suite vm-rdp runs the
same unit in the session node: it waits for a password and listens once it has
one, FreeRDP's client is let in with the password and not with another, and the
picture the client gets on an X server of its own is the display. FreeRDP
decodes KRDP's H.264 with OpenH264 and shows nothing, so the guests' client is
built without it and decodes with FFmpeg. Keys over RDP stay unverified, since
no client can script them against KRDP; PLATFORM.md says so and what the suite
checks.

docs/dev/utm.md says how to build and run the guests, and what a builder has to
be.
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 bab8f35234 ci(github): a weekly canary against the tip of nixpkgs
komorebi follows the tip of nixpkgs-unstable, so KWin and Plasma move under it
each time flake.lock does, and the pull request that moves the lock runs the
slow VM tier only when it is labeled. The `canary` job of ci.yaml, on the weekly
schedule and on a manual run, copies the checkout, runs `nix flake update
nixpkgs` in the copy and builds every check against the result: the checks that
need no virtual machine, listed as the lint job lists them, then the suites of
both tiers, listed from `vmTiers` as the discover job does. They run one after
the other on one KVM runner, so that komorebi and the guests are built once
against the new nixpkgs; the job may take the six hours a hosted runner allows.

When a check fails, the job opens an issue titled "Canary: a check fails against
the tip of nixpkgs-unstable" with the failed checks, the nixpkgs revision, and
the KWin version at the tip and in flake.lock, or comments on that issue if it
is open. The same report goes to the job summary on every run. The job's token
can read the contents and write issues, nothing more, and the checkout keeps no
credentials, so the repository is never changed.

The composite action takes `cache: "false"`, which the canary passes. The store
cache is keyed on flake.lock: what it restored would be the old nixpkgs, taking
disk the guests need, and what it saved would replace the store of the jobs that
do use the lock.

`just lint` runs actionlint, with shellcheck over the scripts, both from the
nixpkgs flake.lock pins, so the lint job and `just check` check the workflows.

The steps were run as written on an x86_64 machine with KVM, against a fake gh:
GitHub Actions has not run the job yet. CONTRIBUTING.md, docs/dev/vm-tests.md
and DESIGN.md say what it does and why.
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 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 22c14ba5be feat(kwin): a border color for each kind of window
The stock KDE Rounded Corners has one color for the active window and one for
the others, and komorebi colors a window by what it is to the layout: the
focused window, the others, a locked container that does not have the focus, a
floating window that has it, a stack, a monocle. komorebi's build of the effect
takes a color for each window over D-Bus. After every event and message the
border manager works out the kind of each window and tells the backend, which
sends the effect what changed, and tells it that no window has a color when
borders are off. Without that build the borders are the look's two colors, and
the backend says so once.

komorebic gets border-colour. The fake desktop keeps the colors it was told, and
gets window_commands, which leaves the look out of the list of what the window
manager asked for. vm-features reads the colors of the kinds of window in a real
session.

The colors of a stack and of a monocled window are read from the fake and, in
vm-features, from the pixels. The border_colours key of komorebi.json is not
here: its colors are written as the Colour of komorebi-themes, which the themes
(49) bring, and 49 adds the key.

The maximize check of vm-features, which was the whole suite, reads a pixel a
quarter of the way into the window and not the middle one: komorebi puts the
pointer in the middle of the window it focuses, and a screenshot has the arrow
on it, white. It read the arrow on most runs once the border effect was loaded.

vm-features waits for the panel's room too, where it starts the daemon for its
sections B to E: the border colors of section C are read from the pixels around
a window, which the panel can cover.
2026-10-28 07:20:49 -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 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
LGUG2Z 6f657e53e3 feat(cli): komorebic
The daemon answers on its socket, but a person has no way to write a message on
it. komorebic is that way: each subcommand that asks something of the daemon is
one message, sent with komorebi-client, and a question's answer is printed.
komorebic is a separate program because the daemon takes no input but its
socket, so a key binding, a script and a terminal all drive it the same way.

It has what the daemon can answer so far: state, monitor-information, query,
retile and stop. Around them, start runs the komorebi on the PATH in the
background and passes it what follows a --, such as --fake-backend to try it
without a compositor; log follows the daemon's log, which the daemon now writes
to a file a day in LOG_DIR as well as to stdout; data-directory shows where the
socket is; completions prints a shell completion script. The hidden docgen
writes each subcommand's help to docs/cli, in fences that name their language
and wrapped at 100 columns whatever the width of the terminal, so that the pages
are the same wherever they are generated; just docgen runs it.

The user guide starts here: docs/index.md for KDE Plasma, and a page on running
komorebi. tests/cli.rs runs each subcommand against a socket the test serves in
place of the daemon. In the VM tests the guests get komorebic next to the
daemon, and vm-core-e2e reads the daemon's state with it (the windows it
manages, the one it focuses, after KWin restarts too) and stops it with
komorebic stop, which unloads the bridge and removes the socket.

In the VM test library, Kde.state asks again, a second (times the scale) after
an empty answer or a failed command, up to 10 times. The daemon drops a query
when it cannot take hold of the window manager within a second, and komorebic
then prints an empty line and succeeds; komorebic itself gives the daemon a
second to answer and fails when that is not enough. A daemon busy with the first
windows does either on a machine that is loaded or slow, and a suite that polled
the state died on JSONDecodeError. An answer that is not JSON still fails at
once.
2026-10-28 07:20:37 -07:00
LGUG2Z 15ec3c1274 feat(kwin): read the desktop through a script in KWin
KWin has no protocol that lets another process read or set the geometry of a
window, but its scripting API has both, and a signal for every change the window
manager follows. So the KWin backend is in two parts: bridge.js, a script the
daemon loads into KWin, which reports what KWin sees and holds no window manager
logic, and a D-Bus service in the daemon, org.komorebi.Bridge, which the script
calls with every event as a JSON string.

Every decision about what an event means is made by the table in kwin::state,
which is pure and is unit-tested with the JSON the script sends: which windows
may be managed, the ids komorebi gives KWin's windows, and the Show, Destroy,
FocusChange, Minimize and Restore events of the window manager. Events reach the
window manager in the order KWin raised them: the Event interface handles its
calls inline (zbus reordered them otherwise), and one channel and one thread,
window_manager_event_listener and listen_for_events, carry them on.

Commands go to the script as the answer to a Poll call that the script always
has open and the daemon holds until it has something to send: in the spikes this
was as fast as any other channel, and it makes one call every 10 seconds when
nothing happens. Here the only command is the cursor query. Every command of the
Backend trait is refused with "the KWin backend only reads the desktop", so the
daemon follows KWin and moves nothing.

The bridge is JavaScript that KWin runs as it is, and it gets types and tests
without a JavaScript build chain. tsc checks it as strict TypeScript through
JSDoc types of every message, against kwin-api.d.ts, a declaration of the part
of KWin's scripting API it uses, in which every property is read-only and
callDBus takes one string. bridge.test.js runs it under node --test against a
fake KWin in a node:vm context. The messages of a session are JSON fixtures that
both sides' tests read: the bridge's tests check that it sends those events, and
the table's and the backend's tests read the same events and commands, so a
message cannot change on one side alone. just check-js and the flake's
komorebi-bridge-js check run both, with nodejs and typescript from nixpkgs;
there is no package.json.

The daemon now connects to KWin by default, and --fake-backend is hidden. The VM
suite vm-kwin-events starts it against a headless KWin and checks, from its
debug log, that the script loads and reports what was open, that KWin's signals
arrive as events and in order, and that stopping the daemon unloads the script.
docs/dev/PLATFORM.md starts with what komorebi needs from Plasma.

zbus 5's zvariant, zbus_names and zvariant_derive are flagged unmaintained,
since their code moved into zbus 6; deny.toml ignores the three advisories.
2026-10-27 14:46:35 -07:00
LGUG2Z 0725705298 ci(github): run the VM tests on KVM
`discover` asks the flake for the suites of each tier
(`legacyPackages.x86_64-linux.vmTiers`) and hands them to two matrices, so a new
directory under `nix/tests/vm/suites` is run without an edit to the workflow.
`vm-fast` runs the fast tier on every pull request, one machine per suite, as a
sandboxed `nix build` of the check. `vm-slow` runs on master, every week, and on
a pull request labeled `ci:vm-slow`; it builds the driver and runs it by hand,
outside the sandbox, so that the screenshots and journals of a failed run are
uploaded with `if: always()`.

A composite action installs nix with the store cached and, for the VM jobs,
frees disk space (a Plasma guest is a closure of about 10 GB), writes the udev
rule that makes /dev/kvm writable for the runner's user, checks it with `test
-w`, and adds `kvm` to nix's system features. KVM on hosted runners is
documented for public repositories only. Whether this private repository has it
is not known: the first run of these jobs says, and if it does not, they need a
different runner.

`lint` no longer runs `nix flake check`: on Linux that now boots every VM suite,
which a runner with no KVM cannot do. It builds the checks that need no virtual
machine, and `vm-lib`, the library's own tests.

CONTRIBUTING.md and docs/dev/vm-tests.md say what runs where.
2026-10-27 14:06:52 -07:00
LGUG2Z 3c51b9b4bf build(nix): a NixOS test framework for headless KWin
The unit tests run against a fake desktop and cannot say whether KWin does what
the daemon asks. This adds the other half: NixOS machines with Plasma in them, a
real KWin, and a Python library to drive it.

Every directory under `nix/tests/vm/suites` is a test. The generator lists the
directory, so a new suite appears as `checks.<system>.vm-NAME` on Linux and as
`legacyPackages.<system>.vm.NAME` everywhere, with no edit to the flake. A suite
names its tier and its node and has a `test.py`;
`legacyPackages.<system>.vmTiers` lists the names of each tier as JSON for the
CI matrix, `vmGuests` the guests to build on a Linux builder, and `vmTcg` the
same suites for a machine with no KVM. `scale` multiplies every wait: 1 for KVM,
1.5 for HVF, 8 for TCG.

The headless node runs `kwin_wayland --virtual` as user units of a lingering
user, one pair of units per session (`session-bus@NAME`, `kwin-virtual@NAME`),
so a test can run several sessions with their own bus and socket. A virtual KWin
has no output to draw on, so anything that needs pixels or a pointer needs the
session node.

The library, `komorebi_vm`, has `Checks` (counted checks that fail once, at the
end) and `Kde` (start a session, run clients and tools inside it, read windows
through kdotool, poll with `wait_for`). It depends on a small `Machine` protocol
rather than on the test driver. The `vm-lib` check runs its unit tests against a
fake machine, with ruff and ty. The driver type checks each `test.py` with ty,
which is how a protocol that did not match `QemuMachine.execute` was caught.

`vm-kwin` starts a KWin with two outputs, spawns a foot window, reads it back,
starts a second session and checks that it cannot see the first.

On a Mac the guest is built on a Linux builder and the driver run directly,
because the macOS nix sandbox cannot give QEMU the hypervisor: `scripts/vm-mac`,
`just vm NAME`. docs/dev/vm-tests.md covers this, adding a test, and the
builder.
2026-10-27 14:06:19 -07:00
LGUG2Z 3a20a60823 ci(github): check with nix on every push
`ci.yaml` runs two jobs on GitHub-hosted runners, gated on the repository name
so that forks do not run them. `cargo-deny` checks licenses, advisories and
sources. `lint` runs `nix flake check`, which builds every check of the flake,
and the repository's own macro rules from the justfile. The triggers are the
ones komorebi for Mac uses: pull requests, pushes to master, feature and hotfix
branches, version tags, a weekly schedule and a manual run. The Nix store is
cached with cache-nix-action, which needs no account.

komorebi follows the tip of nixpkgs-unstable, so `flake.lock` moves, and KWin
with it. `update-flake-lock.yaml` opens a pull request each week with the title
`chore(deps): flake.lock: Update`; the same jobs run on it, which is what says
whether the new KWin still works.

Dependabot keeps the actions and the crates up to date, and the pull request
template carries over from komorebi for Mac. CONTRIBUTING.md describes what CI
runs.
2026-10-27 09:18:55 -07:00
LGUG2Z 9524b777b3 build(nix): build and check on Linux and macOS
The flake builds the workspace with crane and checks it the way `just check`
does, so that CI and a laptop run the same things: `komorebi-workspace-fmt`
(nightly rustfmt, since rustfmt.toml uses unstable options), `-clippy` with
warnings denied, `-doc` with rustdoc warnings denied, `-toml-fmt`, `-deny` and
`-nextest`. Treefmt and a pre-commit hook format Rust, Nix, TOML and the
justfile. `nix develop` gives a toolchain with the nightly rustfmt, cargo-deny,
nextest and udeps.

Linux is where komorebi runs, so only Linux gets packages and the overlay. An
Apple silicon Mac gets the dev shell, the formatter and the checks; the tests
there run against the fake desktop that arrives with the backend trait.
x86_64-darwin is left out because nixpkgs 26.11 dropped it, and the flake
follows the tip of nixpkgs-unstable.

The commit hash is a constant (`nix-check`) for the dependencies and for every
check, and the real one only goes into the final build. The version crate reads
it with `env!`, so putting it in the shared arguments would rebuild the
dependencies and rerun every check for each commit.

The README and CONTRIBUTING.md describe `nix develop` and `nix flake check`.
2026-10-27 09:17:51 -07:00
LGUG2Z 426409fcfc chore(repo): a workspace of two crates
komorebi for KDE is built up as a Cargo workspace with one crate per concern,
the way komorebi and komorebi for Mac are. The first two crates are the
`komorebi` daemon, which does nothing yet but report its version, and `version`,
which owns the string every binary of the workspace reports.

`version` reads the commit from git when HEAD is on a branch, and from
COMMIT_HASH otherwise. A jj workspace leaves HEAD detached, so
`.cargo/config.toml` defaults the variable to `unknown` (nix will set the real
commit), and a plain `cargo build` in a jj checkout compiles.

The tooling files come from komorebi for Mac: the licence, deny.toml (trimmed to
the licences in use), rustfmt.toml and the toolchain file. `just check` runs the
formatting check, clippy and rustdoc with warnings denied, and the tests.
rustfmt.toml uses an unstable option, so formatting needs a nightly rustfmt. The
`rg` calls in `just lint` name a path because rg reads stdin when it is given
none and stdin is not a terminal.

The README and CONTRIBUTING.md say how to run, check and commit.
2026-10-27 09:11:36 -07:00