Created
April 17, 2022 01:49
-
-
Save eqyiel/412bc7acb68d1d15826893989f84fca7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ config, lib, pkgs, ... }: | |
with lib; | |
let | |
cfg = attrByPath [ "services" "local-modules" "nix-darwin" "keyboard" ] { } config; | |
globalKeyMappings = { } | |
// (if cfg.remapCapsLockToControl then { "Keyboard Caps Lock" = "Keyboard Left Control"; } else { }) | |
// (if cfg.remapCapsLockToEscape then { "Keyboard Caps Lock" = "Keyboard Escape"; } else { }) | |
// (if cfg.nonUS.remapTilde then { "Keyboard Non-US # and ~" = "Keyboard Grave Accent and Tilde"; } else { }); | |
keyMappingTable = ( | |
mapAttrs | |
(name: value: | |
# hidutil accepts values that consists of 0x700000000 binary ORed with the | |
# desired keyboard usage value. | |
# | |
# The actual number can be base-10 or hexadecimal. | |
# 0x700000000 | |
# | |
# 30064771072 == 0x700000000 | |
# | |
# https://developer.apple.com/library/archive/technotes/tn2450/_index.html | |
bitOr 30064771072 value) | |
(import ./hid-usage-table.nix) | |
) // { | |
# These are not documented, but they work with hidutil. | |
# | |
# Sources: | |
# https://apple.stackexchange.com/a/396863/383501 | |
# http://www.neko.ne.jp/~freewing/software/macos_keyboard_setting_terminal_commandline/ | |
"Keyboard Left Function (fn)" = 1095216660483; | |
"Keyboard Right Function (fn)" = 280379760050179; | |
}; | |
keyMappingTableKeys = attrNames keyMappingTable; | |
isValidKeyMapping = key: elem key keyMappingTableKeys; | |
xpc_set_event_stream_handler = | |
pkgs.callPackage | |
./xpc_set_event_stream_handler.nix | |
{ | |
inherit (pkgs.darwin.apple_sdk.frameworks) Foundation; | |
}; | |
mappingOptions = types.submodule { | |
options = { | |
productId = mkOption { | |
type = types.int; | |
description = ''; | |
Product ID of the keyboard which should have this mapping applied. To find the Product ID of a keyboard, you can check the output of <literal>hidutil list --matching keyboard</literal>. | |
Note that you have to convert the value from hexadecimal to decimal because Nix only has base 10 integers. For example: <literal>printf "%d" 0x27e</literal> | |
''; | |
}; | |
vendorId = mkOption { | |
type = types.int; | |
description = ''; | |
Vendor ID of the keyboard which should have this mapping applied. To find the Vendor ID of a keyboard, you can check the output of <literal>hidutil list --matching keyboard</literal>. | |
Note that you have to convert the value from hexadecimal to decimal because Nix only has base 10 integers. For example: <literal>printf "%d" 0x5ac</literal> | |
''; | |
}; | |
mappings = mkOption { | |
type = types.attrsOf (types.enum keyMappingTableKeys); | |
description = '' | |
Mappings that should be applied. To see what values are available, check <link xlink:href="https://github.com/LnL7/nix-darwin/blob/master/modules/system/keyboard/hid-usage-table.nix"/>. | |
''; | |
}; | |
}; | |
}; | |
in | |
{ | |
options = { | |
services.local-modules.nix-darwin.keyboard.enableKeyMapping = mkOption { | |
type = types.bool; | |
default = false; | |
description = "Whether to enable keyboard mappings."; | |
}; | |
services.local-modules.nix-darwin.keyboard.remapCapsLockToControl = mkOption { | |
type = types.bool; | |
default = false; | |
description = "Whether to remap the Caps Lock key to Control."; | |
}; | |
services.local-modules.nix-darwin.keyboard.remapCapsLockToEscape = mkOption { | |
type = types.bool; | |
default = false; | |
description = "Whether to remap the Caps Lock key to Escape."; | |
}; | |
services.local-modules.nix-darwin.keyboard.nonUS.remapTilde = mkOption { | |
type = types.bool; | |
default = false; | |
description = "Whether to remap the Tilde key on non-us keyboards."; | |
}; | |
services.local-modules.nix-darwin.keyboard.mappings = mkOption { | |
type = types.nullOr (types.either mappingOptions (types.listOf mappingOptions)); | |
default = null; | |
description = '' | |
Either an attribute set of key mappings (that will be applied to all keyboards), or a list of attribute sets of key mappings (mappings that will be applied to keyboards with specific Product IDs). | |
''; | |
example = literalExample '' | |
services.local-modules.nix-darwin.keyboard.enableKeyMapping = true; | |
services.local-modules.nix-darwin.keyboard.mappings = [ | |
{ | |
productId = 273; | |
vendorId = 2131; | |
mappings = { | |
"Keyboard Caps Lock" = "Keyboard Left Function (fn)"; | |
}; | |
} | |
{ | |
productId = 638; | |
vendorId = 1452; | |
mappings = { | |
# For the built-in MacBook keyboard, change the modifiers to match a | |
# traditional keyboard layout. | |
"Keyboard Caps Lock" = "Keyboard Left Function (fn)"; | |
"Keyboard Left Alt" = "Keyboard Left GUI"; | |
"Keyboard Left Function (fn)" = "Keyboard Left Control"; | |
"Keyboard Left GUI" = "Keyboard Left Alt"; | |
"Keyboard Right Alt" = "Keyboard Right Control"; | |
"Keyboard Right GUI" = "Keyboard Right Alt"; | |
}; | |
} | |
]; | |
''; | |
}; | |
}; | |
config = { | |
assertions = | |
let | |
mkAssertion = { element, productId ? null, ... }: { | |
assertion = isValidKeyMapping element; | |
message = "${element} ${if productId != null then "in mapping for ${productId}" else ""} must be one of ${builtins.toJSON keyMappingTableKeys}"; | |
}; | |
in | |
( | |
flatten | |
(optionals (cfg.mappings != null) | |
( | |
map | |
({ productId, mappings, ... }: | |
(mapAttrsToList | |
(src: dest: [ | |
(mkAssertion { inherit productId; element = src; }) | |
(mkAssertion { inherit productId; element = dest; }) | |
]) | |
mappings | |
) | |
) | |
cfg.mappings | |
) ++ ( | |
mapAttrsToList | |
(src: dest: [ | |
(mkAssertion { element = src; }) | |
(mkAssertion { element = dest; }) | |
]) | |
globalKeyMappings | |
)) ++ [ | |
{ | |
assertion = !(cfg.mappings != null && (length (attrNames globalKeyMappings) > 0)); | |
message = "Configuring both global and device-specific key mappings is not reliable, please use one or the other."; | |
} | |
] | |
); | |
warnings = [ ] | |
++ ( | |
optional | |
(!cfg.enableKeyMapping && (cfg.mappings != null || globalKeyMappings != { })) | |
"services.local-modules.nix-darwin.keyboard.enableKeyMapping is false, keyboard mappings will not be configured." | |
) | |
++ ( | |
optional | |
(cfg.enableKeyMapping && (cfg.mappings == null && globalKeyMappings == { })) | |
"services.local-modules.nix-darwin.keyboard.enableKeyMapping is true but you have not configured any key mappings." | |
); | |
launchd.user.agents = | |
let | |
mkUserKeyMapping = mapping: builtins.toJSON ({ | |
UserKeyMapping = ( | |
mapAttrsToList | |
(src: dst: { | |
HIDKeyboardModifierMappingSrc = keyMappingTable."${src}"; | |
HIDKeyboardModifierMappingDst = keyMappingTable."${dst}"; | |
}) | |
mapping | |
); | |
}); | |
in | |
if (cfg.enableKeyMapping && length (attrNames globalKeyMappings) > 0) then | |
{ | |
keyboard = ({ | |
serviceConfig.ProgramArguments = [ | |
"${xpc_set_event_stream_handler}/bin/xpc_set_event_stream_handler" | |
"${pkgs.writeScriptBin "apply-keybindings" '' | |
#!${pkgs.stdenv.shell} | |
set -euo pipefail | |
echo "$(date) configuring keyboard..." >&2 | |
hidutil property --set '${mkUserKeyMapping globalKeyMappings}' > /dev/null | |
''}/bin/apply-keybindings" | |
]; | |
serviceConfig.LaunchEvents = { | |
"com.apple.iokit.matching" = { | |
"com.apple.usb.device" = { | |
IOMatchLaunchStream = true; | |
IOProviderClass = "IOUSBDevice"; | |
idProduct = "*"; | |
idVendor = "*"; | |
}; | |
}; | |
}; | |
serviceConfig.RunAtLoad = true; | |
}); | |
} | |
else if (cfg.enableKeyMapping && cfg.mappings != null) then | |
(listToAttrs (map | |
({ mappings | |
, productId | |
, vendorId | |
, ... | |
}: (nameValuePair "keyboard-${toString productId}" ({ | |
serviceConfig.ProgramArguments = [ | |
# Use xpc_set_event_stream_handler to mark this event as "consumed", | |
# otherwise it the script will never stop being called (something | |
# like every 10 seconds). | |
"${xpc_set_event_stream_handler}/bin/xpc_set_event_stream_handler" | |
"${pkgs.writeScriptBin "apply-keybindings" ( | |
let intToHexString = value: | |
pkgs.runCommand "${toString value}-to-hex-string" | |
{ } ''printf "%#0x" ${toString value} > $out''; in | |
'' | |
#!${pkgs.stdenv.shell} | |
set -euxo pipefail | |
# Sometimes it takes a moment for the keyboard to be | |
# visible to hidutil, even when the script is launched | |
# with "LaunchEvents". | |
function retry () { | |
local attempt=1 | |
local max_attempts=10 | |
local delay=0.2 | |
while true; do | |
"$@" && break || { | |
if (test $attempt -lt $max_attempts); then | |
attempt=$((attempt + 1)) | |
sleep $delay | |
else | |
exit 1 | |
fi | |
} | |
done | |
} | |
function get_vendor_id () { | |
hidutil list --matching keyboard | | |
awk '{ print $1 }' | | |
grep $(<${intToHexString vendorId}) | |
} | |
function get_product_id () { | |
hidutil list --matching keyboard | | |
awk '{ print $2 }' | | |
grep $(<${intToHexString productId}) | |
} | |
echo "$(date) configuring keyboard ${toString productId} ($(<${intToHexString productId}))..." >&2 | |
retry get_vendor_id | |
retry get_product_id | |
hidutil property --matching '${builtins.toJSON { ProductID = productId; }}' --set '${mkUserKeyMapping mappings}' > /dev/null | |
'' | |
)}/bin/apply-keybindings" | |
]; | |
serviceConfig.LaunchEvents = { | |
"com.apple.iokit.matching" = { | |
"com.apple.usb.device" = { | |
IOMatchLaunchStream = true; | |
IOProviderClass = "IOUSBDevice"; | |
idProduct = productId; | |
idVendor = vendorId; | |
}; | |
}; | |
}; | |
serviceConfig.RunAtLoad = true; | |
}) | |
)) | |
cfg.mappings | |
)) else { }; | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# These attributes are based on this table: | |
# https://developer.apple.com/library/archive/technotes/tn2450/_index.html | |
# | |
# There are more "usage IDs" in the specification[0] than cited there, but maybe | |
# not all of them are supported on macOS. | |
# | |
# [0] https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf | |
{ | |
"Keyboard ' and \"" = 52; # 0x34 | |
"Keyboard , and \"<\"" = 54; # 0x36 | |
"Keyboard - and _" = 45; # 0x2D | |
"Keyboard . and \">\"" = 55; # 0x37 | |
"Keyboard / and ?" = 56; # 0x38 | |
"Keyboard 0 and )" = 39; # 0x27 | |
"Keyboard 1 and !" = 30; # 0x1E | |
"Keyboard 2 and @" = 31; # 0x1F | |
"Keyboard 3 and #" = 32; # 0x20 | |
"Keyboard 4 and $" = 33; # 0x21 | |
"Keyboard 5 and %" = 34; # 0x22 | |
"Keyboard 6 and ^" = 35; # 0x23 | |
"Keyboard 7 and &" = 36; # 0x24 | |
"Keyboard 8 and *" = 37; # 0x25 | |
"Keyboard 9 and (" = 38; # 0x26 | |
"Keyboard ; and :" = 51; # 0x33 | |
"Keyboard = and +" = 46; # 0x2E | |
"Keyboard Application" = 101; # 0x65 | |
"Keyboard Caps Lock" = 57; # 0x39 | |
"Keyboard Delete (Backspace)" = 42; # 0x2A | |
"Keyboard Delete Forward" = 76; # 0x4C | |
"Keyboard Down Arrow" = 81; # 0x51 | |
"Keyboard End" = 77; # 0x4D | |
"Keyboard Escape" = 41; # 0x29 | |
"Keyboard F1" = 58; # 0x3A | |
"Keyboard F10" = 67; # 0x43 | |
"Keyboard F11" = 68; # 0x44 | |
"Keyboard F12" = 69; # 0x45 | |
"Keyboard F13" = 104; # 0x68 | |
"Keyboard F14" = 105; # 0x69 | |
"Keyboard F15" = 106; # 0x6A | |
"Keyboard F16" = 107; # 0x6B | |
"Keyboard F17" = 108; # 0x6C | |
"Keyboard F18" = 109; # 0x6D | |
"Keyboard F19" = 110; # 0x6E | |
"Keyboard F2" = 59; # 0x3B | |
"Keyboard F20" = 111; # 0x6F | |
"Keyboard F21" = 112; # 0x70 | |
"Keyboard F22" = 113; # 0x71 | |
"Keyboard F23" = 114; # 0x72 | |
"Keyboard F24" = 115; # 0x73 | |
"Keyboard F3" = 60; # 0x3C | |
"Keyboard F4" = 61; # 0x3D | |
"Keyboard F5" = 62; # 0x3E | |
"Keyboard F6" = 63; # 0x3F | |
"Keyboard F7" = 64; # 0x40 | |
"Keyboard F8" = 65; # 0x41 | |
"Keyboard F9" = 66; # 0x42 | |
"Keyboard Grave Accent and Tilde" = 53; # 0x35 | |
"Keyboard Home" = 74; # 0x4A | |
"Keyboard Insert" = 73; # 0x49 | |
"Keyboard Left Alt" = 226; # 0xE2 | |
"Keyboard Left Arrow" = 80; # 0x50 | |
"Keyboard Left Control" = 224; # 0xE0 | |
"Keyboard Left GUI" = 227; # 0xE3 | |
"Keyboard Left Shift" = 225; # 0xE1 | |
"Keyboard Non-US # and ~" = 50; # 0x32 | |
"Keyboard Non-US \ and |" = 100; # 0x64 | |
"Keyboard Page Down" = 78; # 0x4E | |
"Keyboard Page Up" = 75; # 0x4B | |
"Keyboard Pause" = 72; # 0x48 | |
"Keyboard Power" = 102; # 0x66 | |
"Keyboard Print Screen" = 70; # 0x46 | |
"Keyboard Return (Enter)" = 40; # 0x28 | |
"Keyboard Right Alt" = 230; # 0xE6 | |
"Keyboard Right Arrow" = 79; # 0x4F | |
"Keyboard Right Control" = 228; # 0xE4 | |
"Keyboard Right GUI" = 231; # 0xE7 | |
"Keyboard Right Shift" = 229; # 0xE5; | |
"Keyboard Scroll Lock" = 71; # 0x47 | |
"Keyboard Spacebar" = 44; # 0x2C | |
"Keyboard Tab" = 43; # 0x2B | |
"Keyboard Up Arrow" = 82; # 0x52 | |
"Keyboard [ and {" = 47; # 0x2F | |
"Keyboard \ and |" = 49; # 0x31 | |
"Keyboard ] and }" = 48; # 0x30 | |
"Keyboard a and A" = 4; # 0x04 | |
"Keyboard b and B" = 5; # 0x05 | |
"Keyboard c and C" = 6; # 0x06 | |
"Keyboard d and D" = 7; # 0x07 | |
"Keyboard e and E" = 8; # 0x08 | |
"Keyboard f and F" = 9; # 0x09 | |
"Keyboard g and G" = 10; # 0x0A | |
"Keyboard h and H" = 11; # 0x0B | |
"Keyboard i and I" = 12; # 0x0C | |
"Keyboard j and J" = 13; # 0x0D | |
"Keyboard k and K" = 14; # 0x0E | |
"Keyboard l and L" = 15; # 0x0F | |
"Keyboard m and M" = 16; # 0x10 | |
"Keyboard n and N" = 17; # 0x11 | |
"Keyboard o and O" = 18; # 0x12 | |
"Keyboard p and P" = 19; # 0x13 | |
"Keyboard q and Q" = 20; # 0x14 | |
"Keyboard r and R" = 21; # 0x15 | |
"Keyboard s and S" = 22; # 0x16 | |
"Keyboard t and T" = 23; # 0x17 | |
"Keyboard u and U" = 24; # 0x18 | |
"Keyboard v and V" = 25; # 0x19 | |
"Keyboard w and W" = 26; # 0x1A | |
"Keyboard x and X" = 27; # 0x1B | |
"Keyboard y and Y" = 28; # 0x1C | |
"Keyboard z and Z" = 29; # 0x1D | |
"Keypad *" = 85; # 0x55 | |
"Keypad +" = 87; # 0x57 | |
"Keypad -" = 86; # 0x56 | |
"Keypad . and Delete" = 99; # 0x63 | |
"Keypad /" = 84; # 0x54 | |
"Keypad 0 and Insert" = 98; # 0x62 | |
"Keypad 1 and End" = 89; # 0x59 | |
"Keypad 2 and Down Arrow" = 90; # 0x5A | |
"Keypad 3 and Page Down" = 91; # 0x5B | |
"Keypad 4 and Left Arrow" = 92; # 0x5C | |
"Keypad 5" = 93; # 0x5D | |
"Keypad 6 and Right Arrow" = 94; # 0x5E | |
"Keypad 7 and Home" = 95; # 95; # 0x5F | |
"Keypad 8 and Up Arrow" = 96; # 0x60 | |
"Keypad 9 and Page Up" = 97; # 0x61 | |
"Keypad =" = 103; # 0x67 | |
"Keypad Enter" = 88; # 0x58 | |
"Keypad Num Lock and Clear" = 83; # 0x53 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ stdenv | |
, Foundation | |
, fetchFromGitHub | |
, lib | |
, xcbuildHook | |
}: | |
let rev = "4bbfc25b485e444afcca8b9d5492ef0018c03823"; in | |
stdenv.mkDerivation { | |
name = "xpc_set_event_stream_handler"; | |
version = builtins.substring 1 7 rev; | |
src = fetchFromGitHub { | |
owner = "snosrap"; | |
repo = "xpc_set_event_stream_handler"; | |
inherit rev; | |
sha256 = "17vv5nacl56h59h3pmawab4cpk54xxg2cxvnijqid4lmvlz6nidq"; | |
}; | |
nativeBuildInputs = [ | |
xcbuildHook | |
Foundation | |
]; | |
installPhase = '' | |
mkdir -p $out/bin | |
cp Products/Release/xpc_set_event_stream_handler $out/bin | |
''; | |
meta = with lib; { | |
description = "Consume a com.apple.iokit.matching event, then run the executable specified in the first parameter."; | |
homepage = https://github.com/snosrap/xpc_set_event_stream_handler; | |
platforms = platforms.darwin; | |
license = [ licenses.mit ]; | |
maintainers = [ maintainers.eqyiel ]; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The configuration I use with this