#keyboard #debounce #evdev #interception

bin+lib intercept-bounce

An Interception Tools filter to eliminate keyboard chatter (switch bounce)

1 unstable release

new 0.9.0 May 6, 2025

#1119 in Command line utilities

MIT/Apache

120KB
2K SLoC

intercept-bounce

Crates.io License: MIT OR Apache-2.0 Build Status

An Interception Tools filter designed to eliminate keyboard chatter (also known as switch bounce) while providing detailed statistics and diagnostics.

It reads raw Linux input_event structs from standard input, filters out rapid duplicate key press/release events based on a configurable time threshold, and writes the filtered events to standard output. Comprehensive statistics about dropped events, timings, and near-misses are printed to standard error on exit or periodically.

Features

  • Configurable Debouncing: Filters rapid duplicate key press/release events within a specified time window (--debounce-time, default: 25ms). Key repeats (value=2) are never filtered.
  • Near-Miss Tracking: Identifies and reports key events that pass the filter but occur just slightly after the debounce window closes (--near-miss-threshold-time, default: 100ms). Useful for diagnosing keys with inconsistent timing.
  • Detailed Statistics (Human-Readable & JSON):
    • Overall counts (key events processed, passed, dropped).
    • Overall histograms showing the distribution of bounce and near-miss timings.
    • Per-key statistics:
      • Total processed, passed, dropped counts, and drop rate (%).
      • Bounce time statistics (min/avg/max) for dropped press/release events.
      • Near-miss time statistics (min/avg/max) for passed press/release events.
      • Detailed histograms for bounce and near-miss timings per key/state (JSON only).
  • Flexible Logging:
    • --log-all-events: Log details ([PASS]/[DROP]) for (almost) every event.
    • --log-bounces: Log details only for dropped (bounced) key events.
    • --verbose: Enable DEBUG level internal logging.
    • RUST_LOG environment variable for fine-grained tracing filter control (overrides --verbose).
  • Periodic Reporting: Dump statistics periodically (--log-interval, default: 15m).
  • JSON Output: Output statistics in JSON format (--stats-json) for machine parsing.
  • Graceful Shutdown: Handles SIGINT, SIGTERM, SIGQUIT to ensure final statistics are reported.
  • Device Listing: List available input devices with keyboard capabilities (--list-devices).
  • Debugging Ring Buffer: Optionally store the last N passed events in memory for debugging complex issues (--ring-buffer-size).
  • OpenTelemetry Export: Optionally export metrics to an OTLP endpoint (--otel-endpoint).
  • Interception Tools Integration: Designed for use in standard Interception Tools pipelines (intercept | intercept-bounce | uinput).
  • Robust Testing: Includes unit tests, integration tests (assert_cmd), property tests (proptest), and fuzzing (cargo-fuzz).
  • Benchmarking: Core filter logic and channel communication can be benchmarked (cargo bench).

Installation

From Crates.io

cargo install intercept-bounce

From Source (Git Repository)

git clone https://github.com/sinity/intercept-bounce.git
cd intercept-bounce
cargo install --path .

Using Nix

With Nix flakes enabled:

# Build and run directly
nix run github:sinity/intercept-bounce -- --help

# Build the package
nix build github:sinity/intercept-bounce

# Install into your Nix profile
nix profile install github:sinity/intercept-bounce

The Nix flake also provides a development shell (nix develop) with necessary tools (see Development).

Usage

intercept-bounce is designed to be used within an Interception Tools pipeline.

Basic Pipeline

The most common usage involves capturing events from a physical keyboard, filtering them with intercept-bounce, and creating a new virtual keyboard with the filtered output using uinput.

# Find your keyboard device first (e.g., using 'intercept-bounce --list-devices' or 'intercept -L')
# Example device path: /dev/input/by-id/usb-My_Awesome_Keyboard-event-kbd

# Run the pipeline (requires root/sudo)
sudo sh -c 'intercept -g /dev/input/by-id/usb-My_Awesome_Keyboard-event-kbd \
           | intercept-bounce --debounce-time 15ms \
           | uinput -d /dev/input/by-id/usb-My_Awesome_Keyboard-event-kbd'

Important:

  1. Replace /dev/input/by-id/usb-My_Awesome_Keyboard-event-kbd with the actual path to your keyboard device. Using paths from /dev/input/by-id/ is recommended as they are stable.
  2. The device path provided to intercept -g must be the same as the one provided to uinput -d.
  3. This command creates a new virtual device. Your desktop environment (Xorg/Wayland) needs to use this new device instead of the original physical one. See the Integration section for details.

Using udevmon (part of Interception Tools) is the recommended way to manage the pipeline automatically when the device is connected/disconnected. Add a job to your /etc/interception/udevmon.yaml (or user-specific config):

