main
1# Automatically inhibit (disable) the laptop's internal keyboard when an
2# external keyboard is connected, and re-enable it on disconnect.
3#
4# Uses bluetooth-monitor for Bluetooth keyboards (D-Bus events) and udev
5# rules for USB keyboards. Writes to the kernel's input device `inhibited`
6# sysfs attribute (Linux 5.11+).
7#
8# Usage:
9# services.laptop-keyboard-inhibit = {
10# enable = true;
11# # Match by device name (uses bluetooth-monitor for BT detection)
12# keyboards = [ "Eyelash Corne" ];
13# # Or match USB keyboards by vendor:product ID (uses udev)
14# usbKeyboards = [ { vendor = "3297"; product = "1969"; } ];
15# };
16{
17 config,
18 lib,
19 pkgs,
20 ...
21}:
22let
23 inherit (lib)
24 mkEnableOption
25 mkIf
26 mkOption
27 types
28 concatMapStringsSep
29 ;
30 cfg = config.services.laptop-keyboard-inhibit;
31
32 # Script that finds and inhibits/uninhibits the internal keyboard
33 # Searches for keyboards by name pattern (works for both PS/2 and USB internal keyboards)
34 toggleScript = pkgs.writeShellScript "toggle-internal-kbd" ''
35 ACTION="$1" # "inhibit" or "uninhibit"
36
37 VALUE=1
38 if [ "$ACTION" = "uninhibit" ]; then
39 VALUE=0
40 fi
41
42 # Find internal keyboard by common name patterns
43 # Matches: "AT Translated Set 2 keyboard", "Asus Keyboard", etc.
44 for input_dir in /sys/class/input/input*/; do
45 if [ ! -f "$input_dir/name" ]; then
46 continue
47 fi
48
49 name=$(cat "$input_dir/name")
50
51 # Match internal laptop keyboards (not external devices)
52 if echo "$name" | grep -qiE "(AT Translated|Asus Keyboard|Internal Keyboard|Laptop Keyboard)"; then
53 inhibit_file="$input_dir/inhibited"
54 if [ -f "$inhibit_file" ]; then
55 echo "$VALUE" > "$inhibit_file" 2>/dev/null && \
56 echo "[$ACTION] $name" || true
57 fi
58 fi
59 done
60 '';
61
62 # Generate udev rules for USB vendor:product matching
63 usbRules = concatMapStringsSep "\n" (kb: ''
64 # Inhibit internal keyboard when USB keyboard ${kb.vendor}:${kb.product} connects
65 ACTION=="add", SUBSYSTEM=="input", ATTR{id/vendor}=="${kb.vendor}", ATTR{id/product}=="${kb.product}", ATTR{name}!="", RUN+="${toggleScript} inhibit"
66 ACTION=="remove", SUBSYSTEM=="input", ENV{ID_VENDOR_ID}=="${kb.vendor}", ENV{ID_MODEL_ID}=="${kb.product}", RUN+="${toggleScript} uninhibit"
67 '') cfg.usbKeyboards;
68in
69{
70 options = {
71 services.laptop-keyboard-inhibit = {
72 enable = mkEnableOption "Inhibit internal laptop keyboard when external keyboard is connected";
73
74 keyboards = mkOption {
75 type = with types; listOf str;
76 default = [ ];
77 example = [
78 "Eyelash Corne"
79 "My Custom Keyboard"
80 ];
81 description = ''
82 List of external keyboard device names to watch for.
83 When a keyboard with this name connects, the internal laptop keyboard
84 is inhibited (disabled). It is re-enabled when the external keyboard
85 disconnects.
86
87 Find your keyboard's name with: cat /proc/bus/input/devices
88 '';
89 };
90
91 usbKeyboards = mkOption {
92 type = with types; listOf (attrsOf str);
93 default = [ ];
94 example = [
95 {
96 vendor = "3297";
97 product = "1969";
98 }
99 ];
100 description = ''
101 List of USB keyboard vendor/product ID pairs to watch for.
102 Find your keyboard's IDs with: lsusb
103
104 Example: ZSA Moonlander is vendor 3297, product 1969.
105 '';
106 };
107 };
108 };
109
110 config = mkIf cfg.enable {
111 # USB keyboard detection via udev
112 services.udev.extraRules = mkIf (usbRules != "") usbRules;
113
114 # Bluetooth keyboard detection via bluetooth-monitor
115 systemd.services.bluetooth-keyboard-monitor = mkIf (cfg.keyboards != [ ]) {
116 description = "Monitor Bluetooth keyboards and toggle internal keyboard";
117 after = [ "bluetooth.service" ];
118 requires = [ "bluetooth.service" ];
119 wantedBy = [ "multi-user.target" ];
120 path = with pkgs; [
121 bluez
122 gawk
123 ];
124
125 serviceConfig = {
126 Type = "simple";
127 Restart = "always";
128 RestartSec = 5;
129 };
130
131 script =
132 let
133 # Get Bluetooth MAC addresses for configured keyboard names
134 # User needs to pair keyboards first with bluetoothctl
135 getDeviceScript = pkgs.writeShellScript "get-bt-devices" ''
136 # Wait for bluetooth to be ready
137 sleep 2
138
139 # Find MAC addresses for configured keyboard names
140 ${concatMapStringsSep "\n" (name: ''
141 MAC=$(bluetoothctl devices | grep -i "${name}" | awk '{print $2}' | head -1)
142 if [ -n "$MAC" ]; then
143 echo "$MAC"
144 fi
145 '') cfg.keyboards}
146 '';
147 in
148 ''
149 set -euo pipefail
150
151 # Get MAC addresses of configured keyboards
152 MACS=$(${getDeviceScript})
153
154 if [ -z "$MACS" ]; then
155 echo "No Bluetooth keyboards found. Make sure they are paired."
156 echo "Use: bluetoothctl pair <MAC>"
157 sleep 10
158 exit 1
159 fi
160
161 # Monitor each keyboard
162 while IFS= read -r MAC; do
163 (
164 echo "Monitoring Bluetooth keyboard: $MAC"
165 while true; do
166 if bluetoothctl info "$MAC" 2>/dev/null | grep -q "Connected: yes"; then
167 # Keyboard connected - disable internal keyboard
168 ${toggleScript} inhibit
169 else
170 # Keyboard disconnected - enable internal keyboard
171 ${toggleScript} uninhibit
172 fi
173 sleep 5
174 done
175 ) &
176 done <<< "$MACS"
177
178 # Wait for all background monitors
179 wait
180 '';
181 };
182 };
183}