Skip to content

Instantly share code, notes, and snippets.

@eqyiel
Created April 17, 2022 01:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eqyiel/412bc7acb68d1d15826893989f84fca7 to your computer and use it in GitHub Desktop.
Save eqyiel/412bc7acb68d1d15826893989f84fca7 to your computer and use it in GitHub Desktop.
{ 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 { };
};
}
# 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
}
{ 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 ];
};
}
@eqyiel
Copy link
Author

eqyiel commented Apr 17, 2022

The configuration I use with this

{
  # ...

  services.local-modules.nix-darwin.keyboard.enableKeyMapping = true;
  services.local-modules.nix-darwin.keyboard.mappings =
    let
      macbookKeyMappings = {
        "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";
      };
    in
    [
      {
        productId = 273;
        vendorId = 2131;
        mappings = {
          # Bind Caps Lock to Left Function for Realforce 87u.
          "Keyboard Caps Lock" = "Keyboard Left Function (fn)";
        };
      }
  
      {
        # 0x27e = 638
        # MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
        productId = 638;
        vendorId = 1452;
        mappings = macbookKeyMappings;
      }
  
      {
        # Different MacBook, with a different internal keyboard ID: 0x27b = 635
        # MacBook Pro (13-inch, 2018, Four Thunderbolt 3 Ports)
        productId = 635;
        vendorId = 1452;
        mappings = macbookKeyMappings;
      }
    ];

  # ...
}

@aisamu
Copy link

aisamu commented May 10, 2022

Thank you! ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment