Skip to content

fibsussy/keymux

Repository files navigation

keymux ⌨️

QMK-inspired keyboard customization for Linux - A blazing-fast, zero-latency keyboard middleware daemon that brings advanced QMK features to any keyboard

✨ Features

Core Functionality

  • QMK-Style Mod-Tap (MT): Advanced tap/hold with permissive hold, roll detection, chord detection, adaptive timing
  • Adaptive Timing: Per-key personalized thresholds that learn your typing patterns
  • Custom Layers: Define unlimited layers (navigation, numpad, symbols, etc.)
  • Game Mode: Automatic detection via Steam/Gamescope with SOCD support
  • SOCD Cleaner: Last-input-priority for FPS games (eliminates W+S conflicts)
  • Command Runner: Execute arbitrary shell commands on key press
  • Per-Keyboard Configs: Different keymaps for different keyboards
  • Hot-Reload: Automatic config reload on file save with desktop notifications
  • Full Keyboard Tracking: Collects tap statistics for 100% of keyboard keys

Advanced Features

  • Zero Input Lag: Direct evdev access with non-blocking I/O
  • Multi-Keyboard Support: Handle multiple keyboards simultaneously
  • Hotplug Detection: Automatically detect keyboard connect/disconnect
  • Hardware-Based IDs: Keyboards identified by USB properties, not device paths
  • Adaptive Statistics: View real-time typing analytics with adaptive-stats command
  • Shell Completions: Bash, Zsh, Fish support built-in

System Integration

  • Systemd Service: Runs as user service with automatic startup
  • Desktop Notifications: Config reload success/error notifications
  • IPC Architecture: Manage keyboards without restarting daemon
  • RON Configuration: Human-readable config with extensive comments

🔧 Installation

One-Line Install (Arch Linux)

Precompiled binary (default, fast):

curl -fsSL https://raw.githubusercontent.com/fibsussy/keymux/main/install.sh | bash

Or build from source:

curl -fsSL https://raw.githubusercontent.com/fibsussy/keymux/main/install.sh | bash -s local

Note: For security, inspect the install script before running it. View it here.

Manual Installation

Prerequisites

Add yourself to the input group:

sudo usermod -a -G input $USER
# Log out and log back in for changes to take effect

From Source

# Clone and build
git clone https://github.com/fibsussy/keymux.git
cd keymux
cargo build --release

# Install
sudo cp target/release/keymux /usr/bin/
sudo cp keymux.service /usr/lib/systemd/system/
sudo cp keymux-niri.service /usr/lib/systemd/user/
sudo cp config.example.ron /usr/share/doc/keymux/

# Enable and start root daemon
sudo systemctl enable --now keymux.service

Post-Installation Setup

  1. Copy the example config:
mkdir -p ~/.config/keymux
cp /usr/share/doc/keymux/config.example.ron ~/.config/keymux/config.ron
  1. Edit your config:
$EDITOR ~/.config/keymux/config.ron
  1. Select which keyboards to enable:
keymux toggle
  1. (Optional) Enable Niri watcher for automatic game mode:
systemctl --user enable --now keymux-niri.service

Systemd Services

Root daemon (required): Manages keyboard devices

  • Path: /usr/lib/systemd/system/keymux.service
  • Enable: sudo systemctl enable --now keymux.service

User service (optional): Watches Niri windows for automatic game mode

  • Path: /usr/lib/systemd/user/keymux-niri.service
  • Enable: systemctl --user enable --now keymux-niri.service

📖 Configuration Guide

Configuration File Location

~/.config/keymux/config.ron

Basic Structure

(
    tapping_term_ms: 130,
    enabled_keyboards: [
        "*",                      // enable all by default
        // "6913",                 // enable keyboard with 6913 in ID
        // "Keychron K2",          // enable by keyboard name
        // "1234": Enable,         // explicit enable (map syntax)
        // "event3": Disable,      // disable by event path
        // ("1234", "Enable"),    // explicit enable (tuple syntax)
    ],
    remaps: { /* base layer keymaps */ },
    layers: { /* additional layers */ },
    game_mode: ( remaps: { /* game mode keymaps */ } ),
    keyboard_overrides: { /* per-keyboard configs */ },
    
    // MT configuration (all optional, shown with defaults)
    mt_config: (
        permissive_hold: true,
        enable_roll_detection: true,
        enable_chord_detection: true,
        enable_multi_mod_detection: true,
        enable_cross_hand_unwrap: true,
        enable_double_tap_hold: true,
        enable_adaptive_timing: true,
        pause_learning_in_game_mode: true,
        roll_threshold_ms: 100,
        chord_threshold_ms: 100,
        target_margin_ms: 30,
        ema_alpha: 0.02,
        auto_save_interval_secs: 30,
    ),
)

