diff --git a/modules/modules.nix b/modules/modules.nix index 7d3b54043..9226ed63b 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -342,6 +342,7 @@ let ./services/linux-wallpaperengine.nix ./services/listenbrainz-mpd.nix ./services/lorri.nix + ./services/macos-remap-keys ./services/mako.nix ./services/mbsync.nix ./services/megasync.nix diff --git a/modules/services/macos-remap-keys/default.nix b/modules/services/macos-remap-keys/default.nix new file mode 100644 index 000000000..0c0d2575d --- /dev/null +++ b/modules/services/macos-remap-keys/default.nix @@ -0,0 +1,72 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.macos-remap-keys; + keytables = import ./keytables.nix { inherit lib; }; + + keyToHIDCode = table: key: keytables.${table}.${key}; + + # Note: hidutil requires HIDKeyboardModifierMapping values to be in hexadecimal + # format rather than decimal JSON. Using hex strings instead of numbers will + # crash macOS. + makeMapping = table: from: to: + '' + { "HIDKeyboardModifierMappingSrc": ${ + keyToHIDCode table from + }, "HIDKeyboardModifierMappingDst": ${keyToHIDCode table to} }''; + + makeMappingsList = table: mappings: + lib.mapAttrsToList (from: to: makeMapping table from to) mappings; + + allMappings = (makeMappingsList "keyboard" (cfg.keyboard or { })) + ++ (makeMappingsList "keypad" (cfg.keypad or { })); + + allMappingsString = lib.concatStringsSep ", " allMappings; + propertyString = ''{ "UserKeyMapping": [ ${allMappingsString} ] }''; +in { + meta.maintainers = [ lib.maintainers.WeetHet ]; + + options.services.macos-remap-keys = { + enable = lib.mkEnableOption "macOS key remapping service"; + + keyboard = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + example = { + Capslock = "Escape"; + SquareBracketOpen = "SquareBracketClose"; + }; + description = "Mapping of keyboard keys to remap"; + }; + + keypad = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + example = { + Enter = "Equal"; + Plus = "Minus"; + }; + description = "Mapping of keypad keys to remap"; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + (lib.hm.assertions.assertPlatform "services.macos-remap-keys" pkgs + lib.platforms.darwin) + ]; + home.activation.macosRemapKeys = + lib.hm.dag.entryAfter [ "writeBoundary" ] '' + run --silence /usr/bin/hidutil property --set '${propertyString}' + ''; + + launchd.agents.remap-keys = { + enable = true; + config = { + ProgramArguments = + [ "/usr/bin/hidutil" "property" "--set" propertyString ]; + KeepAlive.SuccessfulExit = false; + RunAtLoad = true; + }; + }; + }; +} diff --git a/modules/services/macos-remap-keys/keytables.nix b/modules/services/macos-remap-keys/keytables.nix new file mode 100644 index 000000000..a44bb4ece --- /dev/null +++ b/modules/services/macos-remap-keys/keytables.nix @@ -0,0 +1,139 @@ +{ lib }: +let + letters = let + alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + lettersList = lib.stringToCharacters alphabet; + indices = builtins.genList (i: i + 4) 26; + in lib.listToAttrs (lib.zipListsWith (letter: index: { + name = letter; + value = "0x${lib.toHexString index}"; + }) lettersList indices); + + numbers = { + One = "0x1E"; + Two = "0x1F"; + Three = "0x20"; + Four = "0x21"; + Five = "0x22"; + Six = "0x23"; + Seven = "0x24"; + Eight = "0x25"; + Nine = "0x26"; + Zero = "0x27"; + }; + + specialKeys = { + Enter = "0x28"; + Escape = "0x29"; + Backspace = "0x2A"; + Tab = "0x2B"; + Spacebar = "0x2C"; + Minus = "0x2D"; + Equal = "0x2E"; + SquareBracketOpen = "0x2F"; + SquareBracketClose = "0x30"; + Backslash = "0x31"; + Hash = "0x32"; + Semicolon = "0x33"; + SingleQuote = "0x34"; + GraveAccent = "0x35"; + Comma = "0x36"; + Dot = "0x37"; + Slash = "0x38"; + Capslock = "0x39"; + }; + + fKeys1To12 = { + F1 = "0x3A"; + F2 = "0x3B"; + F3 = "0x3C"; + F4 = "0x3D"; + F5 = "0x3E"; + F6 = "0x3F"; + F7 = "0x40"; + F8 = "0x41"; + F9 = "0x42"; + F10 = "0x43"; + F11 = "0x44"; + F12 = "0x45"; + }; + + fKeys13To24 = { + F13 = "0x68"; + F14 = "0x69"; + F15 = "0x6A"; + F16 = "0x6B"; + F17 = "0x6C"; + F18 = "0x6D"; + F19 = "0x6E"; + F20 = "0x6F"; + F21 = "0x70"; + F22 = "0x71"; + F23 = "0x72"; + F24 = "0x73"; + }; + + navigationKeys = { + PrintScreen = "0x46"; + ScrollLock = "0x47"; + Pause = "0x48"; + Insert = "0x49"; + Home = "0x4A"; + PageUp = "0x4B"; + ForwardDelete = "0x4C"; + End = "0x4D"; + PageDown = "0x4E"; + RightArrow = "0x4F"; + LeftArrow = "0x50"; + DownArrow = "0x51"; + UpArrow = "0x52"; + NumLock = "0x53"; + }; + + modifierKeys = { + Control = "0xE0"; + Shift = "0xE1"; + Option = "0xE2"; + Command = "0xE3"; + RightControl = "0xE4"; + RightShift = "0xE5"; + RightOption = "0xE6"; + RightCommand = "0xE7"; + }; + + keypadKeys = { + Slash = "0x54"; + Asterisk = "0x55"; + Minus = "0x56"; + Plus = "0x57"; + Enter = "0x58"; + One = "0x59"; + Two = "0x5A"; + Three = "0x5B"; + Four = "0x5C"; + Five = "0x5D"; + Six = "0x5E"; + Seven = "0x5F"; + Eight = "0x60"; + Nine = "0x61"; + Zero = "0x62"; + Dot = "0x63"; + BashSlash = "0x64"; + Application = "0x65"; + Power = "0x66"; + Equal = "0x67"; + }; + + mapToInt = keyPage: attrs: + lib.mapAttrs (name: value: + let keycode = lib.fromHexString (lib.removePrefix "0x" value); + in "0x${lib.toHexString (keyPage + keycode)}") attrs; + + page7Keys = mapToInt (lib.fromHexString "700000000") (letters // numbers + // specialKeys // fKeys1To12 // fKeys13To24 // navigationKeys + // modifierKeys); + pageFFKeys = mapToInt (lib.fromHexString "FF00000000") { Fn = "0x3"; }; +in { + keyboard = page7Keys // pageFFKeys; + keypad = mapToInt keypadKeys; +} diff --git a/tests/default.nix b/tests/default.nix index 94a993da4..05036d6a7 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -325,6 +325,7 @@ in import nmtSrc { ./modules/services/git-sync-darwin ./modules/services/imapnotify-darwin ./modules/services/nix-gc-darwin + ./modules/services/macos-remap-keys ./modules/services/ollama/darwin ./modules/services/yubikey-agent-darwin ./modules/targets-darwin diff --git a/tests/modules/services/macos-remap-keys/basic-agent.plist b/tests/modules/services/macos-remap-keys/basic-agent.plist new file mode 100644 index 000000000..b59f97d99 --- /dev/null +++ b/tests/modules/services/macos-remap-keys/basic-agent.plist @@ -0,0 +1,22 @@ + + + + + KeepAlive + + SuccessfulExit + + + Label + org.nix-community.home.remap-keys + ProgramArguments + + /usr/bin/hidutil + property + --set + { "UserKeyMapping": [ { "HIDKeyboardModifierMappingSrc": 0x700000039, "HIDKeyboardModifierMappingDst": 0x70000002A } ] } + + RunAtLoad + + + \ No newline at end of file diff --git a/tests/modules/services/macos-remap-keys/basic-configuration.nix b/tests/modules/services/macos-remap-keys/basic-configuration.nix new file mode 100644 index 000000000..7cc0b9018 --- /dev/null +++ b/tests/modules/services/macos-remap-keys/basic-configuration.nix @@ -0,0 +1,12 @@ +{ + services.macos-remap-keys = { + enable = true; + keyboard = { Capslock = "Backspace"; }; + }; + + nmt.script = '' + launchAgent=LaunchAgents/org.nix-community.home.remap-keys.plist + assertFileExists "$launchAgent" + assertFileContent "$launchAgent" ${./basic-agent.plist} + ''; +} diff --git a/tests/modules/services/macos-remap-keys/default.nix b/tests/modules/services/macos-remap-keys/default.nix new file mode 100644 index 000000000..796110f76 --- /dev/null +++ b/tests/modules/services/macos-remap-keys/default.nix @@ -0,0 +1 @@ +{ macos-remap-keys-basic-configuration = ./basic-configuration.nix; }