at 23.05-pre 6.8 kB view raw
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.addCheck (types.listOf types.str) 12 (devices: (length devices) > 0); 13 example = [ "/dev/input/by-id/usb-0000_0000-event-kbd" ]; 14 # TODO replace note with tip, which has not been implemented yet in 15 # nixos/lib/make-options-doc/mergeJSON.py 16 description = mdDoc '' 17 Paths to keyboard devices. 18 19 ::: {.note} 20 To avoid unnecessary triggers of the service unit, unplug devices in 21 the order of the list. 22 ::: 23 ''; 24 }; 25 config = mkOption { 26 type = types.lines; 27 example = '' 28 (defsrc 29 grv 1 2 3 4 5 6 7 8 9 0 - = bspc 30 tab q w e r t y u i o p [ ] \ 31 caps a s d f g h j k l ; ' ret 32 lsft z x c v b n m , . / rsft 33 lctl lmet lalt spc ralt rmet rctl) 34 35 (deflayer qwerty 36 grv 1 2 3 4 5 6 7 8 9 0 - = bspc 37 tab q w e r t y u i o p [ ] \ 38 @cap a s d f g h j k l ; ' ret 39 lsft z x c v b n m , . / rsft 40 lctl lmet lalt spc ralt rmet rctl) 41 42 (defalias 43 ;; tap within 100ms for capslk, hold more than 100ms for lctl 44 cap (tap-hold 100 100 caps lctl)) 45 ''; 46 description = mdDoc '' 47 Configuration other than `defcfg`. See [example config 48 files](https://github.com/jtroo/kanata) for more information. 49 ''; 50 }; 51 extraDefCfg = mkOption { 52 type = types.lines; 53 default = ""; 54 example = "danger-enable-cmd yes"; 55 description = mdDoc '' 56 Configuration of `defcfg` other than `linux-dev`. See [example 57 config files](https://github.com/jtroo/kanata) for more information. 58 ''; 59 }; 60 extraArgs = mkOption { 61 type = types.listOf types.str; 62 default = [ ]; 63 description = mdDoc "Extra command line arguments passed to kanata."; 64 }; 65 port = mkOption { 66 type = types.nullOr types.port; 67 default = null; 68 example = 6666; 69 description = mdDoc '' 70 Port to run the notification server on. `null` will not run the 71 server. 72 ''; 73 }; 74 }; 75 }; 76 77 mkName = name: "kanata-${name}"; 78 79 mkDevices = devices: concatStringsSep ":" devices; 80 81 mkConfig = name: keyboard: pkgs.writeText "${mkName name}-config.kdb" '' 82 (defcfg 83 ${keyboard.extraDefCfg} 84 linux-dev ${mkDevices keyboard.devices}) 85 86 ${keyboard.config} 87 ''; 88 89 mkService = name: keyboard: nameValuePair (mkName name) { 90 description = "kanata for ${mkDevices keyboard.devices}"; 91 92 # Because path units are used to activate service units, which 93 # will start the old stopped services during "nixos-rebuild 94 # switch", stopIfChanged here is a workaround to make sure new 95 # services are running after "nixos-rebuild switch". 96 stopIfChanged = false; 97 98 serviceConfig = { 99 ExecStart = '' 100 ${cfg.package}/bin/kanata \ 101 --cfg ${mkConfig name keyboard} \ 102 --symlink-path ''${RUNTIME_DIRECTORY}/${name} \ 103 ${optionalString (keyboard.port != null) "--port ${toString keyboard.port}"} \ 104 ${utils.escapeSystemdExecArgs keyboard.extraArgs} 105 ''; 106 107 DynamicUser = true; 108 RuntimeDirectory = mkName name; 109 SupplementaryGroups = with config.users.groups; [ 110 input.name 111 uinput.name 112 ]; 113 114 # hardening 115 DeviceAllow = [ 116 "/dev/uinput rw" 117 "char-input r" 118 ]; 119 CapabilityBoundingSet = [ "" ]; 120 DevicePolicy = "closed"; 121 IPAddressAllow = optional (keyboard.port != null) "localhost"; 122 IPAddressDeny = [ "any" ]; 123 LockPersonality = true; 124 MemoryDenyWriteExecute = true; 125 PrivateNetwork = keyboard.port == null; 126 PrivateUsers = true; 127 ProcSubset = "pid"; 128 ProtectClock = true; 129 ProtectControlGroups = true; 130 ProtectHome = true; 131 ProtectHostname = true; 132 ProtectKernelLogs = true; 133 ProtectKernelModules = true; 134 ProtectKernelTunables = true; 135 ProtectProc = "invisible"; 136 RestrictAddressFamilies = 137 if (keyboard.port == null) then "none" else [ "AF_INET" ]; 138 RestrictNamespaces = true; 139 RestrictRealtime = true; 140 SystemCallArchitectures = [ "native" ]; 141 SystemCallFilter = [ 142 "@system-service" 143 "~@privileged" 144 "~@resources" 145 ]; 146 UMask = "0077"; 147 }; 148 }; 149 150 mkPathName = i: name: "${mkName name}-${toString i}"; 151 152 mkPath = name: n: i: device: 153 nameValuePair (mkPathName i name) { 154 description = 155 "${toString (i+1)}/${toString n} kanata trigger for ${name}, watching ${device}"; 156 wantedBy = optional (i == 0) "multi-user.target"; 157 pathConfig = { 158 PathExists = device; 159 # (ab)use systemd.path to construct a trigger chain so that the 160 # service unit is only started when all paths exist 161 # however, manual of systemd.path says Unit's suffix is not ".path" 162 Unit = 163 if (i + 1) == n 164 then "${mkName name}.service" 165 else "${mkPathName (i + 1) name}.path"; 166 }; 167 unitConfig.StopPropagatedFrom = optional (i > 0) "${mkName name}.service"; 168 }; 169 170 mkPaths = name: keyboard: 171 let 172 n = length keyboard.devices; 173 in 174 imap0 (mkPath name n) keyboard.devices 175 ; 176in 177{ 178 options.services.kanata = { 179 enable = mkEnableOption (lib.mdDoc "kanata"); 180 package = mkOption { 181 type = types.package; 182 default = pkgs.kanata; 183 defaultText = literalExpression "pkgs.kanata"; 184 example = literalExpression "pkgs.kanata-with-cmd"; 185 description = mdDoc '' 186 The kanata package to use. 187 188 ::: {.note} 189 If `danger-enable-cmd` is enabled in any of the keyboards, the 190 `kanata-with-cmd` package should be used. 191 ::: 192 ''; 193 }; 194 keyboards = mkOption { 195 type = types.attrsOf (types.submodule keyboard); 196 default = { }; 197 description = mdDoc "Keyboard configurations."; 198 }; 199 }; 200 201 config = mkIf cfg.enable { 202 hardware.uinput.enable = true; 203 204 systemd = { 205 paths = trivial.pipe cfg.keyboards [ 206 (mapAttrsToList mkPaths) 207 concatLists 208 listToAttrs 209 ]; 210 services = mapAttrs' mkService cfg.keyboards; 211 }; 212 }; 213 214 meta.maintainers = with maintainers; [ linj ]; 215}