python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / hardware / kanata.nix
blob84265eb8f947c32c5bd2e79a3a8709dcf1a45ebd
1 { config, lib, pkgs, utils, ... }:
3 with lib;
5 let
6   cfg = config.services.kanata;
8   keyboard = {
9     options = {
10       devices = mkOption {
11         type = types.addCheck (types.listOf types.str)
12           (devices: (length devices) > 0);
13         example = [ "/dev/input/by-id/usb-0000_0000-event-kbd" ];
14         # TODO replace note with tip, which has not been implemented yet in
15         # nixos/lib/make-options-doc/mergeJSON.py
16         description = mdDoc ''
17           Paths to keyboard devices.
19           ::: {.note}
20           To avoid unnecessary triggers of the service unit, unplug devices in
21           the order of the list.
22           :::
23         '';
24       };
25       config = mkOption {
26         type = types.lines;
27         example = ''
28           (defsrc
29             grv  1    2    3    4    5    6    7    8    9    0    -    =    bspc
30             tab  q    w    e    r    t    y    u    i    o    p    [    ]    \
31             caps a    s    d    f    g    h    j    k    l    ;    '    ret
32             lsft z    x    c    v    b    n    m    ,    .    /    rsft
33             lctl lmet lalt           spc            ralt rmet rctl)
35           (deflayer qwerty
36             grv  1    2    3    4    5    6    7    8    9    0    -    =    bspc
37             tab  q    w    e    r    t    y    u    i    o    p    [    ]    \
38             @cap a    s    d    f    g    h    j    k    l    ;    '    ret
39             lsft z    x    c    v    b    n    m    ,    .    /    rsft
40             lctl lmet lalt           spc            ralt rmet rctl)
42           (defalias
43             ;; tap within 100ms for capslk, hold more than 100ms for lctl
44             cap (tap-hold 100 100 caps lctl))
45         '';
46         description = mdDoc ''
47           Configuration other than `defcfg`. See [example config
48           files](https://github.com/jtroo/kanata) for more information.
49         '';
50       };
51       extraDefCfg = mkOption {
52         type = types.lines;
53         default = "";
54         example = "danger-enable-cmd yes";
55         description = mdDoc ''
56           Configuration of `defcfg` other than `linux-dev`. See [example
57           config files](https://github.com/jtroo/kanata) for more information.
58         '';
59       };
60       extraArgs = mkOption {
61         type = types.listOf types.str;
62         default = [ ];
63         description = mdDoc "Extra command line arguments passed to kanata.";
64       };
65       port = mkOption {
66         type = types.nullOr types.port;
67         default = null;
68         example = 6666;
69         description = mdDoc ''
70           Port to run the notification server on. `null` will not run the
71           server.
72         '';
73       };
74     };
75   };
77   mkName = name: "kanata-${name}";
79   mkDevices = devices: concatStringsSep ":" devices;
81   mkConfig = name: keyboard: pkgs.writeText "${mkName name}-config.kdb" ''
82     (defcfg
83       ${keyboard.extraDefCfg}
84       linux-dev ${mkDevices keyboard.devices})
86     ${keyboard.config}
87   '';
89   mkService = name: keyboard: nameValuePair (mkName name) {
90     description = "kanata for ${mkDevices keyboard.devices}";
92     # Because path units are used to activate service units, which
93     # will start the old stopped services during "nixos-rebuild
94     # switch", stopIfChanged here is a workaround to make sure new
95     # services are running after "nixos-rebuild switch".
96     stopIfChanged = false;
98     serviceConfig = {
99       ExecStart = ''
100         ${cfg.package}/bin/kanata \
101           --cfg ${mkConfig name keyboard} \
102           --symlink-path ''${RUNTIME_DIRECTORY}/${name} \
103           ${optionalString (keyboard.port != null) "--port ${toString keyboard.port}"} \
104           ${utils.escapeSystemdExecArgs keyboard.extraArgs}
105       '';
107       DynamicUser = true;
108       RuntimeDirectory = mkName name;
109       SupplementaryGroups = with config.users.groups; [
110         input.name
111         uinput.name
112       ];
114       # hardening
115       DeviceAllow = [
116         "/dev/uinput rw"
117         "char-input r"
118       ];
119       CapabilityBoundingSet = [ "" ];
120       DevicePolicy = "closed";
121       IPAddressAllow = optional (keyboard.port != null) "localhost";
122       IPAddressDeny = [ "any" ];
123       LockPersonality = true;
124       MemoryDenyWriteExecute = true;
125       PrivateNetwork = keyboard.port == null;
126       PrivateUsers = true;
127       ProcSubset = "pid";
128       ProtectClock = true;
129       ProtectControlGroups = true;
130       ProtectHome = true;
131       ProtectHostname = true;
132       ProtectKernelLogs = true;
133       ProtectKernelModules = true;
134       ProtectKernelTunables = true;
135       ProtectProc = "invisible";
136       RestrictAddressFamilies =
137         if (keyboard.port == null) then "none" else [ "AF_INET" ];
138       RestrictNamespaces = true;
139       RestrictRealtime = true;
140       SystemCallArchitectures = [ "native" ];
141       SystemCallFilter = [
142         "@system-service"
143         "~@privileged"
144         "~@resources"
145       ];
146       UMask = "0077";
147     };
148   };
150   mkPathName = i: name: "${mkName name}-${toString i}";
152   mkPath = name: n: i: device:
153     nameValuePair (mkPathName i name) {
154       description =
155         "${toString (i+1)}/${toString n} kanata trigger for ${name}, watching ${device}";
156       wantedBy = optional (i == 0) "multi-user.target";
157       pathConfig = {
158         PathExists = device;
159         # (ab)use systemd.path to construct a trigger chain so that the
160         # service unit is only started when all paths exist
161         # however, manual of systemd.path says Unit's suffix is not ".path"
162         Unit =
163           if (i + 1) == n
164           then "${mkName name}.service"
165           else "${mkPathName (i + 1) name}.path";
166       };
167       unitConfig.StopPropagatedFrom = optional (i > 0) "${mkName name}.service";
168     };
170   mkPaths = name: keyboard:
171     let
172       n = length keyboard.devices;
173     in
174     imap0 (mkPath name n) keyboard.devices
175   ;
178   options.services.kanata = {
179     enable = mkEnableOption (lib.mdDoc "kanata");
180     package = mkOption {
181       type = types.package;
182       default = pkgs.kanata;
183       defaultText = literalExpression "pkgs.kanata";
184       example = literalExpression "pkgs.kanata-with-cmd";
185       description = mdDoc ''
186         The kanata package to use.
188         ::: {.note}
189         If `danger-enable-cmd` is enabled in any of the keyboards, the
190         `kanata-with-cmd` package should be used.
191         :::
192       '';
193     };
194     keyboards = mkOption {
195       type = types.attrsOf (types.submodule keyboard);
196       default = { };
197       description = mdDoc "Keyboard configurations.";
198     };
199   };
201   config = mkIf cfg.enable {
202     hardware.uinput.enable = true;
204     systemd = {
205       paths = trivial.pipe cfg.keyboards [
206         (mapAttrsToList mkPaths)
207         concatLists
208         listToAttrs
209       ];
210       services = mapAttrs' mkService cfg.keyboards;
211     };
212   };
214   meta.maintainers = with maintainers; [ linj ];