- JOB: intercept -g $DEVNODE | intercept-bounce --debounce-time 15ms | uinput -d $DEVNODE
  DEVICE:
    LINK: /dev/input/by-id/usb-My_Awesome_Keyboard-event-kbd # <-- Change this!

Remember to replace the LINK with the correct path for your keyboard and restart the udevmon service (sudo systemctl restart interception-udevmon or similar).

Command-Line Options

Usage: intercept-bounce [OPTIONS]

Options:
  -t, --debounce-time <DURATION>
          Debounce time threshold (e.g., "25ms", "0.01s"). [default: 25ms]
      --near-miss-threshold-time <DURATION>
          Threshold for logging "near-miss" events (e.g., "100ms"). [default: 100ms]
      --log-interval <DURATION>
          Periodically dump statistics to stderr (e.g., "15m", "60s", "0s" to disable). [default: 15m]
      --log-all-events
          Log details of *every* incoming event ([PASS]/[DROP]).
      --log-bounces
          Log details of *only dropped* (bounced) key events.
      --list-devices
          List available input devices and their capabilities (requires root).
      --stats-json
          Output statistics as JSON format to stderr.
      --verbose
          Enable verbose logging (DEBUG level).
      --ring-buffer-size <SIZE>
          Size of the ring buffer for storing recently passed events (0 to disable). [default: 0]
      --otel-endpoint <URL>
          OTLP endpoint URL for exporting traces and metrics (e.g., "http://localhost:4317").
  -h, --help
          Print help
  -V, --version
          Print version

For detailed explanations of each option, see man intercept-bounce (if installed) or intercept-bounce --help.

How it Works

Debouncing

intercept-bounce filters key chatter by remembering the timestamp of the last passed event for each unique combination of key code (e.g., KEY_A) and key state (press=1, release=0).

  1. When a new key event arrives, its timestamp is compared to the last passed timestamp for the same key code and state.
  2. If the time difference is less than the configured --debounce-time, the new event is considered a bounce and is dropped.
  3. If the time difference is greater than or equal to the --debounce-time, or if the event has a different key code or state, the event is passed through, and its timestamp becomes the new "last passed" time for that specific key/state.
  4. Key repeat events (value=2) are always passed without debouncing.
  5. Non-key events (mouse, sync, etc.) are always passed.

Near-Miss Tracking

This feature helps diagnose keys with inconsistent timing just outside the debounce window.

  1. When a key event passes the debounce filter, the time difference since the previous passed event for the same key/state is calculated.
  2. If this difference is less than or equal to the --near-miss-threshold-time, the event is recorded as a "near-miss" in the statistics.
  3. High near-miss counts for a key might indicate a failing switch or that the --debounce-time needs adjustment.

Statistics

intercept-bounce collects detailed statistics, printed to stderr on exit (Ctrl+C) or periodically (--log-interval).

Human-Readable Format (Default)

  • Overall Statistics: Total key events processed, passed, dropped, and overall drop percentage.
  • Overall Histograms: Visual distribution of bounce timings and near-miss timings across all keys.
  • Dropped Event Statistics Per Key: For each key with activity:
    • Summary: Total processed, passed, dropped, drop %.
    • Details per state (Press/Release/Repeat): Processed, Passed, Dropped, Drop Rate (%), Bounce Time (Min/Avg/Max) if drops occurred.
  • Passed Event Near-Miss Statistics: For each key/state with near-misses: Count, Near-Miss Time (Min/Avg/Max).

JSON Format (--stats-json)

Provides a machine-readable JSON object containing all the information from the human-readable report, plus raw timing data arrays and detailed histogram bucket counts. Key top-level fields:

  • report_type: "Cumulative" or "Periodic".
  • runtime_us: Total runtime (cumulative only).
  • Configuration values (debounce_time_us, near_miss_threshold_us, etc.).
  • Overall counts (key_events_processed, key_events_passed, key_events_dropped).
  • overall_bounce_histogram, overall_near_miss_histogram: Detailed histogram objects.
  • per_key_stats: Array of objects per key, including detailed stats per state (press/release/repeat) with raw timings_us and bounce_histogram.
  • per_key_near_miss_stats: Array of objects per key/state with raw timings_us and near_miss_histogram.

Refer to the StatsCollector::print_stats_json implementation or the man page for the exact structure.

Histograms

Histograms show the distribution of timings (bounce or near-miss) in milliseconds across predefined buckets (e.g., <1ms, 1-2ms, 2-4ms, ..., >=128ms). They help visualize the typical duration of bounces or near-misses. The average timing is also calculated.

Logging

