From 041dd3522435ea01899c81fc16184a0dcdd54d72 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Wed, 28 Oct 2026 12:27:23 -0700 Subject: [PATCH] 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. --- CONTRIBUTING.md | 6 +- README.md | 6 +- docs/dev/PLATFORM.md | 12 +- docs/dev/utm.md | 68 +++++++- flake.nix | 82 +++++---- nix/dev/krdp.nix | 40 ++++- nix/dev/plasma-vm.nix | 18 +- nix/dev/run-kde.nix | 384 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 569 insertions(+), 47 deletions(-) create mode 100644 nix/dev/run-kde.nix diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9e74c65..98a7f226 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -144,12 +144,14 @@ nix build .#checks.x86_64-linux.kwin-effect-geometry-change-komorebi-install [`nix/dev`](nix/dev) has NixOS guests with Plasma for trying a change by hand, on an Apple silicon Mac (two that QEMU runs and a disk image for UTM) and on a Linux machine with KVM (the same two QEMU guests, for `x86_64` and for -`aarch64`). [`docs/dev/utm.md`](docs/dev/utm.md) says how to build and run them, +`aarch64`, and a demo guest, [`nix/dev/run-kde.nix`](nix/dev/run-kde.nix)). +[`docs/dev/utm.md`](docs/dev/utm.md) says how to build and run them, and what a builder has to be for a Mac. They install komorebi with the NixOS module, and carry nothing of the person who uses them: a key, a password or a configuration of yours goes into the guest when it runs, never into these files. The `plasma-guests-eval` check evaluates them, and the VM suite `rdp` runs their -RDP server, [`nix/dev/krdp.nix`](nix/dev/krdp.nix). +RDP server, [`nix/dev/krdp.nix`](nix/dev/krdp.nix). The demo guest has no +password and no key in its closure: the host shares a directory with them. ## Continuous integration diff --git a/README.md b/README.md index 7863231a..51b9e486 100644 --- a/README.md +++ b/README.md @@ -182,8 +182,10 @@ To try komorebi by hand in a real Plasma session, the flake has Plasma guests to develop in. For an Apple silicon Mac there are two that QEMU runs, one with a session that can be reached over RDP, and a disk image for UTM, which renders on the Mac's GPU. For a Linux machine with KVM there are the same two QEMU guests, -for `x86_64` and for `aarch64`. [`docs/dev/utm.md`](docs/dev/utm.md) says how to -build and run them; `plasma-guests-eval` evaluates them on every system. +for `x86_64` and for `aarch64`, and a demo guest of the session guest, which +shows komorebi tiling a few windows in the first minute. +[`docs/dev/utm.md`](docs/dev/utm.md) says how to build and run them; +`plasma-guests-eval` evaluates them on every system. ## How it fits together diff --git a/docs/dev/PLATFORM.md b/docs/dev/PLATFORM.md index 629b95d5..6863d723 100644 --- a/docs/dev/PLATFORM.md +++ b/docs/dev/PLATFORM.md @@ -806,6 +806,15 @@ At time of writing (2026-10-27): - KRDP 6.8.1, Plasma's RDP server, does not start without a TLS certificate and key, although its `--help` says it generates one; it logs the error and keeps running with nothing listening. `xrdp` is not an option, since there is no X11 session to start. +- A guest can have OpenGL through the host's GPU. At time of writing (2026-10-28), the demo guest + started under KVM with `-display gtk,gl=on -device virtio-vga-gl` on an X server (Xvfb) whose + OpenGL is Mesa's llvmpipe, with QEMU 11.1.2 and Mesa 26.2.5 from nixpkgs: KWin drew with the + driver `virgl`, and the guest's renderer string was `virgl (LLVMPIPE (LLVM 21.1.8, 256 bits))`. + The guest names its own driver whatever the host renders with, so it cannot tell a host GPU + from a host software renderer. QEMU's `screendump` answers "no surface" with `virtio-vga-gl`: the picture + has to be read from the window. `-display egl-headless` needs a DRM render node and stops at + startup without one ("egl: no drm render node available", on the machine of these runs, which has + none). A GPU that renders is not measured here. ### RDP into the session @@ -814,7 +823,8 @@ The session guests to develop in ([utm.md](utm.md)) run KRDP as a user unit of t screen and for input through its own Wayland protocols (KRDP's desktop file lists `zkde_screencast_unstable_v1` and `org_kde_kwin_fake_input`), so no portal asks the user first. The unit makes a self-signed certificate, and logs in the session's user with a password the user -writes to a file; without the file it does not start. At time of writing (2026-10-27), with KWin +writes to a file, or that a guest takes from the host (the demo guest); without a password file it +does not start. At time of writing (2026-10-27), with KWin and KRDP 6.8.1 and FreeRDP 3.32.2: - KRDP sends the screen as H.264 only, encoded with x264 in software when there is no VAAPI device diff --git a/docs/dev/utm.md b/docs/dev/utm.md index 6c4d0ae2..2bc0676e 100644 --- a/docs/dev/utm.md +++ b/docs/dev/utm.md @@ -12,6 +12,7 @@ own architecture: | `plasma-utm` | Apple silicon Mac | [UTM](https://mac.getutm.app), with a GPU | the session guest as a disk image that boots by UEFI | | `plasma-vm-x86_64`, `plasma-vm-session-x86_64` | Linux on x86_64 | QEMU from nixpkgs, with KVM | the first two, as `x86_64-linux` guests | | `plasma-vm-aarch64`, `plasma-vm-session-aarch64` | Linux on aarch64 | QEMU from nixpkgs, with KVM | the first two, as `aarch64-linux` guests | +| `run-kde-x86_64`, `run-kde-aarch64` | Linux on x86_64, on aarch64 | QEMU from nixpkgs, with KVM | the session guest with a demo on it, which `run-kde` starts ([below](#the-demo-guest)) | The guests of a Mac are `aarch64-linux`. A guest of a Linux host that has no KVM does not start: the runner refuses, because Plasma under software emulation is too slow to try anything. @@ -111,6 +112,61 @@ of an x86_64 guest has a VGA of its own, which would be a second display next to so the guest has `virtio-vga` (a virtio-gpu that is also a VGA) in its place and `-vga none`. The session guest shows nothing on the host, as on a Mac: it is reached over RDP ([below](#rdp)). +## The demo guest + +`run-kde-x86_64` and `run-kde-aarch64` are the session guest with a demo on it, made by +[`nix/dev/run-kde.nix`](../../nix/dev/run-kde.nix) on top of `plasma-vm.nix`. komorebi is +configured by the module's typed `config` (eight workspaces with a layout each, borders and +animation) and by the sample key bindings, `docs/skhdrc.sample`, in the module's default mode. At +login the guest opens two Konsole windows, Kate and Dolphin, which komorebi tiles, and a cheat sheet +that it leaves alone: the main key bindings, `komorebic` commands to try, what KWin draws with, and +the output of `komorebic check` for the configuration the daemon runs. The VM suite `run-kde` boots +the same module on the session node. + +The closure is the same for everyone. No password, key or configuration of a person is in it, and +what a person's run needs comes from the environment of the runner: + +| Variable | What the guest does with it | +|---|---| +| `KOMOREBI_REPO` | the directory mounted at `/repo` | +| `RUN_KDE_STATE` | the directory mounted at `/run/run-kde`, with `rdp-password` (the password for RDP) and `ssh_key.pub` (the public key that may log in over ssh) | +| `RUN_KDE_MEMORY` | the memory in MiB, which has to be the `-m` of the same run | +| `QEMU_NET_OPTS` | the port forwards, as `hostfwd=tcp:127.0.0.1:2223-:22,hostfwd=tcp:127.0.0.1:3389-:3389` | +| `QEMU_OPTS` | the display device: `-vga none -device virtio-vga,xres=1280,yres=800`, or `-display gtk,gl=on -vga none -device virtio-vga-gl,xres=1280,yres=800` for a GPU | +| `NIX_DISK_IMAGE` | the disk image, which the runner makes when it is not there | + +`run-kde` sets them; to start the guest by hand, on a Linux machine: + +```sh +nix build .#nixosConfigurations.run-kde-x86_64.config.system.build.vm -o .vm/result-run-kde +mkdir -m 700 .vm/state && cd .vm +ssh-keygen -q -t ed25519 -N '' -f state/ssh_key +printf %s "a password" > state/rdp-password +KOMOREBI_REPO=$PWD/.. RUN_KDE_STATE=$PWD/state RUN_KDE_MEMORY=6144 \ + QEMU_NET_OPTS=hostfwd=tcp:127.0.0.1:2223-:22,hostfwd=tcp:127.0.0.1:3389-:3389 \ + QEMU_OPTS='-cpu host -vga none -device virtio-vga,xres=1280,yres=800' \ + ./result-run-kde/bin/run-komorebi-demo-vm -m 6144 -smp 4 +``` + +The guest differs from `plasma-vm-session` in what the runner of nixpkgs would otherwise fix when +the closure is built: + +- The display device is not part of the guest (`komorebi.plasmaVm.display` is false). The same + closure and the same disk run with the software `virtio-vga` or with `virtio-vga-gl`, which + renders on the host's GPU through VirGL, as the runner's environment says. +- The ports are not forwarded by the guest, so that a person can choose them. +- The runner does not insist on KVM (`qemu.forceAccel` is false): `run-kde` checks for it and + lets `--tcg` start a guest without. +- The shared memory that virtiofs needs is a memory backend of the guest's memory size, which + nixpkgs fixes at `memorySize` in the command line of the runner. The guest makes its own, from + `RUN_KDE_MEMORY`, so that a run can have less memory than the closure's 8 GB. +- The directory `/run/run-kde` is mounted after the boot and not in the initrd, where a mount + under `/run` is hidden by the `/run` that the booted system mounts. +- The files in it can belong to a user of the host who is not the guest's user, and the guest + cannot read the directory then. The unit `run-kde-credentials` copies the two files as root, to + `/run/run-kde-guest`, where the RDP unit (`komorebi.krdp.passwordFile`) and sshd + (`AuthorizedKeysFile`) read them. + ## The UTM disk UTM boots `plasma-utm` from a disk image. The image is made by installing the system onto a blank @@ -215,8 +271,10 @@ At time of writing (2026-10-28), the guests of a Mac, made from these files, hav evaluates them. The `x86_64` pair for a Linux host was built and started under KVM on an `x86_64` machine. Both guests reached the login on their serial console and had one DRM device with a render node, and the session guest drew the Plasma desktop on its display (a screenshot taken through -QEMU's monitor) with komorebi running in the session. The `aarch64` pair for a Linux host was -evaluated and not built. The RDP server is the exception to all of this: the VM suite `rdp` runs the -same unit in the test session. That KWin 6.8 runs well on VirGL under UTM is not known either: the -UTM and Mesa notes say VirGL is the way to render a Linux desktop, and Plasma works on it for other -people, but it has not been measured here. +QEMU's monitor) with komorebi running in the session. The demo guest, `run-kde-x86_64`, was +started under KVM with a state directory: ssh took the key from it, the RDP unit started with the +password from it, komorebi tiled the four windows, and the cheat sheet opened. The `aarch64` guests +for a Linux host were evaluated and not built. The RDP server is the exception to all of this: the +VM suite `rdp` runs the same unit in the test session. That KWin 6.8 runs well on VirGL under UTM +is not known either: the UTM and Mesa notes say VirGL is the way to render a Linux desktop, and +Plasma works on it for other people, but it has not been measured here. diff --git a/flake.nix b/flake.nix index 60dd3552..f2384766 100644 --- a/flake.nix +++ b/flake.nix @@ -292,38 +292,37 @@ plasma-guests-eval = let guests = self.nixosConfigurations; - machines = { - plasma-vm = { - guest = "aarch64-linux"; - host = "aarch64-darwin"; + machines = + let + mac = { + guest = "aarch64-linux"; + host = "aarch64-darwin"; + }; + linux = system: { + guest = system; + host = system; + }; + # the runner of the demo is given its display device, its ports and whether to + # insist on KVM when it starts, by `run-kde` + demo = machine: machine // { demo = true; }; + in + { + plasma-vm = mac; + plasma-vm-session = mac; + plasma-vm-x86_64 = linux "x86_64-linux"; + plasma-vm-session-x86_64 = linux "x86_64-linux"; + plasma-vm-aarch64 = linux "aarch64-linux"; + plasma-vm-session-aarch64 = linux "aarch64-linux"; + run-kde-x86_64 = demo (linux "x86_64-linux"); + run-kde-aarch64 = demo (linux "aarch64-linux"); }; - plasma-vm-session = { - guest = "aarch64-linux"; - host = "aarch64-darwin"; - }; - plasma-vm-x86_64 = { - guest = "x86_64-linux"; - host = "x86_64-linux"; - }; - plasma-vm-session-x86_64 = { - guest = "x86_64-linux"; - host = "x86_64-linux"; - }; - plasma-vm-aarch64 = { - guest = "aarch64-linux"; - host = "aarch64-linux"; - }; - plasma-vm-session-aarch64 = { - guest = "aarch64-linux"; - host = "aarch64-linux"; - }; - }; isDisplay = option: lib.hasPrefix "-device virtio-vga" option || lib.hasPrefix "-device virtio-gpu" option; qemuGuest = name: let machine = machines.${name}; + demo = machine.demo or false; vm = guests.${name}.config.virtualisation.vmVariant.virtualisation; in assert lib.assertMsg ( @@ -332,15 +331,19 @@ assert lib.assertMsg ( vm.host.pkgs.stdenv.hostPlatform.system == machine.host ) "${name} does not run on a ${machine.host} host"; - assert lib.assertMsg (vm.qemu.forceAccel + assert lib.assertMsg ( + vm.qemu.forceAccel != demo ) "${name} would run without KVM or HVF, which is too slow to use"; assert lib.assertMsg ( - builtins.length (builtins.filter isDisplay vm.qemu.options) == 1 - ) "${name} does not have exactly one display device"; + builtins.length (builtins.filter isDisplay vm.qemu.options) == (if demo then 0 else 1) + ) "${name} does not have the display devices it should have"; assert lib.assertMsg # an x86_64 machine has a VGA of its own, which would be a second display - ((machine.guest == "x86_64-linux") == builtins.elem "-vga none" vm.qemu.options) - "${name} has `-vga none` if and only if it is an x86_64 guest"; + ((machine.guest == "x86_64-linux" && !demo) == builtins.elem "-vga none" vm.qemu.options) + "${name} has `-vga none` if and only if it is an x86_64 guest with a display device"; + assert lib.assertMsg ( + demo -> (vm.forwardPorts == [ ] && vm.sharedDirectories.run-kde.target == "/run/run-kde") + ) "${name} does not leave its ports and its shared directory to the runner's environment"; { ${name} = guests.${name}.config.virtualisation.vmVariant.system.build.toplevel; "${name}, its runner" = guests.${name}.config.system.build.vm; @@ -508,7 +511,8 @@ # Plasma guests to develop against (docs/dev/utm.md). For an Apple silicon Mac, QEMU runs # the first two under HVF, and UTM boots the third from a disk image. For a Linux machine # with KVM, QEMU runs the other four: the same two guests for an x86_64 host and for an - # aarch64 one, each of which runs the guest of its own architecture. + # aarch64 one, each of which runs the guest of its own architecture. The last two are the + # demo that `run-kde` starts. nixosConfigurations = let # `guest` is the system of the guest, and `host` the pkgs of the machine that runs QEMU @@ -537,6 +541,19 @@ ./nix/dev/plasma-vm.nix { komorebi.plasmaVm.session = true; } ]; + # the session guest with the demo of `run-kde` (nix/dev/run-kde.nix) and no display + # device, which the command gives it when it starts the guest + demo = [ + ./nix/dev/plasma-vm.nix + { + komorebi.plasmaVm = { + session = true; + display = false; + }; + } + ./nix/dev/run-kde.nix + ({ lib, ... }: { networking.hostName = lib.mkOverride 40 "komorebi-demo"; }) + ]; in { plasma-vm = onMac [ ./nix/dev/plasma-vm.nix ]; @@ -547,6 +564,9 @@ plasma-vm-session-x86_64 = onLinux "x86_64-linux" session; plasma-vm-aarch64 = onLinux "aarch64-linux" [ ./nix/dev/plasma-vm.nix ]; plasma-vm-session-aarch64 = onLinux "aarch64-linux" session; + + run-kde-x86_64 = onLinux "x86_64-linux" demo; + run-kde-aarch64 = onLinux "aarch64-linux" demo; }; }; }; diff --git a/nix/dev/krdp.nix b/nix/dev/krdp.nix index 62e7ff89..a7a21300 100644 --- a/nix/dev/krdp.nix +++ b/nix/dev/krdp.nix @@ -4,6 +4,8 @@ # # The password is not in the repository: the unit starts once the user has written one to # ~/.config/komorebi-vm/rdp-password, and a client logs in with the user's name and that password. +# A guest whose password comes from outside, as the demo guest's does (run-kde.nix), names a file +# of the guest in `passwordFile`, which the unit reads in place of that one when it is there. { config, lib, @@ -11,9 +13,18 @@ ... }: let + cfg = config.komorebi.krdp; + startKrdp = pkgs.writeShellScript "krdp-start" '' set -eu dir=$HOME/.config/komorebi-vm + password_file=$dir/rdp-password + ${lib.optionalString (cfg.passwordFile != null) '' + if [ -r ${lib.escapeShellArg cfg.passwordFile} ]; then + password_file=${lib.escapeShellArg cfg.passwordFile} + fi + ''} + mkdir -p "$dir" # krdpserver refuses to run without a TLS certificate and key (its --help promises a temporary # one, but it only checks that the files exist), so make a throwaway self-signed pair if [ ! -f "$dir/rdp.crt" ] || [ ! -f "$dir/rdp.key" ]; then @@ -24,13 +35,27 @@ let # --plasma asks KWin for the screen and for input directly, with no portal prompt to click exec ${pkgs.kdePackages.krdp}/bin/krdpserver --plasma --address 0.0.0.0 --port 3389 \ --certificate "$dir/rdp.crt" --certificate-key "$dir/rdp.key" \ - --username "$USER" --password "$(cat "$dir/rdp-password")" + --username "$USER" --password "$(cat "$password_file")" ''; in { - options.komorebi.krdp.enable = lib.mkEnableOption "KRDP, an RDP server, in every Plasma session"; + options.komorebi.krdp = { + enable = lib.mkEnableOption "KRDP, an RDP server, in every Plasma session"; - config = lib.mkIf config.komorebi.krdp.enable { + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/run/run-kde-guest/rdp-password"; + description = '' + The path, in the guest, of a file with the password a client logs in with. It is a path in + the running guest and not a file of the Nix store, so that no password is part of the + system. The unit reads it in place of `~/.config/komorebi-vm/rdp-password` when it exists, + and starts when either exists. + ''; + }; + }; + + config = lib.mkIf cfg.enable { environment.systemPackages = [ pkgs.kdePackages.krdp # A client, to try the server from inside the guest. FreeRDP decodes H.264 with OpenH264 @@ -54,7 +79,14 @@ in after = [ "plasma-core.target" ]; # with no password the unit is skipped, not failed: `systemctl --user start krdp` once the # file is there - unitConfig.ConditionPathExists = "%h/.config/komorebi-vm/rdp-password"; + unitConfig.ConditionPathExists = + if cfg.passwordFile == null then + "%h/.config/komorebi-vm/rdp-password" + else + [ + "|%h/.config/komorebi-vm/rdp-password" + "|${cfg.passwordFile}" + ]; serviceConfig = { ExecStart = startKrdp; Restart = "on-failure"; diff --git a/nix/dev/plasma-vm.nix b/nix/dev/plasma-vm.nix index d9b8add7..83a8995e 100644 --- a/nix/dev/plasma-vm.nix +++ b/nix/dev/plasma-vm.nix @@ -49,6 +49,18 @@ in a full autologin Plasma Wayland session (sddm + plasmashell), in addition to the headless `kwin_wayland --virtual` workflow that is always available from the console and over ssh ''; + + display = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether the runner gives the guest its display device, a virtio-gpu. A guest that sets this + to false is started with a display device of its own choosing, which can then differ from + one start to the next: `run-kde` starts the demo guest (run-kde.nix) with a software + `virtio-vga` or with a `virtio-vga-gl` that renders on the host's GPU, from one closure and + one disk. + ''; + }; }; config = lib.mkMerge [ @@ -146,7 +158,7 @@ in diskSize = 20 * 1024; graphics = false; # no display, but a gpu so there is a drm render node - qemu.options = lib.mkIf (!cfg.session) (gpu ""); + qemu.options = lib.mkIf (cfg.display && !cfg.session) (gpu ""); forwardPorts = [ { from = "host"; @@ -182,7 +194,9 @@ in # kwin drives virtio-gpu through drm on the primary node; that is the only place mesa's software # rasterizer can allocate buffers (see docs/dev/PLATFORM.md: the headless --virtual backend # cannot) - virtualisation.vmVariant.virtualisation.qemu.options = gpu ",xres=1920,yres=1080"; + virtualisation.vmVariant.virtualisation.qemu.options = lib.mkIf cfg.display ( + gpu ",xres=1920,yres=1080" + ); # an idle lock screen or dimmed display changes how kwin animates and what screenshots show environment.etc."xdg/kscreenlockerrc".text = '' [Daemon] diff --git a/nix/dev/run-kde.nix b/nix/dev/run-kde.nix new file mode 100644 index 00000000..2fbb0e12 --- /dev/null +++ b/nix/dev/run-kde.nix @@ -0,0 +1,384 @@ +# The demo guest of `run-kde`: a Plasma session in which komorebi tiles a few windows that open by +# themselves, with a cheat sheet to start from. It is a module to put on the session guest +# (plasma-vm.nix with `session` and not `display`: the flake's `run-kde-x86_64` and +# `run-kde-aarch64`), and the VM suite `run-kde` puts it on the session node, so that what the +# command starts is what CI boots. +# +# The closure is the same for everyone: no password, no key and no configuration of a person is in +# it. The host shares a directory, which the guest mounts at /run/run-kde, with +# +# rdp-password the password a client logs in to RDP with +# ssh_key.pub the public key that may log in over ssh +# +# and the guest takes them at boot (`run-kde-credentials`). What a person's own run needs at start +# up, the memory, the ports, the display device and the repository, comes from the environment of +# the runner, which `run-kde` sets (docs/dev/utm.md). The guest shows a window through QEMU or is +# reached over RDP, and the terminal of whoever starts it is its console. +{ + config, + lib, + pkgs, + ... +}: +let + # where the host's directory is mounted, and where the guest keeps what it takes from it + shared = "/run/run-kde"; + taken = "/run/run-kde-guest"; + + user = "komorebi"; + + cheatSheet = pkgs.writeText "run-kde-cheat-sheet.txt" '' + komorebi for KDE tiles the windows of this Plasma session. + + The windows that opened by themselves (two terminals, Kate and Dolphin) share + the screen. Open another one and it takes a tile of its own; close one and the + others grow. This window is not tiled: komorebi leaves it alone. Press q to + close it, and run `run-kde-cheat-sheet` in a terminal to open it again. + + Start with these. Alt is either Alt key. + + alt + h, j, k, l focus the window to the left, below, above, right + alt + shift + h, j, k, l move the window that way + alt + shift + return promote the window to the biggest tile + alt + t take the window out of the tiles and back in + alt + shift + f toggle monocle: the window fills the screen + alt + 1 ... 9 show workspace 1 ... 9, each with its own layout + alt + shift + 1 ... 9 move the window to that workspace + alt + x, alt + y flip the layout horizontally, vertically + + Or type the same in a terminal: `komorebic` is the program that every key + runs. + + komorebic state everything komorebi knows, as JSON + komorebic change-layout columns bsp, rows, grid, scrolling, ... + komorebic toggle-monocle + komorebic retile put every window back in its tile + komorebic border off borders are drawn by a KWin effect + komorebic --help + + komorebic reads its configuration from ~/.config/komorebi unless it is told + where, and this machine starts komorebi with a file of its own, so tell it: + + KOMOREBI_CONFIG_HOME=/etc/run-kde/config-home komorebic check + + The key bindings komorebi reads are the whole of the file below, written in + skhd's format. The repository is mounted at /repo: edit komorebi there and + build it in this machine with `cargo`. + + ${builtins.readFile ../../docs/skhdrc.sample} + ''; + + # the file the module starts the daemon with, which is the `--config` of its command line, and + # the directory `komorebic check` reads to see what the daemon runs: it looks in the config + # home, not at the daemon's command line + configHome = + let + words = lib.splitString " " config.services.komorebi.execStart; + at = lib.lists.findFirstIndex (word: word == "--config") null words; + in + pkgs.linkFarm "run-kde-config-home" [ + { + name = "komorebi.json"; + path = builtins.elemAt words (at + 1); + } + { + name = "skhdrc"; + path = ../../docs/skhdrc.sample; + } + ]; + + # What the window shows: the graphics line, which the guest looks up when it has started, the + # text above, and what `komorebic check` finds now. + cheatSheetCommand = pkgs.writeShellApplication { + name = "run-kde-cheat-sheet"; + runtimeInputs = [ + pkgs.coreutils + pkgs.less + config.services.komorebi.package + ]; + text = '' + text=$(mktemp) + { + printf 'Graphics: %s\n\n' "$(cat "''${XDG_RUNTIME_DIR:-/tmp}/run-kde-renderer" 2>/dev/null || echo "not known yet")" + cat ${cheatSheet} + printf '\nkomorebic check\n\n' + NO_COLOR=1 KOMOREBI_CONFIG_HOME=${configHome} komorebic check 2>&1 || true + } > "$text" + exec less -R "$text" + ''; + }; + + # The windows of the demo, opened once the shell has its panel: a window that is tiled before + # komorebi knows what room the panel takes is tiled under it, and nothing tiles it again. + demo = pkgs.writeShellApplication { + name = "run-kde-demo"; + runtimeInputs = [ + pkgs.coreutils + pkgs.jq + config.services.komorebi.package + ]; + text = '' + panel=0 + for _ in $(seq 1 120); do + if komorebic state 2>/dev/null | + jq -e '.monitors.elements[0] | .work_area_size != .size' > /dev/null 2>&1; then + panel=1 + break + fi + sleep 1 + done + [ "$panel" = 1 ] || echo "komorebi does not know the panel's room: the windows may be tiled under it" + + tiled() { + local n + n=$(komorebic state 2>/dev/null | + jq '[.monitors.elements[].workspaces.elements[] | .containers.elements[].windows.elements[]] | length' 2> /dev/null || true) + echo "''${n:-0}" + } + + # starts a program in a scope of its own, which outlives this script, and waits until komorebi + # has a tile for it + open() { + local before + before=$(tiled) + systemd-run --user --scope --quiet -- "$@" & + for _ in $(seq 1 60); do + [ "$(tiled)" -gt "$before" ] && return 0 + sleep 1 + done + echo "no tile for $1 after a minute" + } + + dir=$HOME + [ -d /repo ] && dir=/repo + + open konsole --workdir "$dir" + open konsole + open kate + open dolphin "$dir" + + # the cheat sheet says what KWin draws with, which the guest looks up once KWin has started + for _ in $(seq 1 60); do + [ -s "$XDG_RUNTIME_DIR/run-kde-renderer" ] && break + sleep 1 + done + + systemd-run --user --scope --quiet -- foot --app-id komorebi-cheat-sheet \ + --title "komorebi cheat sheet" --window-size-chars 100x34 -o main.font=monospace:size=10 \ + run-kde-cheat-sheet & + sleep 1 + ''; + }; + + # What KWin draws with, which is what the cheat sheet and the console say: "GPU" when the host's + # GPU renders for it (virgl) and "software" when the guest's CPUs do (llvmpipe). + report = pkgs.writeShellApplication { + name = "run-kde-report"; + runtimeInputs = [ + pkgs.coreutils + pkgs.gnused + pkgs.kdePackages.qttools + ]; + text = '' + export XDG_RUNTIME_DIR=/run/user/${toString config.users.users.${user}.uid} + export DBUS_SESSION_BUS_ADDRESS=unix:path=$XDG_RUNTIME_DIR/bus + renderer= + for _ in $(seq 1 300); do + renderer=$(qdbus org.kde.KWin /KWin supportInformation 2> /dev/null | + sed -n 's/^OpenGL renderer string: //p' | head -n 1 || true) + [ -n "$renderer" ] && break + sleep 1 + done + [ -n "$renderer" ] || exit 0 + case $renderer in + *llvmpipe* | *softpipe* | *swrast*) kind=software ;; + *) kind=GPU ;; + esac + line="$kind (''${renderer%% *}): $renderer" + printf '%s\n' "$line" > "$XDG_RUNTIME_DIR/run-kde-renderer" + echo "run-kde: KWin draws with $line" + ''; + }; +in +{ + imports = [ ./krdp.nix ]; + + komorebi.krdp = { + enable = true; + passwordFile = "${taken}/rdp-password"; + }; + + services.komorebi = { + config = { + border = true; + border_width = 6; + border_offset = 5; + default_workspace_padding = 15; + default_container_padding = 15; + animation = { + enabled = true; + style = "EaseOutExpo"; + duration = 250; + }; + # this window is the guide to the others, and is not one of them + ignore_rules = [ + { + kind = "Class"; + id = "komorebi-cheat-sheet"; + matching_strategy = "Equals"; + } + ]; + monitors = [ + { + workspaces = [ + { + name = "I"; + layout = "BSP"; + } + { + name = "II"; + layout = "VerticalStack"; + } + { + name = "III"; + layout = "HorizontalStack"; + } + { + name = "IV"; + layout = "Columns"; + } + { + name = "V"; + layout = "Rows"; + } + { + name = "VI"; + layout = "Grid"; + } + { + name = "VII"; + layout = "RightMainVerticalStack"; + } + { + name = "VIII"; + layout = "Scrolling"; + } + ]; + } + ]; + }; + + # the module's default mode, with the sample bindings of docs/skhdrc.sample + keybindings = { + format = "skhd"; + file = ../../docs/skhdrc.sample; + }; + }; + + environment.systemPackages = [ + cheatSheetCommand + pkgs.foot + pkgs.jq + pkgs.kdePackages.dolphin + pkgs.kdePackages.kate + pkgs.kdePackages.konsole + pkgs.kdePackages.qttools + pkgs.less + ]; + environment.etc."run-kde/cheat-sheet.txt".source = cheatSheet; + environment.etc."run-kde/config-home".source = configHome; + + # The host's password and key, taken from the directory it shares. The files may belong to a user + # of the host who is not the guest's user, so the guest copies them, as root, and gives them the + # owner and the mode that RDP's unit and ssh want. Before the login, so that the unit that reads + # the password finds it, and before ssh. A guest that is started with no directory has neither, + # and RDP does not start. + systemd.services.run-kde-credentials = { + description = "Take the RDP password and the ssh key that the host shares"; + wantedBy = [ "multi-user.target" ]; + before = [ + "display-manager.service" + "sshd.service" + ]; + unitConfig = { + ConditionPathExists = shared; + RequiresMountsFor = shared; + }; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + path = [ pkgs.coreutils ]; + script = '' + install -d -m 0755 ${taken} ${taken}/authorized_keys.d + if [ -r ${shared}/rdp-password ]; then + install -m 0400 -o ${user} -g ${config.users.users.${user}.group} \ + ${shared}/rdp-password ${taken}/rdp-password + fi + if [ -r ${shared}/ssh_key.pub ]; then + install -m 0644 ${shared}/ssh_key.pub ${taken}/authorized_keys.d/${user} + fi + ''; + }; + services.openssh.authorizedKeysFiles = [ "${taken}/authorized_keys.d/%u" ]; + + systemd.services.run-kde-report = { + description = "Say what KWin draws with"; + wantedBy = [ "multi-user.target" ]; + after = [ "user@${toString config.users.users.${user}.uid}.service" ]; + serviceConfig = { + # simple, so that waiting for KWin does not hold up the machine's start + Type = "simple"; + User = user; + ExecStart = lib.getExe report; + # the line goes to the console, which is the terminal of whoever started the guest + StandardOutput = "journal+console"; + }; + }; + + # The windows of the demo, once the session is up. The unit does not wait for the panel, the + # script does, since only komorebi can say that it knows the panel's room. + systemd.user.services.run-kde-demo = { + description = "Open the windows of the demo"; + wantedBy = [ "plasma-workspace.target" ]; + after = [ + "plasma-core.target" + "komorebi.service" + ]; + # the PATH a terminal in the session has, as the module's unit has it + enableDefaultPath = false; + environment.PATH = lib.concatStringsSep ":" [ + "/run/wrappers/bin" + "/etc/profiles/per-user/%u/bin" + "/run/current-system/sw/bin" + ]; + serviceConfig = { + Type = "simple"; + ExecStart = lib.getExe demo; + # the windows are in scopes of their own, and stay when the script is done + KillMode = "process"; + }; + }; + + # What only the runner, which is the machine of the person who starts the guest, can say. The + # ports are forwarded and the display device is added on its command line, and the memory is + # sized to what that machine has: the shared memory that virtiofs needs is a backend of the + # size of the guest's memory, which nixpkgs would fix at the size of `memorySize`. + virtualisation.vmVariant.virtualisation = { + forwardPorts = lib.mkForce [ ]; + # `run-kde` says whether KVM is there, and `--tcg` lets it start without + qemu.forceAccel = lib.mkForce false; + qemu.enableSharedMemory = false; + qemu.options = [ + "-object memory-backend-memfd,id=mem0,size=\${RUN_KDE_MEMORY:-8192}M,share=on" + "-machine memory-backend=mem0" + ]; + sharedDirectories.run-kde = { + source = "\${RUN_KDE_STATE:?set RUN_KDE_STATE to the directory with the password and the key}"; + target = shared; + }; + # a mount under /run is hidden when the root file system's /run is replaced by a tmpfs, which + # happens before the units run + fileSystems.${shared}.neededForBoot = lib.mkForce false; + }; +}