Commit ca74e8999e22

Vincent Demeester <vincent@sbr.pm>
2026-02-13 16:49:52
feat: auto-inhibit laptop keyboard on external kbd connect
Added laptop-keyboard-inhibit NixOS module using udev rules and the kernel's sysfs inhibited attribute (Linux 5.11+). Supports both Bluetooth (by device name) and USB (by vendor/product ID) keyboards. Enabled on okinawa for Eyelash Corne (BT) and kyushu for Moonlander (USB).
1 parent ba3a62c
Changed files (3)
modules
laptop-keyboard-inhibit
systems
modules/laptop-keyboard-inhibit/default.nix
@@ -0,0 +1,113 @@
+# Automatically inhibit (disable) the laptop's internal keyboard when an
+# external keyboard is connected, and re-enable it on disconnect.
+#
+# Works with both USB and Bluetooth keyboards via udev rules that write
+# to the kernel's input device `inhibited` sysfs attribute (Linux 5.11+).
+#
+# Usage:
+#   services.laptop-keyboard-inhibit = {
+#     enable = true;
+#     # Match by device name (Bluetooth keyboards, or any input device)
+#     keyboards = [ "Eyelash Corne" ];
+#     # Or match USB keyboards by vendor:product ID
+#     usbKeyboards = [ { vendor = "3297"; product = "1969"; } ];
+#   };
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+let
+  inherit (lib)
+    mkEnableOption
+    mkIf
+    mkOption
+    types
+    concatStringsSep
+    concatMapStringsSep
+    ;
+  cfg = config.services.laptop-keyboard-inhibit;
+
+  # Script that finds and inhibits/uninhibits the internal keyboard
+  # Uses by-path stable device path for i8042 (PS/2) keyboards
+  toggleScript = pkgs.writeShellScript "toggle-internal-kbd" ''
+    ACTION="$1" # "inhibit" or "uninhibit"
+
+    VALUE=1
+    if [ "$ACTION" = "uninhibit" ]; then
+      VALUE=0
+    fi
+
+    # Find the internal keyboard's inhibited file
+    # Standard PS/2 keyboard path on x86 laptops
+    for inhibit_file in /sys/devices/platform/i8042/serio*/input/input*/inhibited; do
+      if [ -f "$inhibit_file" ]; then
+        echo "$VALUE" > "$inhibit_file" 2>/dev/null || true
+      fi
+    done
+  '';
+
+  # Generate udev rules for name-based matching (Bluetooth & USB keyboards)
+  nameRules = concatMapStringsSep "\n" (name: ''
+    # Inhibit internal keyboard when "${name}" connects
+    ACTION=="add", SUBSYSTEM=="input", ATTR{name}=="${name}", RUN+="${toggleScript} inhibit"
+    ACTION=="remove", SUBSYSTEM=="input", ENV{NAME}=="${name}", RUN+="${toggleScript} uninhibit"
+  '') cfg.keyboards;
+
+  # Generate udev rules for USB vendor:product matching
+  usbRules = concatMapStringsSep "\n" (
+    kb: ''
+      # Inhibit internal keyboard when USB keyboard ${kb.vendor}:${kb.product} connects
+      ACTION=="add", SUBSYSTEM=="input", ATTR{id/vendor}=="${kb.vendor}", ATTR{id/product}=="${kb.product}", ATTR{name}!="", RUN+="${toggleScript} inhibit"
+      ACTION=="remove", SUBSYSTEM=="input", ENV{ID_VENDOR_ID}=="${kb.vendor}", ENV{ID_MODEL_ID}=="${kb.product}", RUN+="${toggleScript} uninhibit"
+    ''
+  ) cfg.usbKeyboards;
+in
+{
+  options = {
+    services.laptop-keyboard-inhibit = {
+      enable = mkEnableOption "Inhibit internal laptop keyboard when external keyboard is connected";
+
+      keyboards = mkOption {
+        type = with types; listOf str;
+        default = [ ];
+        example = [ "Eyelash Corne" "My Custom Keyboard" ];
+        description = ''
+          List of external keyboard device names to watch for.
+          When a keyboard with this name connects, the internal laptop keyboard
+          is inhibited (disabled). It is re-enabled when the external keyboard
+          disconnects.
+
+          Find your keyboard's name with: cat /proc/bus/input/devices
+        '';
+      };
+
+      usbKeyboards = mkOption {
+        type = with types; listOf (attrsOf str);
+        default = [ ];
+        example = [
+          {
+            vendor = "3297";
+            product = "1969";
+          }
+        ];
+        description = ''
+          List of USB keyboard vendor/product ID pairs to watch for.
+          Find your keyboard's IDs with: lsusb
+
+          Example: ZSA Moonlander is vendor 3297, product 1969.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.extraRules = concatStringsSep "\n" (
+      lib.filter (s: s != "") [
+        nameRules
+        usbRules
+      ]
+    );
+  };
+}
systems/kyushu/extra.nix
@@ -17,6 +17,7 @@
     ../common/services/docker.nix
     ../common/services/libvirt.nix
     ../common/services/binfmt.nix
+    ../../modules/laptop-keyboard-inhibit
 
     ../redhat
   ];
@@ -32,6 +33,17 @@
     ];
   };
 
+  # Auto-disable internal keyboard when Moonlander (USB) is connected
+  services.laptop-keyboard-inhibit = {
+    enable = true;
+    usbKeyboards = [
+      {
+        vendor = "3297";
+        product = "1969";
+      }
+    ];
+  };
+
   services = {
     getty = {
       autologinOnce = true;
systems/okinawa/extra.nix
@@ -13,6 +13,7 @@
     ../common/services/containers.nix
     ../common/services/docker.nix
     ../common/services/prometheus-exporters-node.nix
+    ../../modules/laptop-keyboard-inhibit
   ];
 
   # Gaming setup
@@ -42,6 +43,12 @@
     autologinUser = "vincent";
   };
 
+  # Auto-disable internal keyboard when Eyelash Corne (BT) is connected
+  services.laptop-keyboard-inhibit = {
+    enable = true;
+    keyboards = [ "Eyelash Corne" ];
+  };
+
   # Wireguard VPN
   services.wireguard = {
     enable = true;