nixpkgs/nixos/modules/services/hardware/kanata.nix
Lin Jian ff19ecca71
nixos/kanata: specify linux-dev as a list
This is better because it allows some special characters in the device
path.  See [1] for more information.

[1]: https://github.com/jtroo/kanata/pull/647
2023-12-23 10:10:15 +08:00

187 lines
5.9 KiB
Nix

{ config, lib, pkgs, utils, ... }:
with lib;
let
cfg = config.services.kanata;
keyboard = {
options = {
devices = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/dev/input/by-id/usb-0000_0000-event-kbd" ];
description = mdDoc ''
Paths to keyboard devices.
An empty list, the default value, lets kanata detect which
input devices are keyboards and intercept them all.
'';
};
config = mkOption {
type = types.lines;
example = ''
(defsrc
grv 1 2 3 4 5 6 7 8 9 0 - = bspc
tab q w e r t y u i o p [ ] \
caps a s d f g h j k l ; ' ret
lsft z x c v b n m , . / rsft
lctl lmet lalt spc ralt rmet rctl)
(deflayer qwerty
grv 1 2 3 4 5 6 7 8 9 0 - = bspc
tab q w e r t y u i o p [ ] \
@cap a s d f g h j k l ; ' ret
lsft z x c v b n m , . / rsft
lctl lmet lalt spc ralt rmet rctl)
(defalias
;; tap within 100ms for capslk, hold more than 100ms for lctl
cap (tap-hold 100 100 caps lctl))
'';
description = mdDoc ''
Configuration other than `defcfg`.
See [example config files](https://github.com/jtroo/kanata)
for more information.
'';
};
extraDefCfg = mkOption {
type = types.lines;
default = "";
example = "danger-enable-cmd yes";
description = mdDoc ''
Configuration of `defcfg` other than `linux-dev` (generated
from the devices option) and
`linux-continue-if-no-devs-found` (hardcoded to be yes).
See [example config files](https://github.com/jtroo/kanata)
for more information.
'';
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [ ];
description = mdDoc "Extra command line arguments passed to kanata.";
};
port = mkOption {
type = types.nullOr types.port;
default = null;
example = 6666;
description = mdDoc ''
Port to run the TCP server on. `null` will not run the server.
'';
};
};
};
mkName = name: "kanata-${name}";
mkDevices = devices:
let
devicesString = pipe devices [
(map (device: "\"" + device + "\""))
(concatStringsSep " ")
];
in
optionalString ((length devices) > 0) "linux-dev (${devicesString})";
mkConfig = name: keyboard: pkgs.writeText "${mkName name}-config.kdb" ''
(defcfg
${keyboard.extraDefCfg}
${mkDevices keyboard.devices}
linux-continue-if-no-devs-found yes)
${keyboard.config}
'';
mkService = name: keyboard: nameValuePair (mkName name) {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "notify";
ExecStart = ''
${getExe cfg.package} \
--cfg ${mkConfig name keyboard} \
--symlink-path ''${RUNTIME_DIRECTORY}/${name} \
${optionalString (keyboard.port != null) "--port ${toString keyboard.port}"} \
${utils.escapeSystemdExecArgs keyboard.extraArgs}
'';
DynamicUser = true;
RuntimeDirectory = mkName name;
SupplementaryGroups = with config.users.groups; [
input.name
uinput.name
];
# hardening
DeviceAllow = [
"/dev/uinput rw"
"char-input r"
];
CapabilityBoundingSet = [ "" ];
DevicePolicy = "closed";
IPAddressAllow = optional (keyboard.port != null) "localhost";
IPAddressDeny = [ "any" ];
LockPersonality = true;
MemoryDenyWriteExecute = true;
PrivateNetwork = keyboard.port == null;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RestrictAddressFamilies = [ "AF_UNIX" ] ++ optional (keyboard.port != null) "AF_INET";
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = [ "native" ];
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
UMask = "0077";
};
};
in
{
options.services.kanata = {
enable = mkEnableOption (mdDoc "kanata");
package = mkPackageOption pkgs "kanata" {
example = "kanata-with-cmd";
extraDescription = ''
::: {.note}
If {option}`danger-enable-cmd` is enabled in any of the keyboards, the
`kanata-with-cmd` package should be used.
:::
'';
};
keyboards = mkOption {
type = types.attrsOf (types.submodule keyboard);
default = { };
description = mdDoc "Keyboard configurations.";
};
};
config = mkIf cfg.enable {
warnings =
let
keyboardsWithEmptyDevices = filterAttrs (name: keyboard: keyboard.devices == [ ]) cfg.keyboards;
existEmptyDevices = length (attrNames keyboardsWithEmptyDevices) > 0;
moreThanOneKeyboard = length (attrNames cfg.keyboards) > 1;
in
optional (existEmptyDevices && moreThanOneKeyboard) "One device can only be intercepted by one kanata instance. Setting services.kanata.keyboards.${head (attrNames keyboardsWithEmptyDevices)}.devices = [ ] and using more than one services.kanata.keyboards may cause a race condition.";
hardware.uinput.enable = true;
systemd.services = mapAttrs' mkService cfg.keyboards;
};
meta.maintainers = with maintainers; [ linj ];
}