Behavior Settings

  • tapping_term_ms (default: 130)

    • Base time threshold for tap vs hold decision
    • When adaptive timing is enabled, this serves as the initial/fallback threshold
    • 100-130ms: More sensitive to holds
    • 150-200ms: More sensitive to taps
  • enabled_keyboards - Which keyboards to process:

    • Absent/omitted: Enable all keyboards (equivalent to ["*"])
    • None: Disable all (no keyboards enabled)
    • ["*"]: Enable all keyboards
    • ["1234", "5678"]: Enable keyboards matching these patterns
    • ["*", "1234": Disable]: Enable all except 1234

    Syntax options:

    • Map style: "pattern": Enable, "pattern": Disable
    • Tuple style: ("pattern", "Enable"), ("pattern", "Disable")
    • Bare string: "1234" = enable keyboard matching 1234
  • mt_config - Configuration for Mod-Tap (MT) keys

    • permissive_hold (default: true): When another key is pressed while MT is held, resolve immediately to hold
    • enable_roll_detection (default: true): Fast same-hand rolls favor tap (prevents accidental holds during typing)
    • enable_chord_detection (default: true): Opposite-hand chords favor hold (e.g., Ctrl+C with home row mods)
    • enable_multi_mod_detection (default: true): Multiple mods on same hand promote to hold
    • enable_cross_hand_unwrap (default: true): Holding opposite-hand mod unwraps MT keys to tap (prevents Shift+Shift)
    • enable_double_tap_hold (default: true): Double-tap then hold to hold the tap key instead of modifier
    • enable_adaptive_timing (default: true): Learn per-key personalized thresholds from your typing patterns
    • pause_learning_in_game_mode (default: true): Disable adaptive learning when game mode is active
    • roll_threshold_ms (default: 100): Max time between keys to count as roll
    • chord_threshold_ms (default: 100): Max time between keys to count as chord
    • target_margin_ms (default: 30): Safety margin added to average tap duration for adaptive threshold
    • ema_alpha (default: 0.02): Exponential moving average smoothing factor (~100 sample window)
    • auto_save_interval_secs (default: 30): How often to save adaptive statistics to disk

Available Key Codes

Letters: KC_A through KC_Z Numbers: KC_1 through KC_0 Modifiers: KC_LCTL, KC_LSFT, KC_LALT, KC_LGUI, KC_RCTL, KC_RSFT, KC_RALT, KC_RGUI Special: KC_ESC, KC_CAPS, KC_TAB, KC_SPC, KC_ENT, KC_BSPC, KC_DEL Function: KC_F1 through KC_F12 Arrows: KC_LEFT, KC_DOWN, KC_UP, KC_RGHT

Available Actions

Key(KeyCode)

Direct key mapping.

KC_CAPS: Key(KC_ESC),  // Caps Lock becomes Escape

MT(tap_key, hold_key)

QMK-style Mod-Tap with advanced features:

  • Tap: Quick press emits tap_key (base key)
  • Hold: Long press emits hold_key (modifier)
  • Permissive Hold: Pressing another key while MT is held resolves immediately to hold
  • Roll Detection: Fast same-hand rolls favor tap (prevents accidental holds during typing)
  • Chord Detection: Opposite-hand chords favor hold (e.g., Ctrl+C with home row mods)
  • Cross-Hand Unwrap: Holding opposite-hand mod unwraps MT keys to tap (prevents Shift+Shift conflicts)
  • Double-Tap-Hold: Press twice quickly then hold to hold the tap key instead of modifier
  • Adaptive Timing: Learns per-key personalized thresholds from your typing patterns
// Home row mods - left hand
KC_A: MT(KC_A, KC_LGUI),  // Tap 'a', hold for Super/Win/Cmd
KC_S: MT(KC_S, KC_LALT),  // Tap 's', hold for Alt
KC_D: MT(KC_D, KC_LCTL),  // Tap 'd', hold for Ctrl
KC_F: MT(KC_F, KC_LSFT),  // Tap 'f', hold for Shift

// Space/Control overload
KC_SPC: MT(KC_SPC, KC_LCTL),  // Tap Space, hold Ctrl

