1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.keyd;
9
10 keyboardOptions =
11 { ... }:
12 {
13 options = {
14 ids = lib.mkOption {
15 type = lib.types.listOf lib.types.str;
16 default = [ "*" ];
17 example = [
18 "*"
19 "-0123:0456"
20 ];
21 description = ''
22 Device identifiers, as shown by {manpage}`keyd(1)`.
23 '';
24 };
25
26 settings = lib.mkOption {
27 type = (pkgs.formats.ini { }).type;
28 default = { };
29 example = {
30 main = {
31 capslock = "overload(control, esc)";
32 rightalt = "layer(rightalt)";
33 };
34
35 rightalt = {
36 j = "down";
37 k = "up";
38 h = "left";
39 l = "right";
40 };
41 };
42 description = ''
43 Configuration, except `ids` section, that is written to {file}`/etc/keyd/<keyboard>.conf`.
44 Appropriate names can be used to write non-alpha keys, for example "equal" instead of "=" sign (see <https://github.com/NixOS/nixpkgs/issues/236622>).
45 See <https://github.com/rvaiya/keyd> how to configure.
46 '';
47 };
48
49 extraConfig = lib.mkOption {
50 type = lib.types.lines;
51 default = "";
52 example = ''
53 [control+shift]
54 h = left
55 '';
56 description = ''
57 Extra configuration that is appended to the end of the file.
58 **Do not** write `ids` section here, use a separate option for it.
59 You can use this option to define compound layers that must always be defined after the layer they are comprised.
60 '';
61 };
62 };
63 };
64in
65{
66 imports = [
67 (lib.mkRemovedOptionModule [ "services" "keyd" "ids" ]
68 ''Use keyboards.<filename>.ids instead. If you don't need a multi-file configuration, just add keyboards.default before the ids. See https://github.com/NixOS/nixpkgs/pull/243271.''
69 )
70 (lib.mkRemovedOptionModule [ "services" "keyd" "settings" ]
71 ''Use keyboards.<filename>.settings instead. If you don't need a multi-file configuration, just add keyboards.default before the settings. See https://github.com/NixOS/nixpkgs/pull/243271.''
72 )
73 ];
74
75 options.services.keyd = {
76 enable = lib.mkEnableOption "keyd, a key remapping daemon";
77
78 keyboards = lib.mkOption {
79 type = lib.types.attrsOf (lib.types.submodule keyboardOptions);
80 default = { };
81 example = lib.literalExpression ''
82 {
83 default = {
84 ids = [ "*" ];
85 settings = {
86 main = {
87 capslock = "overload(control, esc)";
88 };
89 };
90 };
91 externalKeyboard = {
92 ids = [ "1ea7:0907" ];
93 settings = {
94 main = {
95 esc = capslock;
96 };
97 };
98 };
99 }
100 '';
101 description = ''
102 Configuration for one or more device IDs. Corresponding files in the /etc/keyd/ directory are created according to the name of the keys (like `default` or `externalKeyboard`).
103 '';
104 };
105 };
106
107 config = lib.mkIf cfg.enable {
108 # Creates separate files in the `/etc/keyd/` directory for each key in the dictionary
109 environment.etc = lib.mapAttrs' (
110 name: options:
111 lib.nameValuePair "keyd/${name}.conf" {
112 text = ''
113 [ids]
114 ${lib.concatStringsSep "\n" options.ids}
115
116 ${lib.generators.toINI { } options.settings}
117 ${options.extraConfig}
118 '';
119 }
120 ) cfg.keyboards;
121
122 hardware.uinput.enable = lib.mkDefault true;
123
124 systemd.services.keyd = {
125 description = "Keyd remapping daemon";
126 documentation = [ "man:keyd(1)" ];
127
128 wantedBy = [ "multi-user.target" ];
129
130 restartTriggers = lib.mapAttrsToList (
131 name: options: config.environment.etc."keyd/${name}.conf".source
132 ) cfg.keyboards;
133
134 # this is configurable in 2.4.2, later versions seem to remove this option.
135 # post-2.4.2 may need to set makeFlags in the derivation:
136 #
137 # makeFlags = [ "SOCKET_PATH/run/keyd/keyd.socket" ];
138 environment.KEYD_SOCKET = "/run/keyd/keyd.sock";
139
140 serviceConfig = {
141 ExecStart = "${pkgs.keyd}/bin/keyd";
142 Restart = "always";
143
144 # TODO investigate why it doesn't work propeprly with DynamicUser
145 # See issue: https://github.com/NixOS/nixpkgs/issues/226346
146 # DynamicUser = true;
147 SupplementaryGroups = [
148 config.users.groups.input.name
149 config.users.groups.uinput.name
150 ];
151
152 RuntimeDirectory = "keyd";
153
154 # Hardening
155 CapabilityBoundingSet = [ "CAP_SYS_NICE" ];
156 DeviceAllow = [
157 "char-input rw"
158 "/dev/uinput rw"
159 ];
160 ProtectClock = true;
161 PrivateNetwork = true;
162 ProtectHome = true;
163 ProtectHostname = true;
164 PrivateUsers = false;
165 PrivateMounts = true;
166 PrivateTmp = true;
167 RestrictNamespaces = true;
168 ProtectKernelLogs = true;
169 ProtectKernelModules = true;
170 ProtectKernelTunables = true;
171 ProtectControlGroups = true;
172 MemoryDenyWriteExecute = true;
173 RestrictRealtime = true;
174 LockPersonality = true;
175 ProtectProc = "invisible";
176 SystemCallFilter = [
177 "nice"
178 "@system-service"
179 "~@privileged"
180 ];
181 RestrictAddressFamilies = [ "AF_UNIX" ];
182 RestrictSUIDSGID = true;
183 IPAddressDeny = [ "any" ];
184 NoNewPrivileges = true;
185 ProtectSystem = "strict";
186 ProcSubset = "pid";
187 UMask = "0077";
188 };
189 };
190 };
191}