1 unstable release
new 0.9.0 | May 6, 2025 |
---|
#1119 in Command line utilities
120KB
2K
SLoC
intercept-bounce
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-grainedtracing
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:
- 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. - The device path provided to
intercept -g
must be the same as the one provided touinput -d
. - 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.
udevmon Integration (Recommended)
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).
- When a new key event arrives, its timestamp is compared to the last passed timestamp for the same key code and state.
- If the time difference is less than the configured
--debounce-time
, the new event is considered a bounce and is dropped. - 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. - Key repeat events (value=2) are always passed without debouncing.
- 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.
- When a key event passes the debounce filter, the time difference since the previous passed event for the same key/state is calculated.
- 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. - 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 rawtimings_us
andbounce_histogram
.per_key_near_miss_stats
: Array of objects per key/state with rawtimings_us
andnear_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. (SkipsEV_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
: EnablesDEBUG
level logging, showing internal state, thread activity, etc. Sets default filter tointercept_bounce=debug
ifRUST_LOG
is not set.RUST_LOG
Environment Variable: Provides fine-grained control using thetracing_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., XorgInputClass
sections, Wayland compositor settings). Use tools likelibinput 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 theuinput
virtual device.
Troubleshooting
- Permission Denied: Running
intercept
anduinput
requires root privileges or specific group memberships (input
group for reading/dev/input/event*
, potentially custom udev rules for/dev/uinput
write access). Usingsudo 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
anduinput -d
is identical and correct. Use stable paths from/dev/input/by-id/
. Useintercept-bounce --list-devices
orintercept -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 useudevmon
. - "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
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Dependencies
~15–24MB
~336K SLoC