// Examples of MT behavior:
// - Quick press A = 'a'
// - Hold A (130ms+) = Super/Cmd held
// - Hold A + press J = Super+J (permissive hold)
// - Type "as" quickly (roll) = "as" (not Alt+s)
// - A on left + J on right quickly = 'a' then 'j' (chord detection)
// - Hold D (Ctrl) + press A = Ctrl+A (cross-hand unwrap prevents Cmd+A)
// - Double-tap-hold SPC = hold Space key (not Ctrl)

All MT features are configurable via mt_config (see Behavior Settings section).

TO(Layer)

Switch to a different layer while held.

KC_LALT: TO("nav"),  // Hold Left Alt to activate navigation layer

SOCD(key, [opposing_keys...])

SOCD cleaner for gaming (last-input-priority).

KC_W: SOCD(KC_W, [KC_S]),  // Pressing W then S = S, release S = W again

CMD (Command Runner)

Execute arbitrary shell commands on key press.

KC_F1: CMD("/usr/bin/notify-send 'Hello from keyboard!'"),
KC_F2: CMD("/usr/bin/playerctl play-pause"),

Example Configurations

Minimal Config (Mod-Tap Only)

(
    tapping_term_ms: 130,
    enabled_keyboards: [
        "*",  // enable all keyboards
    ],

    remaps: {
        // Escape/Caps swap
        KC_CAPS: KC_ESC,
        KC_ESC: KC_GRV,

        // Mod-Tap - left hand
        KC_A: MT(KC_A, KC_LGUI),
        KC_S: MT(KC_S, KC_LALT),
        KC_D: MT(KC_D, KC_LCTL),
        KC_F: MT(KC_F, KC_LSFT),

        // Mod-Tap - right hand
        KC_J: MT(KC_J, KC_RSFT),
        KC_K: MT(KC_K, KC_RCTL),
        KC_L: MT(KC_L, KC_RALT),
        KC_SCLN: MT(KC_SCLN, KC_RGUI),
    },

    layers: {},
    game_mode: (remaps: {}),
    keyboard_overrides: {},
)

Advanced Config (Layers + Game Mode + Custom MT Timing)

(
    tapping_term_ms: 130,
    enabled_keyboards: [
        "2e3c:c365:0110:0003",  // enable specific keyboard by ID
    ],

    remaps: {
        KC_CAPS: KC_ESC,
        KC_ESC: KC_GRV,
        KC_LALT: TO("nav"),

        KC_A: MT(KC_A, KC_LGUI),
        KC_S: MT(KC_S, KC_LALT),
        KC_D: MT(KC_D, KC_LCTL),
        KC_F: MT(KC_F, KC_LSFT),
        KC_J: MT(KC_J, KC_RSFT),
        KC_K: MT(KC_K, KC_RCTL),
        KC_L: MT(KC_L, KC_RALT),
        KC_SCLN: MT(KC_SCLN, KC_RGUI),
    },

    layers: {
        "nav": (
            remaps: {
                // Keep modifiers accessible
                KC_A: Key(KC_LGUI),
                KC_S: Key(KC_LALT),
                KC_D: Key(KC_LCTL),
                KC_F: Key(KC_LSFT),

                // Vim-style navigation
                KC_H: Key(KC_LEFT),
                KC_J: Key(KC_DOWN),
                KC_K: Key(KC_UP),
                KC_L: Key(KC_RGHT),
            },
        ),
    },

    game_mode: (
        remaps: {
            KC_CAPS: KC_ESC,
            KC_ESC: KC_GRV,

            // SOCD for FPS gaming
            KC_W: SOCD(KC_W, [KC_S]),
            KC_S: SOCD(KC_S, [KC_W]),
            KC_A: SOCD(KC_A, [KC_D]),
            KC_D: SOCD(KC_D, [KC_A]),
        },
    ),

    keyboard_overrides: {},

    // Optional: Customize MT behavior
    mt_config: (
        permissive_hold: true,
        enable_roll_detection: true,
        enable_chord_detection: true,
        enable_adaptive_timing: true,
        target_margin_ms: 40,  // More conservative margin (default: 30)
        ema_alpha: 0.01,       // Slower learning (default: 0.02)
    ),
)

Timing Configuration

  • tapping_term_ms: Base time threshold for tap vs hold (130-200ms recommended)
    • When adaptive timing is enabled, this serves as the initial/fallback threshold
    • Lower value = more sensitive to holds, higher = more sensitive to taps
    • Recommended: 130ms for mechanical keyboards, 150-180ms for laptops

Adaptive Timing

