summary refs log tree commit diff
path: root/nixos/modules/services/hardware/kanata.nix
diff options
context:
space:
mode:
authorLin Jian <me@linj.tech>2022-08-01 15:35:34 +0800
committerpennae <82953136+pennae@users.noreply.github.com>2022-08-06 15:07:44 +0200
commitb6d45f1448793e937a81c5d72967b83a23a088e0 (patch)
tree35ad5cf0bad5b83fc5b25acbebe2784760a4a265 /nixos/modules/services/hardware/kanata.nix
parenta43993655a54c3e69ed0145610c7ee26318db7af (diff)
nixos/kanata: sync with version 1.0.6
- improve some descriptions
- device -> devices
- add options
  - extraArgs
  - port
- create a symlink in RUNTIME_DIRECTORY
- grant it read permission of /dev/uinput
- relax network-related restrictions when port is used
- change type of some hardening options to list to align with systemd
  - CapabilityBoundingSet
  - IPAddressDeny
  - SystemCallArchitectures
Diffstat (limited to 'nixos/modules/services/hardware/kanata.nix')
-rw-r--r--nixos/modules/services/hardware/kanata.nix121
1 files changed, 90 insertions, 31 deletions
diff --git a/nixos/modules/services/hardware/kanata.nix b/nixos/modules/services/hardware/kanata.nix
index c7cd3c9d2eb95..ccba87531e66e 100644
--- a/nixos/modules/services/hardware/kanata.nix
+++ b/nixos/modules/services/hardware/kanata.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 with lib;
 
@@ -7,10 +7,20 @@ let
 
   keyboard = {
     options = {
-      device = mkOption {
-        type = types.str;
-        example = "/dev/input/by-id/usb-0000_0000-event-kbd";
-        description = lib.mdDoc "Path to the keyboard device.";
+      devices = mkOption {
+        type = types.addCheck (types.listOf types.str)
+          (devices: (length devices) > 0);
+        example = [ "/dev/input/by-id/usb-0000_0000-event-kbd" ];
+        # TODO replace note with tip, which has not been implemented yet in
+        # nixos/lib/make-options-doc/mergeJSON.py
+        description = mdDoc ''
+          Paths to keyboard devices.
+
+          ::: {.note}
+          To avoid unnecessary triggers of the service unit, unplug devices in
+          the order of the list.
+          :::
+        '';
       };
       config = mkOption {
         type = types.lines;
@@ -33,18 +43,32 @@ let
             ;; tap within 100ms for capslk, hold more than 100ms for lctl
             cap (tap-hold 100 100 caps lctl))
         '';
-        description = lib.mdDoc ''
-          Configuration other than defcfg.
-          See <https://github.com/jtroo/kanata> for more information.
+        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 = lib.mdDoc ''
-          Configuration of defcfg other than linux-dev.
-          See <https://github.com/jtroo/kanata> for more information.
+        description = mdDoc ''
+          Configuration of `defcfg` other than `linux-dev`. 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 notification server on. `null` will not run the
+          server.
         '';
       };
     };
@@ -52,16 +76,18 @@ let
 
   mkName = name: "kanata-${name}";
 
+  mkDevices = devices: concatStringsSep ":" devices;
+
   mkConfig = name: keyboard: pkgs.writeText "${mkName name}-config.kdb" ''
     (defcfg
       ${keyboard.extraDefCfg}
-      linux-dev ${keyboard.device})
+      linux-dev ${mkDevices keyboard.devices})
 
     ${keyboard.config}
   '';
 
   mkService = name: keyboard: nameValuePair (mkName name) {
-    description = "kanata for ${keyboard.device}";
+    description = "kanata for ${mkDevices keyboard.devices}";
 
     # Because path units are used to activate service units, which
     # will start the old stopped services during "nixos-rebuild
@@ -72,10 +98,14 @@ let
     serviceConfig = {
       ExecStart = ''
         ${cfg.package}/bin/kanata \
-          --cfg ${mkConfig name keyboard}
+          --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
@@ -83,15 +113,16 @@ let
 
       # hardening
       DeviceAllow = [
-        "/dev/uinput w"
+        "/dev/uinput rw"
         "char-input r"
       ];
-      CapabilityBoundingSet = "";
+      CapabilityBoundingSet = [ "" ];
       DevicePolicy = "closed";
-      IPAddressDeny = "any";
+      IPAddressAllow = optional (keyboard.port != null) "localhost";
+      IPAddressDeny = [ "any" ];
       LockPersonality = true;
       MemoryDenyWriteExecute = true;
-      PrivateNetwork = true;
+      PrivateNetwork = keyboard.port == null;
       PrivateUsers = true;
       ProcSubset = "pid";
       ProtectClock = true;
@@ -102,10 +133,11 @@ let
       ProtectKernelModules = true;
       ProtectKernelTunables = true;
       ProtectProc = "invisible";
-      RestrictAddressFamilies = "none";
+      RestrictAddressFamilies =
+        if (keyboard.port == null) then "none" else [ "AF_INET" ];
       RestrictNamespaces = true;
       RestrictRealtime = true;
-      SystemCallArchitectures = "native";
+      SystemCallArchitectures = [ "native" ];
       SystemCallFilter = [
         "@system-service"
         "~@privileged"
@@ -115,13 +147,32 @@ let
     };
   };
 
-  mkPath = name: keyboard: nameValuePair (mkName name) {
-    description = "kanata trigger for ${keyboard.device}";
-    wantedBy = [ "multi-user.target" ];
-    pathConfig = {
-      PathExists = keyboard.device;
+  mkPathName = i: name: "${mkName name}-${toString i}";
+
+  mkPath = name: n: i: device:
+    nameValuePair (mkPathName i name) {
+      description =
+        "${toString (i+1)}/${toString n} kanata trigger for ${name}, watching ${device}";
+      wantedBy = optional (i == 0) "multi-user.target";
+      pathConfig = {
+        PathExists = device;
+        # (ab)use systemd.path to construct a trigger chain so that the
+        # service unit is only started when all paths exist
+        # however, manual of systemd.path says Unit's suffix is not ".path"
+        Unit =
+          if (i + 1) == n
+          then "${mkName name}.service"
+          else "${mkPathName (i + 1) name}.path";
+      };
+      unitConfig.StopPropagatedFrom = optional (i > 0) "${mkName name}.service";
     };
-  };
+
+  mkPaths = name: keyboard:
+    let
+      n = length keyboard.devices;
+    in
+    imap0 (mkPath name n) keyboard.devices
+  ;
 in
 {
   options.services.kanata = {
@@ -131,15 +182,19 @@ in
       default = pkgs.kanata;
       defaultText = lib.literalExpression "pkgs.kanata";
       example = lib.literalExpression "pkgs.kanata-with-cmd";
-      description = lib.mdDoc ''
-        kanata package to use.
-        If you enable danger-enable-cmd, pkgs.kanata-with-cmd should be used.
+      description = mdDoc ''
+        The kanata package to use.
+
+        ::: {.note}
+        If `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 = lib.mdDoc "Keyboard configurations.";
+      description = mdDoc "Keyboard configurations.";
     };
   };
 
@@ -147,7 +202,11 @@ in
     hardware.uinput.enable = true;
 
     systemd = {
-      paths = mapAttrs' mkPath cfg.keyboards;
+      paths = trivial.pipe cfg.keyboards [
+        (mapAttrsToList mkPaths)
+        concatLists
+        listToAttrs
+      ];
       services = mapAttrs' mkService cfg.keyboards;
     };
   };