1{ config, lib, pkgs, utils, ... }:
2
3with lib;
4
5let
6 cfg = config.services.kanata;
7
8 keyboard = {
9 options = {
10 devices = mkOption {
11 type = types.listOf types.str;
12 example = [ "/dev/input/by-id/usb-0000_0000-event-kbd" ];
13 description = mdDoc "Paths to keyboard devices.";
14 };
15 config = mkOption {
16 type = types.lines;
17 example = ''
18 (defsrc
19 grv 1 2 3 4 5 6 7 8 9 0 - = bspc
20 tab q w e r t y u i o p [ ] \
21 caps a s d f g h j k l ; ' ret
22 lsft z x c v b n m , . / rsft
23 lctl lmet lalt spc ralt rmet rctl)
24
25 (deflayer qwerty
26 grv 1 2 3 4 5 6 7 8 9 0 - = bspc
27 tab q w e r t y u i o p [ ] \
28 @cap a s d f g h j k l ; ' ret
29 lsft z x c v b n m , . / rsft
30 lctl lmet lalt spc ralt rmet rctl)
31
32 (defalias
33 ;; tap within 100ms for capslk, hold more than 100ms for lctl
34 cap (tap-hold 100 100 caps lctl))
35 '';
36 description = mdDoc ''
37 Configuration other than `defcfg`.
38
39 See [example config files](https://github.com/jtroo/kanata)
40 for more information.
41 '';
42 };
43 extraDefCfg = mkOption {
44 type = types.lines;
45 default = "";
46 example = "danger-enable-cmd yes";
47 description = mdDoc ''
48 Configuration of `defcfg` other than `linux-dev` (generated
49 from the devices option) and
50 `linux-continue-if-no-devs-found` (hardcoded to be yes).
51
52 See [example config files](https://github.com/jtroo/kanata)
53 for more information.
54 '';
55 };
56 extraArgs = mkOption {
57 type = types.listOf types.str;
58 default = [ ];
59 description = mdDoc "Extra command line arguments passed to kanata.";
60 };
61 port = mkOption {
62 type = types.nullOr types.port;
63 default = null;
64 example = 6666;
65 description = mdDoc ''
66 Port to run the TCP server on. `null` will not run the server.
67 '';
68 };
69 };
70 };
71
72 mkName = name: "kanata-${name}";
73
74 mkDevices = devices:
75 optionalString ((length devices) > 0) "linux-dev ${concatStringsSep ":" devices}";
76
77 mkConfig = name: keyboard: pkgs.writeText "${mkName name}-config.kdb" ''
78 (defcfg
79 ${keyboard.extraDefCfg}
80 ${mkDevices keyboard.devices}
81 linux-continue-if-no-devs-found yes)
82
83 ${keyboard.config}
84 '';
85
86 mkService = name: keyboard: nameValuePair (mkName name) {
87 wantedBy = [ "multi-user.target" ];
88 serviceConfig = {
89 Type = "notify";
90 ExecStart = ''
91 ${getExe cfg.package} \
92 --cfg ${mkConfig name keyboard} \
93 --symlink-path ''${RUNTIME_DIRECTORY}/${name} \
94 ${optionalString (keyboard.port != null) "--port ${toString keyboard.port}"} \
95 ${utils.escapeSystemdExecArgs keyboard.extraArgs}
96 '';
97
98 DynamicUser = true;
99 RuntimeDirectory = mkName name;
100 SupplementaryGroups = with config.users.groups; [
101 input.name
102 uinput.name
103 ];
104
105 # hardening
106 DeviceAllow = [
107 "/dev/uinput rw"
108 "char-input r"
109 ];
110 CapabilityBoundingSet = [ "" ];
111 DevicePolicy = "closed";
112 IPAddressAllow = optional (keyboard.port != null) "localhost";
113 IPAddressDeny = [ "any" ];
114 LockPersonality = true;
115 MemoryDenyWriteExecute = true;
116 PrivateNetwork = keyboard.port == null;
117 PrivateUsers = true;
118 ProcSubset = "pid";
119 ProtectClock = true;
120 ProtectControlGroups = true;
121 ProtectHome = true;
122 ProtectHostname = true;
123 ProtectKernelLogs = true;
124 ProtectKernelModules = true;
125 ProtectKernelTunables = true;
126 ProtectProc = "invisible";
127 RestrictAddressFamilies = [ "AF_UNIX" ] ++ optional (keyboard.port != null) "AF_INET";
128 RestrictNamespaces = true;
129 RestrictRealtime = true;
130 SystemCallArchitectures = [ "native" ];
131 SystemCallFilter = [
132 "@system-service"
133 "~@privileged"
134 "~@resources"
135 ];
136 UMask = "0077";
137 };
138 };
139in
140{
141 options.services.kanata = {
142 enable = mkEnableOption (mdDoc "kanata");
143 package = mkOption {
144 type = types.package;
145 default = pkgs.kanata;
146 defaultText = literalExpression "pkgs.kanata";
147 example = literalExpression "pkgs.kanata-with-cmd";
148 description = mdDoc ''
149 The kanata package to use.
150
151 ::: {.note}
152 If `danger-enable-cmd` is enabled in any of the keyboards, the
153 `kanata-with-cmd` package should be used.
154 :::
155 '';
156 };
157 keyboards = mkOption {
158 type = types.attrsOf (types.submodule keyboard);
159 default = { };
160 description = mdDoc "Keyboard configurations.";
161 };
162 };
163
164 config = mkIf cfg.enable {
165 hardware.uinput.enable = true;
166
167 systemd.services = mapAttrs' mkService cfg.keyboards;
168 };
169
170 meta.maintainers = with maintainers; [ linj ];
171}