1 { config, lib, pkgs, utils, ... }:
3 cfg = config.services.kanata;
5 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.";
7 keyboard = { name, config, ... }: {
9 devices = lib.mkOption {
10 type = lib.types.listOf lib.types.str;
12 example = [ "/dev/input/by-id/usb-0000_0000-event-kbd" ];
14 Paths to keyboard devices.
16 An empty list, the default value, lets kanata detect which
17 input devices are keyboards and intercept them all.
20 config = lib.mkOption {
21 type = lib.types.lines;
26 (deflayermap (default-layer)
27 ;; tap caps lock as caps lock, hold caps lock as left control
28 caps (tap-hold 100 100 caps lctl))
31 Configuration other than `defcfg`.
36 extraDefCfg = lib.mkOption {
37 type = lib.types.lines;
39 example = "danger-enable-cmd yes";
41 Configuration of `defcfg` other than `linux-dev` (generated
42 from the devices option) and
43 `linux-continue-if-no-devs-found` (hardcoded to be yes).
48 configFile = lib.mkOption {
49 type = lib.types.path;
50 default = mkConfig name config;
52 "A config file generated by values from other kanata module options.";
56 By default, it is generated by values from other kanata
59 You can also set it to your own full config file which
60 overrides all other kanata module options. ${upstreamDoc}
63 extraArgs = lib.mkOption {
64 type = lib.types.listOf lib.types.str;
66 description = "Extra command line arguments passed to kanata.";
69 type = lib.types.nullOr lib.types.port;
73 Port to run the TCP server on. `null` will not run the server.
79 mkName = name: "kanata-${name}";
83 devicesString = lib.pipe devices [
84 (map (device: "\"" + device + "\""))
85 (lib.concatStringsSep " ")
88 lib.optionalString ((lib.length devices) > 0) "linux-dev (${devicesString})";
90 mkConfig = name: keyboard: pkgs.writeTextFile {
91 name = "${mkName name}-config.kdb";
94 ${keyboard.extraDefCfg}
95 ${mkDevices keyboard.devices}
96 linux-continue-if-no-devs-found yes)
100 # Only the config file generated by this module is checked. A
101 # user-provided one is not checked because it may not be available
102 # at build time. I think this is a good balance between module
103 # complexity and functionality.
105 ${lib.getExe cfg.package} --cfg "$target" --check --debug
109 mkService = name: keyboard: lib.nameValuePair (mkName name) {
110 wantedBy = [ "multi-user.target" ];
114 ${lib.getExe cfg.package} \
115 --cfg ${keyboard.configFile} \
116 --symlink-path ''${RUNTIME_DIRECTORY}/${name} \
117 ${lib.optionalString (keyboard.port != null) "--port ${toString keyboard.port}"} \
118 ${utils.escapeSystemdExecArgs keyboard.extraArgs}
122 RuntimeDirectory = mkName name;
123 SupplementaryGroups = with config.users.groups; [
133 CapabilityBoundingSet = [ "" ];
134 DevicePolicy = "closed";
135 IPAddressAllow = lib.optional (keyboard.port != null) "localhost";
136 IPAddressDeny = [ "any" ];
137 LockPersonality = true;
138 MemoryDenyWriteExecute = true;
139 PrivateNetwork = keyboard.port == null;
143 ProtectControlGroups = true;
145 ProtectHostname = true;
146 ProtectKernelLogs = true;
147 ProtectKernelModules = true;
148 ProtectKernelTunables = true;
149 ProtectProc = "invisible";
150 RestrictAddressFamilies = [ "AF_UNIX" ] ++ lib.optional (keyboard.port != null) "AF_INET";
151 RestrictNamespaces = true;
152 RestrictRealtime = true;
153 SystemCallArchitectures = [ "native" ];
164 options.services.kanata = {
165 enable = lib.mkEnableOption "kanata, a tool to improve keyboard comfort and usability with advanced customization";
166 package = lib.mkPackageOption pkgs "kanata" {
167 example = [ "kanata-with-cmd" ];
168 extraDescription = ''
170 If {option}`danger-enable-cmd` is enabled in any of the keyboards, the
171 `kanata-with-cmd` package should be used.
175 keyboards = lib.mkOption {
176 type = lib.types.attrsOf (lib.types.submodule keyboard);
178 description = "Keyboard configurations.";
182 config = lib.mkIf cfg.enable {
185 keyboardsWithEmptyDevices = lib.filterAttrs (name: keyboard: keyboard.devices == [ ]) cfg.keyboards;
186 existEmptyDevices = lib.length (lib.attrNames keyboardsWithEmptyDevices) > 0;
187 moreThanOneKeyboard = lib.length (lib.attrNames cfg.keyboards) > 1;
189 lib.optional (existEmptyDevices && moreThanOneKeyboard) "One device can only be intercepted by one kanata instance. Setting services.kanata.keyboards.${lib.head (lib.attrNames keyboardsWithEmptyDevices)}.devices = [ ] and using more than one services.kanata.keyboards may cause a race condition.";
191 hardware.uinput.enable = true;
193 systemd.services = lib.mapAttrs' mkService cfg.keyboards;
196 meta.maintainers = with lib.maintainers; [ linj ];