mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-14 12:53:43 +01:00
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).
442 lines
16 KiB
Nix
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;
|
|
};
|
|
}
|