at 25.11-pre 8.1 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 utils, 6 ... 7}: 8 9let 10 cfg = config.services.kmonad; 11 12 # Per-keyboard options: 13 keyboard = 14 { name, ... }: 15 { 16 options = { 17 name = lib.mkOption { 18 type = lib.types.str; 19 default = name; 20 example = "laptop-internal"; 21 description = "Keyboard name."; 22 }; 23 24 device = lib.mkOption { 25 type = lib.types.path; 26 example = "/dev/input/by-id/some-dev"; 27 description = "Path to the keyboard's device file."; 28 }; 29 30 extraGroups = lib.mkOption { 31 type = lib.types.listOf lib.types.str; 32 default = [ ]; 33 description = '' 34 Extra permission groups to attach to the KMonad instance for 35 this keyboard. 36 37 Since KMonad runs as an unprivileged user, it may sometimes 38 need extra permissions in order to read the keyboard device 39 file. If your keyboard's device file isn't in the input 40 group, you'll need to list its group in this option. 41 ''; 42 }; 43 44 enableHardening = lib.mkOption { 45 type = lib.types.bool; 46 default = true; 47 example = false; 48 description = '' 49 Whether to enable systemd hardening. 50 51 ::: {.note} 52 If KMonad is used to execute shell commands, hardening may make some of them fail. 53 ::: 54 ''; 55 }; 56 57 defcfg = { 58 enable = lib.mkEnableOption '' 59 automatic generation of the defcfg block. 60 61 When this option is set to true, the config option for 62 this keyboard should not include a defcfg block 63 ''; 64 65 compose = { 66 key = lib.mkOption { 67 type = lib.types.nullOr lib.types.str; 68 default = "ralt"; 69 description = "The (optional) compose key to use."; 70 }; 71 72 delay = lib.mkOption { 73 type = lib.types.ints.unsigned; 74 default = 5; 75 description = "The delay (in milliseconds) between compose key sequences."; 76 }; 77 }; 78 79 fallthrough = lib.mkEnableOption "re-emitting unhandled key events"; 80 81 allowCommands = lib.mkEnableOption "keys to run shell commands"; 82 }; 83 84 config = lib.mkOption { 85 type = lib.types.lines; 86 description = "Keyboard configuration."; 87 }; 88 }; 89 }; 90 91 mkName = name: "kmonad-" + name; 92 93 # Create a complete KMonad configuration file: 94 mkCfg = 95 keyboard: 96 let 97 defcfg = '' 98 (defcfg 99 input (device-file "${keyboard.device}") 100 output (uinput-sink "${mkName keyboard.name}") 101 ${lib.optionalString (keyboard.defcfg.compose.key != null) '' 102 cmp-seq ${keyboard.defcfg.compose.key} 103 cmp-seq-delay ${toString keyboard.defcfg.compose.delay} 104 ''} 105 fallthrough ${lib.boolToString keyboard.defcfg.fallthrough} 106 allow-cmd ${lib.boolToString keyboard.defcfg.allowCommands} 107 ) 108 ''; 109 in 110 pkgs.writeTextFile { 111 name = "${mkName keyboard.name}.kbd"; 112 text = lib.optionalString keyboard.defcfg.enable (defcfg + "\n") + keyboard.config; 113 checkPhase = "${lib.getExe cfg.package} -d $out"; 114 }; 115 116 # Build a systemd path config that starts the service below when a 117 # keyboard device appears: 118 mkPath = 119 keyboard: 120 let 121 name = mkName keyboard.name; 122 in 123 lib.nameValuePair name { 124 description = "KMonad trigger for ${keyboard.device}"; 125 wantedBy = [ "paths.target" ]; 126 pathConfig = { 127 Unit = "${name}.service"; 128 PathExists = keyboard.device; 129 }; 130 }; 131 132 # Build a systemd service that starts KMonad: 133 mkService = 134 keyboard: 135 lib.nameValuePair (mkName keyboard.name) { 136 description = "KMonad for ${keyboard.device}"; 137 unitConfig = { 138 # Control rate limiting. 139 # Stop the restart logic if we restart more than 140 # StartLimitBurst times in a period of StartLimitIntervalSec. 141 StartLimitIntervalSec = 2; 142 StartLimitBurst = 5; 143 }; 144 serviceConfig = 145 { 146 ExecStart = '' 147 ${lib.getExe cfg.package} ${mkCfg keyboard} \ 148 ${utils.escapeSystemdExecArgs cfg.extraArgs} 149 ''; 150 Restart = "always"; 151 # Restart at increasing intervals from 2s to 1m 152 RestartSec = 2; 153 RestartSteps = 30; 154 RestartMaxDelaySec = "1min"; 155 Nice = -20; 156 DynamicUser = true; 157 User = "kmonad"; 158 Group = "kmonad"; 159 SupplementaryGroups = [ 160 # These ensure that our dynamic user has access to the device node 161 config.users.groups.input.name 162 config.users.groups.uinput.name 163 ] ++ keyboard.extraGroups; 164 } 165 // lib.optionalAttrs keyboard.enableHardening { 166 DeviceAllow = [ 167 "/dev/uinput w" 168 "char-input r" 169 ]; 170 CapabilityBoundingSet = [ "" ]; 171 DevicePolicy = "closed"; 172 IPAddressDeny = [ "any" ]; 173 LockPersonality = true; 174 MemoryDenyWriteExecute = true; 175 PrivateNetwork = true; 176 PrivateUsers = true; 177 ProcSubset = "pid"; 178 ProtectClock = true; 179 ProtectControlGroups = true; 180 ProtectHome = true; 181 ProtectHostname = true; 182 ProtectKernelLogs = true; 183 ProtectKernelModules = true; 184 ProtectKernelTunables = true; 185 ProtectProc = "invisible"; 186 RestrictAddressFamilies = [ "none" ]; 187 RestrictNamespaces = true; 188 RestrictRealtime = true; 189 SystemCallArchitectures = [ "native" ]; 190 SystemCallErrorNumber = "EPERM"; 191 SystemCallFilter = [ 192 "@system-service" 193 "~@privileged" 194 "~@resources" 195 ]; 196 UMask = "0077"; 197 }; 198 # make sure the new config is used after nixos-rebuild switch 199 # stopIfChanged controls[0] how a service is "restarted" during 200 # nixos-rebuild switch. By default, stopIfChanged is true, which stops 201 # the old service and then starts the new service after config updates. 202 # Since we use path-based activation[1] here, the service unit will 203 # immediately[2] be started by the path unit. Probably that start is 204 # before config updates, whcih causes the service unit to use the old 205 # config after nixos-rebuild switch. Setting stopIfChanged to false works 206 # around this issue by restarting the service after config updates. 207 # [0]: https://nixos.org/manual/nixos/unstable/#sec-switching-systems 208 # [1]: man 7 daemon 209 # [2]: man 5 systemd.path 210 stopIfChanged = false; 211 }; 212in 213{ 214 options.services.kmonad = { 215 enable = lib.mkEnableOption "KMonad: an advanced keyboard manager"; 216 217 package = lib.mkPackageOption pkgs "KMonad" { default = "kmonad"; }; 218 219 keyboards = lib.mkOption { 220 type = lib.types.attrsOf (lib.types.submodule keyboard); 221 default = { }; 222 description = "Keyboard configuration."; 223 }; 224 225 extraArgs = lib.mkOption { 226 type = lib.types.listOf lib.types.str; 227 default = [ ]; 228 example = [ 229 "--log-level" 230 "debug" 231 ]; 232 description = "Extra arguments to pass to KMonad."; 233 }; 234 }; 235 236 config = lib.mkIf cfg.enable { 237 hardware.uinput.enable = true; 238 239 services.udev.extraRules = 240 let 241 mkRule = name: '' 242 ACTION=="add", KERNEL=="event*", SUBSYSTEM=="input", ATTRS{name}=="${name}", ATTRS{id/product}=="5679", ATTRS{id/vendor}=="1235", SYMLINK+="input/by-id/${name}" 243 ''; 244 in 245 lib.foldlAttrs ( 246 rules: _: keyboard: 247 rules + "\n" + mkRule (mkName keyboard.name) 248 ) "" cfg.keyboards; 249 250 systemd = { 251 paths = lib.mapAttrs' (_: mkPath) cfg.keyboards; 252 services = lib.mapAttrs' (_: mkService) cfg.keyboards; 253 }; 254 }; 255 256 meta = { 257 maintainers = with lib.maintainers; [ 258 linj 259 rvdp 260 ]; 261 }; 262}