Logging messages are printed to stderr.

  • --log-all-events: Logs [PASS] or [DROP] for almost every event, showing type, code, value, key name, and timing info. (Skips EV_SYN/EV_MSC for clarity). Performance impact!
  • --log-bounces: Logs only [DROP] messages for key events, including bounce time. Less verbose than --log-all-events.
  • --verbose: Enables DEBUG level logging, showing internal state, thread activity, etc. Sets default filter to intercept_bounce=debug if RUST_LOG is not set.
  • RUST_LOG Environment Variable: Provides fine-grained control using the tracing_subscriber::EnvFilter format (e.g., RUST_LOG=info, RUST_LOG=intercept_bounce=trace, RUST_LOG=warn,intercept_bounce::filter=debug). Overrides --verbose.

Performance Note: High logging verbosity (--log-all-events, RUST_LOG=trace) can significantly impact performance and may cause log messages to be dropped if the logger thread cannot keep up. A warning ("Logger channel full...") will be printed if this happens.

Integration with Interception Tools

  • Pipeline: The standard usage is intercept -g <device> | intercept-bounce [OPTIONS] | uinput -d <device>.
  • udevmon: Recommended for managing the pipeline automatically. See Usage.
  • Virtual Device: uinput creates a new virtual input device (e.g., /dev/input/eventX). Your Desktop Environment (Xorg/Wayland) must be configured to use this new virtual device. The original physical device still emits raw events. Configuration methods vary; sometimes automatic, sometimes requiring DE-specific settings (e.g., Xorg InputClass sections, Wayland compositor settings). Use tools like libinput list-devices to identify the virtual device (often contains "Uinput" or "intercept-bounce" in the name).
  • Wayland/Xorg: Interception Tools generally work more reliably under Xorg. Wayland compositors often restrict global input grabbing. Using intercept-bounce under Wayland might require specific compositor support or configuration to recognize and prioritize the uinput virtual device.

Troubleshooting

  • Permission Denied: Running intercept and uinput requires root privileges or specific group memberships (input group for reading /dev/input/event*, potentially custom udev rules for /dev/uinput write access). Using sudo sh -c '...' for the whole pipeline is common. intercept-bounce --list-devices also needs read access.
  • Incorrect Device Path: Ensure the path used for intercept -g and uinput -d is identical and correct. Use stable paths from /dev/input/by-id/. Use intercept-bounce --list-devices or intercept -L to find devices.
  • Filter Not Working / No Output:
    • Check pipeline order and permissions.
    • Verify device paths match.
    • Check udevmon status and logs (sudo systemctl status interception-udevmon, journalctl -u interception-udevmon).
    • Run intercept-bounce with --verbose or --log-all-events to check processing and stderr for errors.
    • Confirm your DE is using the virtual device created by uinput.
  • Too Much Filtering (Missed Keystrokes): Lower --debounce-time.
  • Too Little Filtering (Chatter Still Occurs): Increase --debounce-time. Use --log-bounces or statistics (bounce timings/histograms) with a low debounce time first to measure the chatter duration, then set the time slightly higher.
  • Mixed Output in Terminal: Redirect stderr (2> log.txt) or use udevmon.
  • "Logger channel full..." Warning: Logger thread can't keep up (heavy logging, slow OTLP endpoint, high load). Log messages/stats may be lost. Reduce logging verbosity or disable OTLP if problematic.
  • JSON Stats Errors: Check stderr for non-JSON error messages printed before the JSON output.

Development

Building

cargo build
cargo build --release

Testing

# Run all tests (unit, integration, property)
cargo test --all-targets --all-features

# Run specific integration test
cargo test --test sanity -- --nocapture drops_bounce

# Run property tests only
cargo test --test property_tests

Benchmarking

cargo bench

Linting & Formatting

# Check formatting
cargo fmt --check

# Apply formatting
cargo fmt

# Run clippy lints
cargo clippy --all-targets --all-features -- -D warnings

xtasks

Common development tasks are available via cargo xtask:

# Generate man page and shell completions (in docs/)
cargo xtask generate-docs

# Run checks
cargo xtask check
cargo xtask test
cargo xtask clippy
cargo xtask fmt-check

Nix Development Shell

If you have Nix installed with flakes enabled, use nix develop to enter a shell with all necessary development tools (Rust toolchain, cargo-fuzz, cargo-audit, interception-tools, man, etc.) and useful aliases (ct for test, cl for clippy, cf for fmt, xt for xtask).

Fuzzing

Requires cargo-fuzz:

cargo install cargo-fuzz

# List fuzz targets
cargo fuzz list

# Run the stats fuzzer
cargo fuzz run fuzz_target_stats

Contributing

Contributions are welcome! Please feel free to open an issue or submit a pull request on GitHub.

License

Licensed under either of

at your option.

Dependencies

~15–24MB
~336K SLoC