Commit Graph
9 Commits
Author SHA1 Message Date
LGUG2Z 1ee2405e44 docs(repo): trying komorebi in a virtual machine
The guide for the one command: docs/trying-it.md says what a machine needs to
run the demo guest, with the lines of .wslconfig and of the NixOS-WSL
configuration for Windows 11, how to check it with `run-kde --doctor`, what the
command does and shows, the key bindings to try, how the GPU path is chosen, how
to open Windows' own RDP client, how long the first run takes (measured on a
machine with 32 cores and an empty store, by number of cores, with an estimate
for a WSL 2 machine and its caveats), how to start again, how to work on
komorebi in the guest, and what to do when something is wrong, by symptom. It
ends with what was run and what was not: nothing has been run on WSL 2 yet. The
README has a section and a link, and the user guide index a line.

DESIGN.md has four entries. Two record what I decided and want: the QEMU window
first and RDP second, and the GPU wherever the host has one. Two say that a
reason was not written down, for QEMU with KVM and not Hyper-V and for sharing
the password by a directory, and what is known that bears on each; the password
entry also says why a password is made for each user. PLATFORM.md has a section
for WSL 2 with what the command expects, a table of the probe results still to
be recorded (KVM, display, GPU, RDP, resources, virtiofs), and what is known of
virtiofsd without WSL 2.
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 9b60a0020d chore(scripts): probe a WSL2 NixOS VM for KVM, WSLg and RDP
The port can only be tried on a Windows machine, through a NixOS virtual machine
in WSL2, which the machines this history is built on do not have. What such a
machine can do for a Plasma guest is not known until something runs there, and
every guess about it is a risk for what comes next. wsl2-probe is that
something: one command, nix run .#wsl2-probe, that answers in about two minutes
what the guest needs to know and prints, for each answer that is not a yes, the
exact change: the .wslconfig lines, the NixOS option, the PowerShell command.

It checks the environment (WSL2, memory, disk, .wslconfig, Nix); KVM (/dev/kvm,
its group, and a tiny guest booted under KVM and under TCG, with the same loop
timed in both and on the machine itself, which must differ by several times);
the display (a QEMU GTK window through WSLg for a few seconds, and whether you
saw it); the GPU (/dev/dxg, the Windows driver libraries, the OpenGL renderer in
four environments, and virtio-vga-gl with gl=on); the Windows side (interop,
mstsc.exe, whether Windows reaches a port that WSL listens on); whether memory,
cores and disk are enough for the real guest; and virtiofs, which the real guest
uses for its /nix/store and every shared directory (/dev/fuse, virtiofsd as the
user with the options of nixpkgs' runner, and a guest that mounts the host's
/nix/store and a writable directory, reads, writes and compares). Each probe
runs apart from the others, so one that fails leaves the rest alone, and the
output ends in a block to paste back, which is also written to
wsl2-probe-report.txt.

The flake wraps the script with QEMU, virtiofsd, Mesa's eglinfo and glxinfo and
socat from nixpkgs, and builds the guest, a kernel from the binary cache, the
two modules of it that virtiofs needs and a static busybox initrd, as
wsl2-probe-guest, so nothing is compiled on the machine. Two checks keep the
script from rotting: wsl2-probe builds the wrapper, which runs ShellCheck over
the script and over the guest's init, and wsl2-probe-dry-run runs --dry-run in
the build sandbox, which has no KVM, no network and no display.
docs/dev/wsl2-probe.md says what each check means and how to read it.
2026-10-28 13:27:11 -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 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 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 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 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