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