mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-13 12:23:38 +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).
567 lines
22 KiB
Nix
567 lines
22 KiB
Nix
# `services.komorebi`: komorebi in the Plasma session of every user of a NixOS machine. The flake
|
|
# exports this module as `nixosModules.default` and `nixosModules.komorebi`, together with its
|
|
# overlay, which provides `pkgs.komorebi-full` (see docs/installation.md). What the module
|
|
# guarantees, and what enforces each guarantee, is in docs/dev/ARCHITECTURE.md; its tests are
|
|
# nix/tests/nixos-module.nix and the VM suite `module`.
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.komorebi;
|
|
|
|
jsonFormat = pkgs.formats.json { };
|
|
|
|
# The options `just nixgen` makes from `schema.json`, which type `config`
|
|
komorebiOptionsModule = import ./komorebi-options.nix { inherit lib; };
|
|
|
|
# the Nix module system uses null for an optional field that is not set, and the schema wants
|
|
# the field to be absent
|
|
filterNulls =
|
|
value:
|
|
if value == null then
|
|
null
|
|
else if builtins.isList value then
|
|
map filterNulls (builtins.filter (x: x != null) value)
|
|
else if builtins.isAttrs value then
|
|
lib.filterAttrs (_n: v: v != null) (builtins.mapAttrs (_n: v: filterNulls v) value)
|
|
else
|
|
value;
|
|
|
|
# A komorebi.json, checked against the schema the Rust types make (`schema.json`). Comment
|
|
# lines are taken out first: komorebi reads JSON with comments, and the schema checker does not.
|
|
validateJsonFile =
|
|
{
|
|
name,
|
|
jsonFile,
|
|
}:
|
|
pkgs.runCommand "validated-${name}" { nativeBuildInputs = [ pkgs.check-jsonschema ]; } ''
|
|
grep -v '^\s*//' ${jsonFile} > clean.json
|
|
check-jsonschema --schemafile ${../../schemas/linux/schema.json} clean.json
|
|
cp clean.json $out
|
|
'';
|
|
|
|
fetchAppSpecificSource =
|
|
source:
|
|
pkgs.fetchurl {
|
|
url = "https://raw.githubusercontent.com/${source.owner}/${source.repo}/${source.rev}/${source.path}";
|
|
inherit (source) hash;
|
|
};
|
|
|
|
applicationSpecificConfigFiles = map fetchAppSpecificSource cfg.applicationSpecificConfiguration.sources;
|
|
|
|
# one file is a path, several are a list of paths
|
|
appSpecificPaths =
|
|
if builtins.length applicationSpecificConfigFiles == 1 then
|
|
"${builtins.head applicationSpecificConfigFiles}"
|
|
else
|
|
map (f: "${f}") applicationSpecificConfigFiles;
|
|
|
|
shouldUseAppSpecific =
|
|
cfg.applicationSpecificConfiguration.enable && applicationSpecificConfigFiles != [ ];
|
|
|
|
# What `config` and `applicationSpecificConfiguration` make: the komorebi.json as an attribute
|
|
# set, with no null in it, or null when there is nothing to write
|
|
configAttrs =
|
|
if cfg.config == null && !shouldUseAppSpecific then
|
|
null
|
|
else
|
|
filterNulls (
|
|
(if cfg.config == null then { } else cfg.config)
|
|
// lib.optionalAttrs shouldUseAppSpecific { app_specific_configuration_path = appSpecificPaths; }
|
|
);
|
|
|
|
# the file the daemon is given: the one `configFile` names, or the one the options make, each
|
|
# checked against the schema
|
|
komorebiConfigFile =
|
|
if cfg.configFile != null then
|
|
validateJsonFile {
|
|
name = "komorebi.json";
|
|
jsonFile = cfg.configFile;
|
|
}
|
|
else if configAttrs != null then
|
|
validateJsonFile {
|
|
name = "komorebi.json";
|
|
jsonFile = jsonFormat.generate "komorebi.json" configAttrs;
|
|
}
|
|
else
|
|
null;
|
|
|
|
kb = cfg.keybindings;
|
|
|
|
# key bindings are configured when any of the three is set; the assertions say what is missing
|
|
keybindingsSet = kb.format != null || kb.text != null || kb.file != null;
|
|
# the file the daemon reads, named after its format so that it reads as one
|
|
keybindingsFile =
|
|
if kb.file != null then kb.file else pkgs.writeText "komorebi-${toString kb.format}rc" kb.text;
|
|
|
|
# `komorebic hotkeys compile`: the static shortcuts of the file, in share/kglobalaccel, where KWin
|
|
# finds them in the system profile, and what komorebic had to say about the file
|
|
compiled = pkgs.runCommand "komorebi-keybindings" { } ''
|
|
mkdir -p $out/share/kglobalaccel
|
|
if ! ${lib.getExe' cfg.package "komorebic"} hotkeys compile \
|
|
--format ${toString kb.format} --mode ${kb.mode} ${lib.optionalString kb.strict "--strict"} \
|
|
${keybindingsFile} --output $out/share/kglobalaccel 2> $out/diagnostics.txt; then
|
|
cat $out/diagnostics.txt >&2
|
|
exit 1
|
|
fi
|
|
cat $out/diagnostics.txt >&2
|
|
'';
|
|
|
|
keybindingsArgs = [
|
|
"--keybindings"
|
|
"${keybindingsFile}"
|
|
"--keybindings-format"
|
|
(toString kb.format)
|
|
"--keybindings-mode"
|
|
kb.mode
|
|
"--chord-timeout"
|
|
(toString kb.chordTimeout)
|
|
]
|
|
++ lib.concatMap (name: [
|
|
"--app-alias"
|
|
(lib.escapeShellArg "${name}=${lib.concatStringsSep "," kb.appAliases.${name}}")
|
|
]) (lib.attrNames kb.appAliases);
|
|
|
|
# the KWin versions komorebi supports, and the KWin of this system
|
|
supportedKwin = import ./supported-kwin.nix;
|
|
kwinVersion = pkgs.kdePackages.kwin.version;
|
|
kwinUntested = lib.versionOlder supportedKwin.testedMax kwinVersion;
|
|
in
|
|
{
|
|
options.services.komorebi = {
|
|
enable = lib.mkEnableOption "komorebi, the tiling window manager, in the Plasma session of every user";
|
|
|
|
package = lib.mkPackageOption pkgs "komorebi-full" {
|
|
extraDescription = ''
|
|
It must have `bin/komorebi`, which the unit runs, and `bin/komorebic`. The module installs
|
|
both in the system profile.
|
|
'';
|
|
};
|
|
|
|
logLevel = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"error"
|
|
"warn"
|
|
"info"
|
|
"debug"
|
|
"trace"
|
|
];
|
|
default = "info";
|
|
description = ''
|
|
How much the daemon logs: its `--log-level`. The log goes to the journal of the unit
|
|
(`journalctl --user -u komorebi`) and to the daemon's log files, which `komorebic log`
|
|
follows.
|
|
'';
|
|
};
|
|
|
|
configFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
example = lib.literalExpression "./komorebi.json";
|
|
description = ''
|
|
The `komorebi.json` the daemon is configured by, checked against the schema of
|
|
`komorebi.json` when the system is built. The unit starts komorebi with it, as `--config`,
|
|
in place of the `komorebi.json` the daemon looks for in `~/.config/komorebi`
|
|
(`$KOMOREBI_CONFIG_HOME`). The file is in the Nix store, so it does not change under the
|
|
daemon: a new file is read when the daemon starts again. `komorebic configuration` still
|
|
shows the file of the config home, which the daemon does not read while this is set. With
|
|
neither `configFile` nor `config`, the daemon reads the file of the config home, if there
|
|
is one. Ignored by `config`, which is ignored when this is set.
|
|
'';
|
|
};
|
|
|
|
config = lib.mkOption {
|
|
type = lib.types.nullOr komorebiOptionsModule.options.komorebi.type;
|
|
default = null;
|
|
example = lib.literalExpression ''
|
|
{
|
|
default_workspace_padding = 15;
|
|
monitors = [
|
|
{
|
|
workspaces = [
|
|
{ name = "web"; layout = "BSP"; }
|
|
{ name = "code"; layout = "Columns"; }
|
|
];
|
|
}
|
|
];
|
|
}
|
|
'';
|
|
description = ''
|
|
The configuration of komorebi, written as Nix: every key of `komorebi.json`, typed from
|
|
the schema, so a layout that does not exist or a value of the wrong kind does not
|
|
evaluate. The module writes it as a `komorebi.json`, checks that against the schema, and
|
|
starts komorebi with it as `--config`. Ignored if `configFile` is set. A key you leave
|
|
out is the default the schema gives it, if it gives one, and the daemon's own otherwise.
|
|
'';
|
|
};
|
|
|
|
applicationSpecificConfiguration = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Whether to use the sources of application specific configuration.";
|
|
};
|
|
|
|
sources = lib.mkOption {
|
|
type = lib.types.listOf (
|
|
lib.types.submodule {
|
|
options = {
|
|
owner = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "GitHub repository owner.";
|
|
example = "LGUG2Z";
|
|
};
|
|
repo = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "GitHub repository name.";
|
|
example = "komorebi-application-specific-configuration";
|
|
};
|
|
rev = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Git revision (commit hash, tag, or branch).";
|
|
example = "09ebdcd95780b168cecde2f2d49096a58e91365f";
|
|
};
|
|
path = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "applications.json";
|
|
description = "Path to the JSON file within the repository.";
|
|
};
|
|
hash = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "SHA256 hash of the file, for reproducibility.";
|
|
example = "sha256-gfYubJpVEU6vxdd/vqujQhQX8mdAEE9ImugqdIyTQKk=";
|
|
};
|
|
};
|
|
}
|
|
);
|
|
default = [ ];
|
|
description = ''
|
|
Files of application specific configuration, each fetched from GitHub, which the module
|
|
gives the daemon as `app_specific_configuration_path` of the configuration it writes
|
|
(it replaces the value `config` has for that key). There are none by default: no
|
|
community file for KDE Plasma is known. Ignored when `configFile` is set.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
[
|
|
{
|
|
owner = "myuser";
|
|
repo = "my-komorebi-rules";
|
|
rev = "0123456789abcdef0123456789abcdef01234567";
|
|
path = "applications.json";
|
|
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
|
}
|
|
]
|
|
'';
|
|
};
|
|
};
|
|
|
|
effects.borders = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = ''
|
|
Install the KWin effect that draws komorebi's borders, KDE Rounded Corners. This is
|
|
installation only: whether borders are drawn is komorebi's setting (`komorebic border`),
|
|
which asks KWin to load the effect when it is on. KWin finds effects when it starts, so
|
|
an effect installed by a switch to a new system is found after the next login.
|
|
|
|
Without the effect, komorebi draws no border and leaves no room for one.
|
|
'';
|
|
};
|
|
|
|
package = lib.mkPackageOption pkgs "kde-rounded-corners-komorebi" {
|
|
extraDescription = ''
|
|
It must have `lib/qt-6/plugins/kwin/effects/plugins/kwin4_effect_shapecorners.so`, built
|
|
against the KWin of the system and not loaded by default. The default is KDE Rounded
|
|
Corners 0.11.0, which draws correctly on KWin 6.8.
|
|
'';
|
|
};
|
|
};
|
|
|
|
effects.animation = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = ''
|
|
Install the KWin effect that animates komorebi's windows going to their places, Geometry
|
|
Change. This is installation only: whether windows are animated is komorebi's setting
|
|
(`komorebic animation`), which asks KWin to load the effect when it is on. KWin finds
|
|
effects when it starts, so an effect installed by a switch to a new system is found
|
|
after the next login.
|
|
|
|
Without the effect, windows jump to their places.
|
|
'';
|
|
};
|
|
|
|
package = lib.mkPackageOption pkgs "kwin-effect-geometry-change-komorebi" {
|
|
extraDescription = ''
|
|
It must have `share/kwin/effects/kwin4_effect_geometry_change`, a scripted effect that
|
|
is not loaded by default.
|
|
'';
|
|
};
|
|
};
|
|
|
|
internal.configAttrs = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.attrs;
|
|
readOnly = true;
|
|
internal = true;
|
|
description = ''
|
|
What `config` and `applicationSpecificConfiguration` make, before it is written as JSON:
|
|
the `komorebi.json` as an attribute set with no null in it, or null when neither is set.
|
|
The tests of the module read it, so that they need not build the file.
|
|
'';
|
|
};
|
|
|
|
execStart = lib.mkOption {
|
|
type = lib.types.str;
|
|
readOnly = true;
|
|
description = ''
|
|
The command line the unit starts komorebi with: `bin/komorebi` of `package`, with
|
|
`logLevel`, with `--config` and the checked `komorebi.json` when `configFile` or `config` is
|
|
set, and with the key bindings when there are some. The module sets it, and setting it
|
|
anywhere else is an error. A unit of your own that runs komorebi somewhere else, as the VM
|
|
tests do in their headless sessions, starts it with this, the way the module does.
|
|
'';
|
|
};
|
|
|
|
keybindings = {
|
|
format = lib.mkOption {
|
|
type = lib.types.nullOr (
|
|
lib.types.enum [
|
|
"skhd"
|
|
"whkd"
|
|
"sxhkd"
|
|
]
|
|
);
|
|
default = null;
|
|
example = "skhd";
|
|
description = ''
|
|
The format of the key bindings: that of skhd's skhdrc, whkd's whkdrc or sxhkd's sxhkdrc.
|
|
Required with `text` or `file`.
|
|
'';
|
|
};
|
|
|
|
text = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.lines;
|
|
default = null;
|
|
example = ''
|
|
alt - h : komorebic focus left
|
|
alt - l : komorebic focus right
|
|
'';
|
|
description = ''
|
|
The key bindings, written in `format`. Set this or `file`, not both. skhd's `.load`,
|
|
which reads another file, is refused: the file would not be in the store.
|
|
'';
|
|
};
|
|
|
|
file = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
example = lib.literalExpression "./skhdrc";
|
|
description = "A file of key bindings, written in `format`. Set this or `text`, not both.";
|
|
};
|
|
|
|
mode = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"auto"
|
|
"engine"
|
|
"static"
|
|
];
|
|
default = "engine";
|
|
description = ''
|
|
Which bindings KDE runs by itself, as static shortcuts that the module installs in the
|
|
system profile, and which the daemon registers and runs (the engine). `engine`: every
|
|
binding is the engine's. `auto`: every binding that can be a static shortcut is one (one
|
|
combo, pressed, the same in every application, in a file with no modes, blacklist or
|
|
pause binding), and the engine runs the others. `static`: only static shortcuts; what
|
|
needs the engine is left out, with a warning. A static shortcut works with no komorebi
|
|
running and shows in System Settings, but takes about 0.2 s to run its command; the
|
|
engine takes a few milliseconds.
|
|
'';
|
|
};
|
|
|
|
chordTimeout = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 3000;
|
|
description = "How long, in milliseconds, the engine waits for the next key of a chord chain.";
|
|
};
|
|
|
|
layout = lib.mkOption {
|
|
type = lib.types.enum [ "us" ];
|
|
default = "us";
|
|
description = ''
|
|
The keyboard layout positional key names are read with: skhd's macOS keycodes (`0x21`)
|
|
and whkd's `oem_*` keys, and the symbol a shifted key types. `us` is the only one.
|
|
'';
|
|
};
|
|
|
|
strict = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Fail the build when something in the key bindings is not realized the way the file's own
|
|
hotkey daemon realizes it: a diagnostic of `komorebic hotkeys compile` that is a warning
|
|
or an error. The diagnostics are in the build log and in `diagnostics.txt` of the
|
|
`komorebi-keybindings` package either way.
|
|
'';
|
|
};
|
|
|
|
appAliases = lib.mkOption {
|
|
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
|
default = { };
|
|
example = {
|
|
firefox = [
|
|
"firefox-esr"
|
|
"librewolf"
|
|
];
|
|
};
|
|
description = ''
|
|
Other names of an application, for the bindings that are for one application: an skhdrc
|
|
or a whkdrc names applications as macOS or Windows does, and KDE knows them by their
|
|
executable, app id or desktop entry.
|
|
'';
|
|
};
|
|
};
|
|
|
|
kwin.untestedIsError = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Refuse a KWin newer than the newest one komorebi has been tested with (`testedMax` in
|
|
the repository's `nix/supported-kwin.nix`), instead of warning about it. A KWin older than
|
|
the oldest one komorebi supports (`min`) is refused either way.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = lib.mkMerge [
|
|
{
|
|
services.komorebi.execStart = lib.concatStringsSep " " (
|
|
[
|
|
(lib.getExe' cfg.package "komorebi")
|
|
"--log-level"
|
|
cfg.logLevel
|
|
]
|
|
++ lib.optionals (komorebiConfigFile != null) [
|
|
"--config"
|
|
"${komorebiConfigFile}"
|
|
]
|
|
++ lib.optionals keybindingsSet keybindingsArgs
|
|
);
|
|
services.komorebi.internal.configAttrs = configAttrs;
|
|
}
|
|
|
|
(lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = config.services.desktopManager.plasma6.enable;
|
|
message = ''
|
|
services.komorebi needs Plasma 6: komorebi runs in a Plasma Wayland session, and the
|
|
session starts it. Set services.desktopManager.plasma6.enable = true.
|
|
'';
|
|
}
|
|
{
|
|
assertion = keybindingsSet -> (kb.text != null) != (kb.file != null);
|
|
message = ''
|
|
services.komorebi.keybindings needs exactly one of text and file: the key bindings
|
|
themselves, or the file they are in.
|
|
'';
|
|
}
|
|
{
|
|
assertion = keybindingsSet -> kb.format != null;
|
|
message = ''
|
|
services.komorebi.keybindings needs a format: "skhd", "whkd" or "sxhkd".
|
|
'';
|
|
}
|
|
{
|
|
assertion =
|
|
!(
|
|
keybindingsSet
|
|
&& kb.format == "skhd"
|
|
&& lib.any (line: lib.hasPrefix ".load" (lib.trim line)) (
|
|
lib.splitString "\n" (if kb.text != null then kb.text else builtins.readFile kb.file)
|
|
)
|
|
);
|
|
message = ''
|
|
services.komorebi.keybindings: skhd's .load is not supported. It reads another file,
|
|
which would not be in the store; put its bindings in this one.
|
|
'';
|
|
}
|
|
{
|
|
assertion = lib.versionAtLeast kwinVersion supportedKwin.min;
|
|
message = ''
|
|
services.komorebi needs KWin ${supportedKwin.min} or newer, and pkgs.kdePackages.kwin is
|
|
${kwinVersion}. Use a nixpkgs with Plasma ${supportedKwin.min} or newer.
|
|
'';
|
|
}
|
|
{
|
|
assertion = !(cfg.kwin.untestedIsError && kwinUntested);
|
|
message = ''
|
|
pkgs.kdePackages.kwin is ${kwinVersion}, and komorebi has been tested with KWin
|
|
${supportedKwin.testedMax} and older. services.komorebi.kwin.untestedIsError is true, so
|
|
this is an error; set it to false to build the system with a warning instead.
|
|
'';
|
|
}
|
|
];
|
|
|
|
warnings =
|
|
lib.optional (
|
|
cfg.configFile != null && cfg.config != null
|
|
) "services.komorebi.config is ignored: services.komorebi.configFile is set."
|
|
++
|
|
lib.optional (cfg.configFile != null && cfg.applicationSpecificConfiguration.sources != [ ])
|
|
"services.komorebi.applicationSpecificConfiguration is ignored: services.komorebi.configFile is set."
|
|
++
|
|
lib.optional
|
|
(
|
|
cfg.configFile == null
|
|
&& cfg.config != null
|
|
&& (cfg.config.border or null) == true
|
|
&& !cfg.effects.borders.enable
|
|
)
|
|
"services.komorebi.config sets border to true, but services.komorebi.effects.borders.enable is false: without the effect, komorebi draws no border."
|
|
++ lib.optional (kwinUntested && !cfg.kwin.untestedIsError) ''
|
|
pkgs.kdePackages.kwin is ${kwinVersion}, and komorebi has been tested with KWin
|
|
${supportedKwin.testedMax} and older. Set services.komorebi.kwin.untestedIsError = true
|
|
to make this an error.
|
|
'';
|
|
|
|
environment.systemPackages = [
|
|
cfg.package
|
|
]
|
|
++ lib.optional cfg.effects.borders.enable cfg.effects.borders.package
|
|
++ lib.optional cfg.effects.animation.enable cfg.effects.animation.package
|
|
++ lib.optional keybindingsSet compiled;
|
|
|
|
# A user unit, so it runs for every user who logs in to Plasma. plasma-workspace.target is
|
|
# the target of a Plasma session's services, and plasma-core.target, which it requires, is
|
|
# ordered after KWin's service: the daemon exits when KWin is not on the session bus.
|
|
systemd.user.services.komorebi = {
|
|
description = "komorebi, the tiling window manager";
|
|
wantedBy = [ "plasma-workspace.target" ];
|
|
after = [ "plasma-core.target" ];
|
|
# NixOS gives a unit a PATH made of a few store paths; this is the PATH a terminal in the
|
|
# user's session has
|
|
enableDefaultPath = false;
|
|
environment.PATH = lib.concatStringsSep ":" [
|
|
"/run/wrappers/bin"
|
|
"/etc/profiles/per-user/%u/bin"
|
|
"/run/current-system/sw/bin"
|
|
];
|
|
serviceConfig = {
|
|
ExecStart = cfg.execStart;
|
|
# kglobalaccel keeps the keys of a daemon that died until they are unregistered: without
|
|
# this they would stay swallowed, running nothing, until the next start
|
|
ExecStopPost = "${lib.getExe' cfg.package "komorebic"} hotkeys release";
|
|
Restart = "on-failure";
|
|
# what komorebi exits with when it is stopped with SIGTERM, as systemd stops a unit
|
|
SuccessExitStatus = 130;
|
|
};
|
|
};
|
|
})
|
|
];
|
|
}
|