at 24.11-pre 5.5 kB view raw
1{ config, lib, pkgs, utils, ... }: 2 3with lib; 4 5let 6 cfg = config.services.kanata; 7 8 upstreamDoc = "See [the upstream documentation](https://github.com/jtroo/kanata/blob/main/docs/config.adoc) and [example config files](https://github.com/jtroo/kanata/tree/main/cfg_samples) for more information."; 9 10 keyboard = { 11 options = { 12 devices = mkOption { 13 type = types.listOf types.str; 14 default = [ ]; 15 example = [ "/dev/input/by-id/usb-0000_0000-event-kbd" ]; 16 description = '' 17 Paths to keyboard devices. 18 19 An empty list, the default value, lets kanata detect which 20 input devices are keyboards and intercept them all. 21 ''; 22 }; 23 config = mkOption { 24 type = types.lines; 25 example = '' 26 (defsrc 27 caps) 28 29 (deflayermap (default-layer) 30 ;; tap caps lock as caps lock, hold caps lock as left control 31 caps (tap-hold 100 100 caps lctl)) 32 ''; 33 description = '' 34 Configuration other than `defcfg`. 35 36 ${upstreamDoc} 37 ''; 38 }; 39 extraDefCfg = mkOption { 40 type = types.lines; 41 default = ""; 42 example = "danger-enable-cmd yes"; 43 description = '' 44 Configuration of `defcfg` other than `linux-dev` (generated 45 from the devices option) and 46 `linux-continue-if-no-devs-found` (hardcoded to be yes). 47 48 ${upstreamDoc} 49 ''; 50 }; 51 extraArgs = mkOption { 52 type = types.listOf types.str; 53 default = [ ]; 54 description = "Extra command line arguments passed to kanata."; 55 }; 56 port = mkOption { 57 type = types.nullOr types.port; 58 default = null; 59 example = 6666; 60 description = '' 61 Port to run the TCP server on. `null` will not run the server. 62 ''; 63 }; 64 }; 65 }; 66 67 mkName = name: "kanata-${name}"; 68 69 mkDevices = devices: 70 let 71 devicesString = pipe devices [ 72 (map (device: "\"" + device + "\"")) 73 (concatStringsSep " ") 74 ]; 75 in 76 optionalString ((length devices) > 0) "linux-dev (${devicesString})"; 77 78 mkConfig = name: keyboard: pkgs.writeTextFile { 79 name = "${mkName name}-config.kdb"; 80 text = '' 81 (defcfg 82 ${keyboard.extraDefCfg} 83 ${mkDevices keyboard.devices} 84 linux-continue-if-no-devs-found yes) 85 86 ${keyboard.config} 87 ''; 88 checkPhase = '' 89 ${getExe cfg.package} --cfg "$target" --check --debug 90 ''; 91 }; 92 93 mkService = name: keyboard: nameValuePair (mkName name) { 94 wantedBy = [ "multi-user.target" ]; 95 serviceConfig = { 96 Type = "notify"; 97 ExecStart = '' 98 ${getExe cfg.package} \ 99 --cfg ${mkConfig name keyboard} \ 100 --symlink-path ''${RUNTIME_DIRECTORY}/${name} \ 101 ${optionalString (keyboard.port != null) "--port ${toString keyboard.port}"} \ 102 ${utils.escapeSystemdExecArgs keyboard.extraArgs} 103 ''; 104 105 DynamicUser = true; 106 RuntimeDirectory = mkName name; 107 SupplementaryGroups = with config.users.groups; [ 108 input.name 109 uinput.name 110 ]; 111 112 # hardening 113 DeviceAllow = [ 114 "/dev/uinput rw" 115 "char-input r" 116 ]; 117 CapabilityBoundingSet = [ "" ]; 118 DevicePolicy = "closed"; 119 IPAddressAllow = optional (keyboard.port != null) "localhost"; 120 IPAddressDeny = [ "any" ]; 121 LockPersonality = true; 122 MemoryDenyWriteExecute = true; 123 PrivateNetwork = keyboard.port == null; 124 PrivateUsers = true; 125 ProcSubset = "pid"; 126 ProtectClock = true; 127 ProtectControlGroups = true; 128 ProtectHome = true; 129 ProtectHostname = true; 130 ProtectKernelLogs = true; 131 ProtectKernelModules = true; 132 ProtectKernelTunables = true; 133 ProtectProc = "invisible"; 134 RestrictAddressFamilies = [ "AF_UNIX" ] ++ optional (keyboard.port != null) "AF_INET"; 135 RestrictNamespaces = true; 136 RestrictRealtime = true; 137 SystemCallArchitectures = [ "native" ]; 138 SystemCallFilter = [ 139 "@system-service" 140 "~@privileged" 141 "~@resources" 142 ]; 143 UMask = "0077"; 144 }; 145 }; 146in 147{ 148 options.services.kanata = { 149 enable = mkEnableOption "kanata, a tool to improve keyboard comfort and usability with advanced customization"; 150 package = mkPackageOption pkgs "kanata" { 151 example = [ "kanata-with-cmd" ]; 152 extraDescription = '' 153 ::: {.note} 154 If {option}`danger-enable-cmd` is enabled in any of the keyboards, the 155 `kanata-with-cmd` package should be used. 156 ::: 157 ''; 158 }; 159 keyboards = mkOption { 160 type = types.attrsOf (types.submodule keyboard); 161 default = { }; 162 description = "Keyboard configurations."; 163 }; 164 }; 165 166 config = mkIf cfg.enable { 167 warnings = 168 let 169 keyboardsWithEmptyDevices = filterAttrs (name: keyboard: keyboard.devices == [ ]) cfg.keyboards; 170 existEmptyDevices = length (attrNames keyboardsWithEmptyDevices) > 0; 171 moreThanOneKeyboard = length (attrNames cfg.keyboards) > 1; 172 in 173 optional (existEmptyDevices && moreThanOneKeyboard) "One device can only be intercepted by one kanata instance. Setting services.kanata.keyboards.${head (attrNames keyboardsWithEmptyDevices)}.devices = [ ] and using more than one services.kanata.keyboards may cause a race condition."; 174 175 hardware.uinput.enable = true; 176 177 systemd.services = mapAttrs' mkService cfg.keyboards; 178 }; 179 180 meta.maintainers = with maintainers; [ linj ]; 181}