about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/hardware/kanata.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/modules/services/hardware/kanata.nix')
-rw-r--r--nixpkgs/nixos/modules/services/hardware/kanata.nix215
1 files changed, 215 insertions, 0 deletions
diff --git a/nixpkgs/nixos/modules/services/hardware/kanata.nix b/nixpkgs/nixos/modules/services/hardware/kanata.nix
new file mode 100644
index 000000000000..ccba87531e66
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/hardware/kanata.nix
@@ -0,0 +1,215 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.services.kanata;
+
+  keyboard = {
+    options = {
+      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;
+        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`. 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.
+        '';
+      };
+    };
+  };
+
+  mkName = name: "kanata-${name}";
+
+  mkDevices = devices: concatStringsSep ":" devices;
+
+  mkConfig = name: keyboard: pkgs.writeText "${mkName name}-config.kdb" ''
+    (defcfg
+      ${keyboard.extraDefCfg}
+      linux-dev ${mkDevices keyboard.devices})
+
+    ${keyboard.config}
+  '';
+
+  mkService = name: keyboard: nameValuePair (mkName name) {
+    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
+    # switch", stopIfChanged here is a workaround to make sure new
+    # services are running after "nixos-rebuild switch".
+    stopIfChanged = false;
+
+    serviceConfig = {
+      ExecStart = ''
+        ${cfg.package}/bin/kanata \
+          --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 =
+        if (keyboard.port == null) then "none" else [ "AF_INET" ];
+      RestrictNamespaces = true;
+      RestrictRealtime = true;
+      SystemCallArchitectures = [ "native" ];
+      SystemCallFilter = [
+        "@system-service"
+        "~@privileged"
+        "~@resources"
+      ];
+      UMask = "0077";
+    };
+  };
+
+  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 = {
+    enable = mkEnableOption "kanata";
+    package = mkOption {
+      type = types.package;
+      default = pkgs.kanata;
+      defaultText = lib.literalExpression "pkgs.kanata";
+      example = lib.literalExpression "pkgs.kanata-with-cmd";
+      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 = mdDoc "Keyboard configurations.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    hardware.uinput.enable = true;
+
+    systemd = {
+      paths = trivial.pipe cfg.keyboards [
+        (mapAttrsToList mkPaths)
+        concatLists
+        listToAttrs
+      ];
+      services = mapAttrs' mkService cfg.keyboards;
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ linj ];
+}