1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.evremap;
9 format = pkgs.formats.toml { };
10 settings = lib.attrsets.filterAttrs (n: v: v != null) cfg.settings;
11 configFile = format.generate "evremap.toml" settings;
12
13 key = lib.types.strMatching "(BTN|KEY)_[[:upper:][:digit:]_]+" // {
14 description = "key ID prefixed with BTN_ or KEY_";
15 };
16
17 mkKeyOption =
18 description:
19 lib.mkOption {
20 type = key;
21 description = ''
22 ${description}
23
24 You can get a list of keys by running `evremap list-keys`.
25 '';
26 };
27 mkKeySeqOption =
28 description:
29 (mkKeyOption description)
30 // {
31 type = lib.types.listOf key;
32 };
33
34 dualRoleModule = lib.types.submodule {
35 options = {
36 input = mkKeyOption "The key that should be remapped.";
37 hold = mkKeySeqOption "The key sequence that should be output when the input key is held.";
38 tap = mkKeySeqOption "The key sequence that should be output when the input key is tapped.";
39 };
40 };
41
42 remapModule = lib.types.submodule {
43 options = {
44 input = mkKeySeqOption "The key sequence that should be remapped.";
45 output = mkKeySeqOption "The key sequence that should be output when the input sequence is entered.";
46 };
47 };
48in
49{
50 options.services.evremap = {
51 enable = lib.mkEnableOption "evremap, a keyboard input remapper for Linux/Wayland systems";
52
53 settings = lib.mkOption {
54 type = lib.types.submodule {
55 freeformType = format.type;
56
57 options = {
58 device_name = lib.mkOption {
59 type = lib.types.str;
60 example = "AT Translated Set 2 keyboard";
61 description = ''
62 The name of the device that should be remapped.
63
64 You can get a list of devices by running `evremap list-devices` with elevated permissions.
65 '';
66 };
67
68 phys = lib.mkOption {
69 type = lib.types.nullOr lib.types.str;
70 default = null;
71 example = "usb-0000:07:00.3-2.1.1/input0";
72 description = ''
73 The physical device name to listen on.
74
75 This attribute may be specified to disambiguate multiple devices with the same device name.
76 The physical device names of each device can be obtained by running `evremap list-devices` with elevated permissions.
77 '';
78 };
79
80 dual_role = lib.mkOption {
81 type = lib.types.listOf dualRoleModule;
82 default = [ ];
83 example = [
84 {
85 input = "KEY_CAPSLOCK";
86 hold = [ "KEY_LEFTCTRL" ];
87 tap = [ "KEY_ESC" ];
88 }
89 ];
90 description = ''
91 List of dual-role remappings that output different key sequences based on whether the
92 input key is held or tapped.
93 '';
94 };
95
96 remap = lib.mkOption {
97 type = lib.types.listOf remapModule;
98 default = [ ];
99 example = [
100 {
101 input = [
102 "KEY_LEFTALT"
103 "KEY_UP"
104 ];
105 output = [ "KEY_PAGEUP" ];
106 }
107 ];
108 description = ''
109 List of remappings.
110 '';
111 };
112 };
113 };
114
115 description = ''
116 Settings for evremap.
117
118 See the [upstream documentation](https://github.com/wez/evremap/blob/master/README.md#configuration)
119 for how to configure evremap.
120 '';
121 default = { };
122 };
123 };
124
125 config = lib.mkIf cfg.enable {
126 environment.systemPackages = [ pkgs.evremap ];
127
128 hardware.uinput.enable = true;
129
130 systemd.services.evremap = {
131 description = "evremap - keyboard input remapper";
132 wantedBy = [ "multi-user.target" ];
133
134 serviceConfig = {
135 ExecStart = "${lib.getExe pkgs.evremap} remap ${configFile}";
136
137 DynamicUser = true;
138 User = "evremap";
139 SupplementaryGroups = [
140 config.users.groups.input.name
141 config.users.groups.uinput.name
142 ];
143 Restart = "on-failure";
144 RestartSec = 5;
145 TimeoutSec = 20;
146
147 # Hardening
148 ProtectClock = true;
149 ProtectKernelLogs = true;
150 ProtectControlGroups = true;
151 ProtectKernelModules = true;
152 ProtectHostname = true;
153 ProtectKernelTunables = true;
154 ProtectProc = "invisible";
155 ProtectHome = true;
156 ProcSubset = "pid";
157
158 PrivateTmp = true;
159 PrivateNetwork = true;
160 PrivateUsers = true;
161
162 RestrictRealtime = true;
163 RestrictNamespaces = true;
164 RestrictAddressFamilies = "none";
165
166 MemoryDenyWriteExecute = true;
167 LockPersonality = true;
168 IPAddressDeny = "any";
169 AmbientCapabilities = "";
170 CapabilityBoundingSet = "";
171 SystemCallArchitectures = "native";
172 SystemCallFilter = [
173 "@system-service"
174 "~@resources"
175 "~@privileged"
176 ];
177 UMask = "0027";
178 };
179 };
180 };
181}