Skip to content

Instantly share code, notes, and snippets.

@eevmanu
Last active April 26, 2026 01:23
Show Gist options
  • Select an option

  • Save eevmanu/99479ec052a6f086dc5b6fa8a27216b6 to your computer and use it in GitHub Desktop.

Select an option

Save eevmanu/99479ec052a6f086dc5b6fa8a27216b6 to your computer and use it in GitHub Desktop.
A rootless bash script using udevadm monitor to detect specific USB keyboard plug/unplug events (with debouncing) and trigger custom actions.

USB Keyboard Detection Plan

Objective

Create a Linux terminal tool to detect when a specific USB keyboard (e.g., Keychron K1) is connected or disconnected, extract its brand and model, and trigger distinct custom actions for both events. The solution must prioritize running in user-space without requiring sudo privileges.

Key Files & Context

  • monitor_keyboard.sh: The bash script acting as the primary proof-of-concept. It listens to a continuous stream of udevadm events.
  • GitHub Gist Ready: The script should contain metadata tags (e.g., #udev, #linux, #bash, #hotplug) to make it easily searchable in online snippet repositories or personal notes.

Implementation Steps

Phase 1: Minimal PoC Script (Complete)

  • Tooling: Uses udevadm monitor --environment --subsystem-match=input to capture hardware events without root access.
  • Action Parsing: Explicitly intercepts ACTION=add (plug-in) and ACTION=remove (unplug) events and executes separate user-defined code blocks for each.
  • Target Matching: Implements a case-insensitive substring matching system for the keyboard's vendor and model properties.
  • Guard Clauses: Evaluates incoming events using early returns to avoid deeply nested conditionals and keep the code flat/readable.
  • Enhanced Debounce Logic:
    • Composite Device Handling: High-end mechanical keyboards (like Keychron) enumerate as multiple virtual sub-devices (NKRO, Media keys, standard keyboard). The script uses a 3-second debounce window (<= 3) to successfully absorb the delayed enumeration burst of all these interfaces.
    • Non-blocking Stream: Crucially, the script processes events in real-time (no sleep commands inside the read loop) to ensure the debounce timer accurately compares burst events against the initial trigger timestamp.
  • Logging: Timestamped output for debugging and easy tracing.
  • Documentation: Inline comments detailing how to redirect logs and convert the script into a systemd service.

Refinement 1: User-level Background Daemon (Pending)

  • Provide instructions and scaffolding to wrap monitor_keyboard.sh into a systemctl --user service.
  • This allows the monitor to start on login, run in the background, and log to journalctl --user without needing sudo.

Refinement 2: Native udev Rules (Optional, requires sudo)

  • Provide a system-level alternative using /etc/udev/rules.d/ if the user desires kernel-native event handling in the future (requires sudo).

Verification & Testing

  • Run monitor_keyboard.sh in a terminal.
  • Plug Test: Connect the target USB keyboard and verify that the target is matched precisely and only triggers the "add" action once despite the enumeration burst.
  • Unplug Test: Disconnect the target USB keyboard and verify that the "remove" action is triggered exactly once.
  • Negative Test: Plug in a different USB device (e.g., mouse) and verify it is ignored by the guard clauses.
  • Verify timestamped logs are emitted correctly.
#!/bin/bash
# ==============================================================================
# Script: monitor-keyboard.sh
# Description: Monitors for specific USB keyboards and triggers custom scripts
# on plug/unplug events. Uses debouncing to handle event bursts.
# Tags: linux, udev, usb, automation, hotplug, debounce, bash
# ==============================================================================
# Minimal PoC for USB Keyboard Detection using udevadm
# Does not require sudo.
# --- Configuration ---
# Set your target keyboard to match against (use lowercase for case-insensitive matching).
# These will match substrings. E.g., TARGET_VENDOR="keychron" matches "Keychron_K1".
TARGET_VENDOR="keychron"
TARGET_MODEL="k1"
# USB connections fire a "burst" of events. We use a debounce timer
# to ensure your custom script only triggers once per physical plug-in.
DEBOUNCE_SECONDS=3
LAST_TRIGGER=0
LAST_ACTION=""
# Variables to hold the state of the current udev property block being read
ACTION=""
IS_KEYBOARD=0
VENDOR=""
MODEL=""
log() {
# Print with a timestamp for easy tracing and debugging
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
}
log "Starting USB Keyboard monitor..."
log "Targeting keyboard: Vendor='*$TARGET_VENDOR*', Model='*$TARGET_MODEL*'"
log "Waiting for input devices to connect or disconnect..."
# We use udevadm monitor, filtering only 'input' subsystem events.
# --environment outputs key/value pairs separated by an empty line per event.
udevadm monitor --environment --subsystem-match=input | while read -r line; do
# --- Property Streaming ---
# Capture properties as they arrive line-by-line
if [[ "$line" == ACTION=* ]]; then
ACTION="${line#ACTION=}"
continue
fi
if [[ "$line" == "ID_INPUT_KEYBOARD=1" ]]; then
IS_KEYBOARD=1
continue
fi
if [[ "$line" == ID_VENDOR_ENC=* ]]; then
VENDOR="${line#ID_VENDOR_ENC=}"
# Unescape udev hex sequences like \x20 (space) to make it readable
VENDOR=$(printf '%b' "${VENDOR//\\x/\\x}")
continue
fi
if [[ "$line" == ID_MODEL_ENC=* ]]; then
MODEL="${line#ID_MODEL_ENC=}"
MODEL=$(printf '%b' "${MODEL//\\x/\\x}")
continue
fi
# If the line isn't empty, it's just another udev property we don't need to parse right now.
if [[ -n "$line" ]]; then
continue
fi
# --- Event Evaluation (Empty line reached) ---
# We evaluate the captured properties using early returns (guard clauses) to avoid nested ifs.
# Guard 1: Missing action or not an add/remove event
if [[ "$ACTION" != "add" && "$ACTION" != "remove" ]]; then
# Reset state for the next event block
ACTION=""
IS_KEYBOARD=0
VENDOR=""
MODEL=""
continue
fi
# Guard 2: Ignore non-keyboards immediately
if [[ $IS_KEYBOARD -eq 0 ]]; then
ACTION=""
IS_KEYBOARD=0
VENDOR=""
MODEL=""
continue
fi
# Guard 3: Missing vendor or model data
if [[ -z "$VENDOR" || -z "$MODEL" ]]; then
ACTION=""
IS_KEYBOARD=0
VENDOR=""
MODEL=""
continue
fi
# Guard 4: Target Validation (Case-Insensitive Match)
LOWER_VENDOR=$(echo "$VENDOR" | tr '[:upper:]' '[:lower:]')
LOWER_MODEL=$(echo "$MODEL" | tr '[:upper:]' '[:lower:]')
if [[ "$LOWER_VENDOR" != *"$TARGET_VENDOR"* || "$LOWER_MODEL" != *"$TARGET_MODEL"* ]]; then
log " [DEBUG] Ignored keyboard action: '$ACTION' for '$VENDOR' '$MODEL' (Did not match target)"
ACTION=""
IS_KEYBOARD=0
VENDOR=""
MODEL=""
continue
fi
# Guard 5: Debounce/Throttle Check
# We only debounce if the SAME action happens multiple times quickly (event bursts).
# If the action changes (e.g., add immediately followed by remove), we process it.
CURRENT_TIME=$(date +%s)
TIME_DIFF=$((CURRENT_TIME - LAST_TRIGGER))
if [[ "$ACTION" == "$LAST_ACTION" && $TIME_DIFF -le $DEBOUNCE_SECONDS ]]; then
log " [DEBUG] Ignored duplicate '$ACTION' event for target keyboard (Debounce active)"
ACTION=""
IS_KEYBOARD=0
VENDOR=""
MODEL=""
continue
fi
# --- Success ---
# If we reached here, it's exactly the keyboard we want, and it's not a duplicate event burst.
LAST_TRIGGER=$CURRENT_TIME
LAST_ACTION=$ACTION
log "=> [SUCCESS] Target Keyboard Event: Action='$ACTION', Vendor='$VENDOR', Model='$MODEL'"
if [[ "$ACTION" == "add" ]]; then
log " [ACTION] Keyboard Plugged In!"
# -------------------------------------------------------------
# PUT YOUR CUSTOM SCRIPT OR COMMAND FOR **PLUG-IN** HERE
# Example: /path/to/your/connected_script.sh "$VENDOR" "$MODEL" &
# ,kbk
# -------------------------------------------------------------
elif [[ "$ACTION" == "remove" ]]; then
log " [ACTION] Keyboard Unplugged!"
# -------------------------------------------------------------
# PUT YOUR CUSTOM SCRIPT OR COMMAND FOR **UNPLUG** HERE
# Example: /path/to/your/disconnected_script.sh "$VENDOR" "$MODEL" &
# ,kbn
# -------------------------------------------------------------
fi
# Reset state for the next hardware event
ACTION=""
IS_KEYBOARD=0
VENDOR=""
MODEL=""
done
# =========================================================================
# ======================== FUTURE REFINEMENTS =============================
# =========================================================================
#
# 1. Log Redirection (Simple File Logging):
# If you just want to run this in the background and write to a file, run:
# ./monitor_keyboard.sh >> ~/keyboard_monitor.log 2>&1 &
#
# 2. Convert to a User-level Systemd Service (Recommended Refinement Level 1):
# This allows the script to start automatically on login without an open terminal,
# restarts it if it crashes, and logs output natively to the system journal.
#
# Step A: Create the service directory if it doesn't exist:
# mkdir -p ~/.config/systemd/user/
#
# Step B: Create a file named `~/.config/systemd/user/keyboard-monitor.service`
#
# Step C: Add the following contents to the file (update ExecStart path!):
# [Unit]
# Description=USB Keyboard Monitor
#
# [Service]
# ExecStart=/home/manu/monitor_keyboard.sh
# Restart=always
# RestartSec=3
#
# [Install]
# WantedBy=default.target
#
# Step D: Reload the systemd user daemon:
# systemctl --user daemon-reload
#
# Step E: Enable (start on login) and start the service:
# systemctl --user enable --now keyboard-monitor.service
#
# 3. Viewing Logs using Journalctl:
# If you converted this to a systemd service, you DO NOT need a separate log file.
# Instead, standard output goes to the systemd journal. You can view it by running:
#
# journalctl --user -u keyboard-monitor.service -f
#
# (Or use `journalctl --user -xe` for the tail of all your user services).
# =========================================================================
@eevmanu

eevmanu commented Apr 26, 2026

Copy link
Copy Markdown
Author
  • udev-usb-trigger.sh (Most accurate technical description)
  • on-usb-keyboard.sh (Very readable, explains when it runs)
  • usb-hotplug-monitor.sh (Standard Linux terminology)
  • keychron-detector.sh (If you want to remember exactly what you built it for)
  • udev-action-runner.sh (Good if you plan to adapt it for mice/gamepads later)

@eevmanu

eevmanu commented Apr 26, 2026

Copy link
Copy Markdown
Author

bash
linux
udev
udevadm
usb
keyboard
hid
keychron
hardware
automation
hotplug
plug-and-play
debounce
event-driven
background-process
no-sudo

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