Files
komorebi/nix/linux/dev/run-kde.nix
T
LGUG2Z 05698a34ba fix(nix): the nix files read the final directories
The pure moves put the KDE and macOS nix files under nix/linux and nix/macos but left the
relative paths they read: the JSON schema (now schemas/linux and schemas/macos), the example
configurations and key binding samples (docs/linux, docs/macos), the key binding fixtures
(komorebi-hotkeys at the top level), the wsl2 probe script (scripts/linux). Each path now
names the file in the final directory, one more directory up because the files are one level
deeper.

Not touched: flake.nix and the nested flakes (next revisions), which still name the old paths.

Verified: not verified (nothing evaluates until the flake is rewritten; the new flake checks
these paths).
2026-11-05 15:03:27 -08:00

442 lines
16 KiB
Nix

# 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, docs/trying-it.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/linux/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/linux/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/linux/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";
};
};
# Mesa's video compositor (vl_compositor) never loads an RGB to YUV matrix on a driver that takes
# its graphics path, which virgl does: the draw multiplies by the YUV to RGB one of its initial
# state, and the NV12 that scale_vaapi makes of the screen, which KRDP encodes, is the wrong
# picture (nix/dev/mesa-virgl-rgb-yuv-csc.patch). `builtins.path` makes the patch a store path of
# its own, so that Mesa is built once for any way of reading this file. There is no binary cache:
# this is a build of Mesa from source.
#
# The same driver leaks a sync_file, an out-fence of the kernel, for every picture it converts or
# uploads: the surface keeps the fence of its last flush in `pipe_fence`, and the next flush of
# it stores a new one over it, so the first is never released, and its file descriptor is never
# closed. A KRDP session ran out of descriptors (1024) after about 950 frames, and the stream
# froze (nix/dev/mesa-virgl-fence-leak.patch).
hardware.graphics.package = pkgs.mesa.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [
(builtins.path {
path = ./mesa-virgl-rgb-yuv-csc.patch;
name = "mesa-virgl-rgb-yuv-csc.patch";
})
(builtins.path {
path = ./mesa-virgl-fence-leak.patch;
name = "mesa-virgl-fence-leak.patch";
})
];
});
# 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 never asks virglrenderer for the host's video encoder, so the guest's VA-API lists none
# and KRDP encodes H.264 on the guest's CPUs. The patch does (nix/dev/qemu-virgl-video.patch);
# VIRGL_RENDERER_USE_VIDEO is only defined with VIRGL_RENDERER_UNSTABLE_APIS. Only the runner
# has it: there is no binary cache, so it is a QEMU built from source. The virglrenderer under
# it has four patches, in this order, for a host whose encoder is the d3d12 one of WSL 2:
# chroma-format the guest's Mesa and virglrenderer number the chroma format of a codec
# differently: 4:2:0 was read as 4:2:2, and the encoder refused every codec
# video-memory-upload
# the d3d12 driver has no dma-bufs, so the picture to encode goes to the
# surface through memory
# h264-packed-sps Mesa's VA-API takes the level of the stream from a packed SPS, which
# virglrenderer never sent: level 0 crashed the d3d12 encoder
# ubo0-constants a constant buffer 0 bound as a buffer was never read by the shader, so the
# colour matrix of an RGB to YUV conversion was zero
qemu.package =
(pkgs.qemu_kvm.override {
virglrenderer = pkgs.virglrenderer.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [
./virglrenderer-chroma-format.patch
./virglrenderer-video-memory-upload.patch
./virglrenderer-h264-packed-sps.patch
./virglrenderer-ubo0-constants.patch
];
});
}).overrideAttrs
(old: {
patches = (old.patches or [ ]) ++ [ ./qemu-virgl-video.patch ];
configureFlags = (old.configureFlags or [ ]) ++ [
"--extra-cflags=-DVIRGL_RENDERER_UNSTABLE_APIS"
];
doCheck = 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;
};
}