The MT system can learn your typing patterns and personalize thresholds per key:

  • How it works: Records tap durations when keys resolve to TAP (below threshold)
  • Formula: adaptive_threshold = average_tap_duration + target_margin_ms
  • Tracks all keys: Collects statistics for 100% of keyboard (A-Z, numbers, F-keys, etc.)
  • Exponential moving average: Uses α=0.02 (~100 sample window) for smooth adaptation
  • Starts immediately: Begins learning after first tap (no minimum sample requirement)
  • Game mode aware: Pauses learning during games to avoid skewing data

View statistics: keymux adaptive-stats Clear statistics: keymux clear-stats

Game Mode Detection

Game mode activates automatically when:

  1. Steam games: Process tree contains steam + game executable
  2. Gamescope: Window manager reports gamescope app ID
  3. IS_GAME env var: Process has IS_GAME=1 environment variable

Game mode is controlled automatically via the Niri daemon or can be toggled via IPC

🎮 Usage

Daemon Management

# Start daemon (automatically started by systemd)
keymux daemon

# Check status
systemctl status keymux

# View live logs
journalctl -u keymux -f

# Restart daemon
systemctl restart keymux

Keyboard Management

# List all detected keyboards
keymux list

# Toggle which keyboards are enabled (interactive menu)
keymux toggle

# Enable specific keyboards by ID or name
keymux enable 6912           # by partial ID
keymux enable "Keychron"     # by name
keymux enable "*"            # enable all

# Disable specific keyboards by ID or name  
keymux disable 6912          # by partial ID
keymux disable "Built-in"    # by name
keymux disable "*"           # disable all

# Open multi-select menu to choose keyboards
keymux toggle --multi
keymux enable --multi
keymux disable --multi

# Validate your config
keymux validate

# Reload config (automatic on file save, but manual trigger available)
keymux reload

# Game mode is automatically detected when running Steam/Gamescope
# Manual toggle requires sending IPC requests to the daemon

# View adaptive timing statistics
keymux adaptive-stats

# Clear adaptive timing statistics (prompts for confirmation)
keymux clear-stats

# Debug mode (show all keyboard events in real-time)
keymux debug

Shell Completions

# Bash
keymux completion bash | sudo tee /usr/share/bash-completion/completions/keymux

# Zsh
keymux completion zsh | sudo tee /usr/share/zsh/site-functions/_keymux

# Fish
keymux completion fish > ~/.config/fish/completions/keymux.fish

🐛 Troubleshooting

"Permission denied" errors

Add yourself to the input group:

sudo usermod -a -G input $USER

Then log out and back in.

"Device or resource busy"

Another process is grabbing your keyboard. Check for:

# Kill any existing instances
pkill -f keymux

# Check for other remapping tools
ps aux | grep -E "kmonad|keyd|xremap"

Config errors

Watch the logs when editing config:

journalctl -u keymux -f

Config errors show desktop notifications and keep the previous working config.

Hot-reload not working

Ensure the daemon is running:

systemctl status keymux

Check file watcher is working (should see "Config reloaded" in logs when you save).

"MT keys trigger hold too fast/slow"

Option 1: Adjust base threshold

  • 100-130ms: More sensitive to holds
  • 150-200ms: More sensitive to taps
  • Recommended: 130ms for mechanical keyboards, 150-180ms for laptops

Option 2: Enable adaptive timing (recommended)

  • Set mt_config.enable_adaptive_timing: true (enabled by default)
  • The system learns your typing patterns and personalizes thresholds per key
  • View statistics: keymux adaptive-stats
  • Increase safety margin: mt_config.target_margin_ms: 40 (default: 30)

Option 3: Adjust detection features

  • Disable roll detection: mt_config.enable_roll_detection: false
  • Disable chord detection: mt_config.enable_chord_detection: false
  • Disable permissive hold: mt_config.permissive_hold: false

"W+S both pressed in game, not moving"

Use SOCD in game_mode (see configuration examples above).

📚 Related Projects

Alternative keyboard remapping tools with different approaches:

  • kmonad - Haskell-based, cross-platform, mature codebase, S-expression config
  • keyd - C-based, key remapping via config files, active development
  • kanata - Rust-based, cross-platform, programmable key remapper
  • xremap - Python-based, X11 only, older project

Each has different strengths - choose based on your platform, performance needs, and configuration preferences.

📚 Further Reading

🤝 Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

📄 License

MIT License - See LICENSE for details.

🙏 Acknowledgments

About

QMK-inspired keyboard middleware for Linux — home row mods, mod-tap, layers, SOCD, and adaptive timing via evdev. Written in Rust.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors