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; + }; +}