vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / hardware / kanata.nix
blob7557b1ae55f9071c080d8241db037b667d095e53
1 { config, lib, pkgs, utils, ... }:
2 let
3   cfg = config.services.kanata;
5   upstreamDoc = "See [the upstream documentation](https://github.com/jtroo/kanata/blob/main/docs/config.adoc) and [example config files](https://github.com/jtroo/kanata/tree/main/cfg_samples) for more information.";
7   keyboard = { name, config, ... }: {
8     options = {
9       devices = lib.mkOption {
10         type = lib.types.listOf lib.types.str;
11         default = [ ];
12         example = [ "/dev/input/by-id/usb-0000_0000-event-kbd" ];
13         description = ''
14           Paths to keyboard devices.
16           An empty list, the default value, lets kanata detect which
17           input devices are keyboards and intercept them all.
18         '';
19       };
20       config = lib.mkOption {
21         type = lib.types.lines;
22         example = ''
23           (defsrc
24             caps)
26           (deflayermap (default-layer)
27             ;; tap caps lock as caps lock, hold caps lock as left control
28             caps (tap-hold 100 100 caps lctl))
29         '';
30         description = ''
31           Configuration other than `defcfg`.
33           ${upstreamDoc}
34         '';
35       };
36       extraDefCfg = lib.mkOption {
37         type = lib.types.lines;
38         default = "";
39         example = "danger-enable-cmd yes";
40         description = ''
41           Configuration of `defcfg` other than `linux-dev` (generated
42           from the devices option) and
43           `linux-continue-if-no-devs-found` (hardcoded to be yes).
45           ${upstreamDoc}
46         '';
47       };
48       configFile = lib.mkOption {
49         type = lib.types.path;
50         default = mkConfig name config;
51         defaultText =
52           "A config file generated by values from other kanata module options.";
53         description = ''
54           The config file.
56           By default, it is generated by values from other kanata
57           module options.
59           You can also set it to your own full config file which
60           overrides all other kanata module options.  ${upstreamDoc}
61         '';
62       };
63       extraArgs = lib.mkOption {
64         type = lib.types.listOf lib.types.str;
65         default = [ ];
66         description = "Extra command line arguments passed to kanata.";
67       };
68       port = lib.mkOption {
69         type = lib.types.nullOr lib.types.port;
70         default = null;
71         example = 6666;
72         description = ''
73           Port to run the TCP server on. `null` will not run the server.
74         '';
75       };
76     };
77   };
79   mkName = name: "kanata-${name}";
81   mkDevices = devices:
82     let
83       devicesString = lib.pipe devices [
84         (map (device: "\"" + device + "\""))
85         (lib.concatStringsSep " ")
86       ];
87     in
88     lib.optionalString ((lib.length devices) > 0) "linux-dev (${devicesString})";
90   mkConfig = name: keyboard: pkgs.writeTextFile {
91     name = "${mkName name}-config.kdb";
92     text = ''
93       (defcfg
94         ${keyboard.extraDefCfg}
95         ${mkDevices keyboard.devices}
96         linux-continue-if-no-devs-found yes)
98       ${keyboard.config}
99     '';
100     # Only the config file generated by this module is checked.  A
101     # user-provided one is not checked because it may not be available
102     # at build time.  I think this is a good balance between module
103     # complexity and functionality.
104     checkPhase = ''
105       ${lib.getExe cfg.package} --cfg "$target" --check --debug
106     '';
107   };
109   mkService = name: keyboard: lib.nameValuePair (mkName name) {
110     wantedBy = [ "multi-user.target" ];
111     serviceConfig = {
112       Type = "notify";
113       ExecStart = ''
114         ${lib.getExe cfg.package} \
115           --cfg ${keyboard.configFile} \
116           --symlink-path ''${RUNTIME_DIRECTORY}/${name} \
117           ${lib.optionalString (keyboard.port != null) "--port ${toString keyboard.port}"} \
118           ${utils.escapeSystemdExecArgs keyboard.extraArgs}
119       '';
121       DynamicUser = true;
122       RuntimeDirectory = mkName name;
123       SupplementaryGroups = with config.users.groups; [
124         input.name
125         uinput.name
126       ];
128       # hardening
129       DeviceAllow = [
130         "/dev/uinput rw"
131         "char-input r"
132       ];
133       CapabilityBoundingSet = [ "" ];
134       DevicePolicy = "closed";
135       IPAddressAllow = lib.optional (keyboard.port != null) "localhost";
136       IPAddressDeny = [ "any" ];
137       LockPersonality = true;
138       MemoryDenyWriteExecute = true;
139       PrivateNetwork = keyboard.port == null;
140       PrivateUsers = true;
141       ProcSubset = "pid";
142       ProtectClock = true;
143       ProtectControlGroups = true;
144       ProtectHome = true;
145       ProtectHostname = true;
146       ProtectKernelLogs = true;
147       ProtectKernelModules = true;
148       ProtectKernelTunables = true;
149       ProtectProc = "invisible";
150       RestrictAddressFamilies = [ "AF_UNIX" ] ++ lib.optional (keyboard.port != null) "AF_INET";
151       RestrictNamespaces = true;
152       RestrictRealtime = true;
153       SystemCallArchitectures = [ "native" ];
154       SystemCallFilter = [
155         "@system-service"
156         "~@privileged"
157         "~@resources"
158       ];
159       UMask = "0077";
160     };
161   };
164   options.services.kanata = {
165     enable = lib.mkEnableOption "kanata, a tool to improve keyboard comfort and usability with advanced customization";
166     package = lib.mkPackageOption pkgs "kanata" {
167       example = [ "kanata-with-cmd" ];
168       extraDescription = ''
169         ::: {.note}
170         If {option}`danger-enable-cmd` is enabled in any of the keyboards, the
171         `kanata-with-cmd` package should be used.
172         :::
173       '';
174     };
175     keyboards = lib.mkOption {
176       type = lib.types.attrsOf (lib.types.submodule keyboard);
177       default = { };
178       description = "Keyboard configurations.";
179     };
180   };
182   config = lib.mkIf cfg.enable {
183     warnings =
184       let
185         keyboardsWithEmptyDevices = lib.filterAttrs (name: keyboard: keyboard.devices == [ ]) cfg.keyboards;
186         existEmptyDevices = lib.length (lib.attrNames keyboardsWithEmptyDevices) > 0;
187         moreThanOneKeyboard = lib.length (lib.attrNames cfg.keyboards) > 1;
188       in
189       lib.optional (existEmptyDevices && moreThanOneKeyboard) "One device can only be intercepted by one kanata instance.  Setting services.kanata.keyboards.${lib.head (lib.attrNames keyboardsWithEmptyDevices)}.devices = [ ] and using more than one services.kanata.keyboards may cause a race condition.";
191     hardware.uinput.enable = true;
193     systemd.services = lib.mapAttrs' mkService cfg.keyboards;
194   };
196   meta.maintainers = with lib.maintainers; [ linj ];