Files
komorebi/nix/dev/plasma-vm.nix
T
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

232 lines
7.7 KiB
Nix

{
config,
lib,
pkgs,
hostPkgs,
...
}:
# A NixOS Plasma 6 guest to develop and test against a real KWin, with komorebi installed by the
# flake's module, `services.komorebi`, from the flake's packages. The flake builds it for an Apple
# silicon Mac, which runs it under QEMU with HVF, and for a Linux machine, which runs it under QEMU
# with KVM; docs/dev/utm.md says how to build and run it. The system of the guest and the packages
# of the machine that runs QEMU (`hostPkgs`) come from the flake, so that one file serves them all.
#
# The guest mounts the repository at /repo, and keeps its disk in the directory it is started
# from, which is best .vm (gitignored):
#
# nix build .#nixosConfigurations.plasma-vm.config.system.build.vm -o .vm/result-plasma-vm
# cd .vm && KOMOREBI_REPO=$PWD/.. ./result-plasma-vm/bin/run-plasma-vm-vm
#
# The console, in the terminal that runs the guest, is logged in as the user `komorebi`. ssh is
# forwarded to localhost:2222 (2223 for the session), for the keys in ~/.ssh/authorized_keys of
# that user, which the guest's disk keeps.
#
# The session variant is the one to live in: Plasma logs in by itself, the module's unit runs
# komorebi in it, and it can be reached over RDP (krdp.nix).
let
cfg = config.komorebi.plasmaVm;
# The guest's one display, a virtio-gpu, so that KWin has a DRM device to draw on. The machine of
# an aarch64 guest has no display of its own. The machine of an x86_64 guest has a VGA, which QEMU
# adds unless it is told not to, and KWin would draw on it as well as on the virtio-gpu: a second
# head. An x86_64 guest gets `virtio-vga`, a virtio-gpu that is also the VGA, in its place.
gpu =
args:
if pkgs.stdenv.hostPlatform.isx86_64 then
[
"-vga none"
"-device virtio-vga${args}"
]
else
[ "-device virtio-gpu-pci${args}" ];
in
{
imports = [ ./krdp.nix ];
options.komorebi.plasmaVm = {
session = lib.mkEnableOption ''
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 [
{
system.stateVersion = "26.05";
networking.hostName = "plasma-vm";
services.desktopManager.plasma6.enable = true;
programs.dconf.enable = true;
# mesa (llvmpipe over the virtio-gpu render node) so kwin can composite with GL
hardware.graphics.enable = true;
fonts.enableDefaultPackages = true;
# komorebi and komorebic, from the flake's packages; the unit starts with a Plasma session
services.komorebi.enable = true;
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
};
};
# the way in before any key is in ~/.ssh/authorized_keys: there is no password anywhere
services.getty.autologinUser = "komorebi";
users.users.komorebi = {
isNormalUser = true;
uid = 1000;
extraGroups = [
"wheel"
"video"
"render"
];
# keep a user manager (and XDG_RUNTIME_DIR) alive across ssh sessions
linger = true;
};
security.sudo.wheelNeedsPassword = false;
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
environment.systemPackages = with pkgs; [
# clients to tile: Qt/KDE, GTK3/4 (CSD), Electron-ish via firefox, plain wayland, XWayland
kdePackages.konsole
kdePackages.dolphin
kdePackages.kate
kdePackages.spectacle
firefox
gnome-text-editor
gnome-calculator
foot
xterm
# poking at KWin
kdePackages.qttools # qdbus6
dbus
jq
kdotool
strace
# installing plasma-utm onto a disk image (docs/dev/utm.md)
nixos-install-tools
dosfstools
parted
e2fsprogs
# building komorebi from /repo in the guest
rustc
cargo
gcc
pkg-config
git
rsync
htop
];
environment.sessionVariables = {
# kwin refuses to load animating effects (including the stock scripted ones) under software
# rendering; llvmpipe is all this vm has
KWIN_EFFECTS_FORCE_ANIMATIONS = "1";
# always build in guest-local storage; /repo is a 9p mount and is slow for target dirs
CARGO_TARGET_DIR = "/home/komorebi/target";
};
virtualisation.vmVariant = {
virtualisation = {
host.pkgs = hostPkgs;
qemu.forceAccel = true;
cores = 6;
memorySize = 8192;
diskSize = 20 * 1024;
graphics = false;
# no display, but a gpu so there is a drm render node
qemu.options = lib.mkIf (cfg.display && !cfg.session) (gpu "");
forwardPorts = [
{
from = "host";
host.address = "127.0.0.1";
host.port = if cfg.session then 2223 else 2222;
guest.port = 22;
}
];
sharedDirectories.repo = {
# the runner says so when it is not set
source = "\${KOMOREBI_REPO:?set KOMOREBI_REPO to the root of the repository}";
target = "/repo";
writable = true;
};
};
};
}
(lib.mkIf cfg.session {
# a separate disk image (named after the host) and ssh port so both vms can run at once
networking.hostName = lib.mkForce "plasma-vm-session";
services.displayManager = {
sddm = {
enable = true;
wayland.enable = true;
};
autoLogin = {
enable = true;
user = "komorebi";
};
defaultSession = "plasma";
};
# 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 = 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]
Autolock=false
LockOnResume=false
'';
# and so does a display that has gone to sleep: with the output off, KWin sends no frame
# callbacks, so clients stop answering resizes and every window stays where it was while komorebi
# asks for another place (found on the UTM guest after it sat idle: black screenshots, nothing tiled)
environment.etc."xdg/powerdevilrc".text = ''
[AC][Display]
DimDisplayWhenIdle=false
TurnOffDisplayWhenIdle=false
[AC][SuspendAndShutdown]
AutoSuspendAction=0
'';
komorebi.krdp.enable = true;
# only the host's loopback reaches it
virtualisation.vmVariant.virtualisation.forwardPorts = [
{
from = "host";
host.address = "127.0.0.1";
host.port = 3389;
guest.port = 3389;
}
];
})
];
}