From 3b3d99c1ec8e96339593f4a2e84a609334708f19 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 11:05:45 -0400 Subject: [PATCH 01/55] Create config.py Reference for blacklist or command line arguments --- src/config.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/config.py diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..d217b2e --- /dev/null +++ b/src/config.py @@ -0,0 +1,43 @@ +""" +Constants for keyboard chattering filter configuration. + +PRECEDENCE RULES: +1. Command Line (--keys): Highest priority. If provided, this file is ignored. +2. This File (FILTERED_KEYS): Used if no command line argument is provided. +3. Empty: If BOTH the command line and this list are empty, ALL keys will be filtered. +""" + +# To filter specific keys, add them to this set. +# Example: FILTERED_KEYS = {"KEY_A", "KEY_SPACE", "KEY_ENTER"} +# Leave it empty as set() to filter ALL keys by default. +FILTERED_KEYS = set() + + +# ========================================== +# REFERENCE: COMMON KEY VALUES TO COPY/PASTE +# ========================================== +# Letters: +# KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, +# KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, +# KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z +# +# Numbers (Top Row): +# KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0, KEY_MINUS, KEY_EQUAL +# +# Numpad: +# KEY_KP0 to KEY_KP9, KEY_KPMINUS, KEY_KPPLUS, KEY_KPASTERISK, KEY_KPDOT, KEY_KPENTER +# +# Special/Control: +# KEY_SPACE, KEY_ENTER, KEY_BACKSPACE, KEY_TAB, KEY_ESC, KEY_CAPSLOCK +# +# Modifiers: +# KEY_LEFTSHIFT, KEY_RIGHTSHIFT, KEY_LEFTCTRL, KEY_RIGHTCTRL, KEY_LEFTALT, KEY_RIGHTALT, KEY_LEFTMETA (Super/Windows) +# +# Arrows & Navigation: +# KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_HOME, KEY_END, KEY_PAGEUP, KEY_PAGEDOWN, KEY_INSERT, KEY_DELETE +# +# Function Keys: +# KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12 +# +# Punctuation: +# KEY_LEFTBRACE, KEY_RIGHTBRACE, KEY_SEMICOLON, KEY_APOSTROPHE, KEY_GRAVE, KEY_BACKSLASH, KEY_COMMA, KEY_DOT, KEY_SLASH From bff5377cd0b19e73d613cfd218102131cea6c19e Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 11:10:33 -0400 Subject: [PATCH 02/55] Update filtering.py Core logic. Updated with the double-letter fix and specific key filtering. --- src/filtering.py | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/filtering.py b/src/filtering.py index 8fc185d..ee1c87c 100644 --- a/src/filtering.py +++ b/src/filtering.py @@ -1,36 +1,52 @@ import logging from collections import defaultdict -from typing import DefaultDict, Dict, NoReturn +from typing import DefaultDict, Dict, NoReturn, List +import time import libevdev -def filter_chattering(evdev: libevdev.Device, threshold: int) -> NoReturn: - # grab the device - now only we see the events it emits +def filter_chattering(evdev: libevdev.Device, threshold: int, keys_to_filter: List[libevdev.EventCode] = None) -> NoReturn: + # Add delay to allow the Enter key to release after executing the script via terminal + time.sleep(1) + + # Grab the device - now only we see the events it emits evdev.grab() - # create a copy of the device that we can write to - this will emit the filtered events to anyone who listens + + # Create a virtual uinput device - this will emit the filtered events to the OS ui_dev = evdev.create_uinput_device() logging.info("Listening to input events...") + if not keys_to_filter: + keys_to_filter = [] + while True: - # since the descriptor is blocking, this blocks until there are events available + # Descriptor is blocking; this waits until events are available for e in evdev.events(): - if _from_keystroke(e, threshold): + if _from_keystroke(e, threshold, keys_to_filter): ui_dev.send_events([e, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) -def _from_keystroke(event: libevdev.InputEvent, threshold: int) -> bool: - # no need to relay those - we are going to emit our own +def _from_keystroke(event: libevdev.InputEvent, threshold: int, keys_to_filter: List[libevdev.EventCode]) -> bool: + global _last_key_code + + # No need to relay sync/misc events - libevdev uinput handles syncing if event.matches(libevdev.EV_SYN) or event.matches(libevdev.EV_MSC): return False - # some events we don't want to filter, like EV_LED for toggling NumLock and the like, and also key hold events + # Fix for Modifiers/Combinations: If the event isn't a key, or it's a "hold" event (value > 1), forward it. + # Holding a key naturally spams events; we don't want to filter those. if not event.matches(libevdev.EV_KEY) or event.value > 1: logging.debug(f'FORWARDING {event.code}') return True - # the values are 0 for up, 1 for down and 2 for hold + # SPECIFIC KEY FILTERING: If the user provided specific keys to fix, and this isn't one of them, forward it. + if keys_to_filter and event.code not in keys_to_filter: + logging.debug(f'FORWARDING {event.code} (not in targeted filter list)') + return True + + # Values: 0 for Key Up, 1 for Key Down, 2 for Key Hold if event.value == 0: if _key_pressed[event.code]: logging.debug(f'FORWARDING {event.code} up') @@ -44,15 +60,19 @@ def _from_keystroke(event: libevdev.InputEvent, threshold: int) -> bool: prev = _last_key_up.get(event.code) now = event.sec * 1E6 + event.usec - if prev is None or now - prev > threshold * 1E3: + # We now check `_last_key_code != event.code`. + # If you type fast (e.g. e -> v -> e), the second 'e' won't be filtered just because it was fast, + # because 'v' was pressed in between! + if prev is None or now - prev > threshold * 1E3 or _last_key_code != event.code: logging.debug(f'FORWARDING {event.code} down') _key_pressed[event.code] = True + _last_key_code = event.code return True - logging.info( - f'FILTERED {event.code} down: last key up event happened {(now - prev) / 1E3} ms ago') + logging.info(f'FILTERED {event.code} down: last key up event happened {(now - prev) / 1E3} ms ago') return False _last_key_up: Dict[libevdev.EventCode, int] = {} -_key_pressed: DefaultDict[libevdev.EventCode, bool] = defaultdict(bool) \ No newline at end of file +_key_pressed: DefaultDict[libevdev.EventCode, bool] = defaultdict(bool) +_last_key_code = None From 423882ac9f10f93c54e1e598c7fb14d8c02d5708 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 11:11:49 -0400 Subject: [PATCH 03/55] Update __main__.py Entry point. Updated with CPU fix, config routing, and CLI parsing. --- src/__main__.py | 53 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/src/__main__.py b/src/__main__.py index 96fc06d..46da4fe 100755 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,6 +1,7 @@ import argparse import logging import sys +import os from contextlib import contextmanager import libevdev @@ -8,12 +9,26 @@ from src.filtering import filter_chattering from src.keyboard_retrieval import retrieve_keyboard_name, INPUT_DEVICES_PATH, abs_keyboard_path +# Import the config file. If missing/broken, default to empty safely. +try: + from src.config import FILTERED_KEYS +except ImportError: + FILTERED_KEYS = set() + @contextmanager def get_device_handle(keyboard_name: str) -> libevdev.Device: """ Safely get an evdev device handle. """ + device_path = abs_keyboard_path(keyboard_name) + + # If the physical keyboard is disconnected/undocked, the script + # used to crash and loop at 100% CPU. Now, it checks if the path exists. + # If not, it cleanly exits (status 0). Systemd will try to restart it later safely. + if not os.path.exists(device_path): + logging.critical(f"Keyboard device {keyboard_name} not connected. Exiting to prevent CPU loop.") + sys.exit(0) - fd = open(abs_keyboard_path(keyboard_name), 'rb') + fd = open(device_path, 'rb') evdev = libevdev.Device(fd) try: yield evdev @@ -21,6 +36,13 @@ def get_device_handle(keyboard_name: str) -> libevdev.Device: fd.close() +def parse_keys(keys_str): + """Parse a comma-separated list of keys into a list of strings.""" + if not keys_str: + return [] + return [key.strip() for key in keys_str.split(',')] + + if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('-k', '--keyboard', type=str, default=str(), @@ -28,6 +50,7 @@ def get_device_handle(keyboard_name: str) -> libevdev.Device: f"If left unset, will be attempted to be retrieved automatically.") parser.add_argument('-t', '--threshold', type=int, default=30, help="Filter time threshold in milliseconds. " "Default=30ms.") + parser.add_argument('--keys', type=parse_keys, default=[], help="Comma-separated list of keys to filter. Default All. e.g KEY_A,KEY_SPACE") parser.add_argument('-v', '--verbosity', type=int, default=1, choices=[0, 1, 2]) args = parser.parse_args() @@ -38,13 +61,33 @@ def get_device_handle(keyboard_name: str) -> libevdev.Device: 2: logging.DEBUG }[args.verbosity], handlers=[ - logging.StreamHandler( - sys.stdout - ) + logging.StreamHandler(sys.stdout) ], format="%(asctime)s - %(message)s", datefmt="%H:%M:%S" ) + # PRECEDENCE LOGIC FOR TARGETED KEYS: + # 1. Use --keys argument if provided. + # 2. Use src/config.py if no argument is provided. + # 3. If both are empty, list stays empty (Filter ALL keys natively). + keys_list = [] + if args.keys: + logging.info("Using targeted keys from command line argument --keys") + keys_list = args.keys + elif FILTERED_KEYS: + logging.info("Using targeted keys from src/config.py") + keys_list = list(FILTERED_KEYS) + else: + logging.info("No specific keys targeted. Filtering ALL keys.") + + # Convert requested string keys to libevdev.EventCode objects + keys_to_filter = [] + for key in keys_list: + try: + keys_to_filter.append(libevdev.evbit(key)) + except Exception as e: + logging.warning(f"Key '{key}' not recognized by libevdev and will be ignored. Error: {e}") + with get_device_handle(args.keyboard or retrieve_keyboard_name()) as device: - filter_chattering(device, args.threshold) + filter_chattering(device, args.threshold, keys_to_filter) From f7ebe34ead67a666ab5647248012f110d065da86 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 11:12:48 -0400 Subject: [PATCH 04/55] Update keyboard_retrieval.py --- src/keyboard_retrieval.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/keyboard_retrieval.py b/src/keyboard_retrieval.py index 057a750..f4ce1bc 100644 --- a/src/keyboard_retrieval.py +++ b/src/keyboard_retrieval.py @@ -3,23 +3,27 @@ from typing import Final INPUT_DEVICES_PATH: Final = '/dev/input/by-id' -_KEYBOARD_NAME_SUFFIX: Final = '-kbd' def retrieve_keyboard_name() -> str: - keyboard_devices = list(filter(lambda d: d.endswith(_KEYBOARD_NAME_SUFFIX), os.listdir(INPUT_DEVICES_PATH))) + # List all devices in the directory + all_devices = os.listdir(INPUT_DEVICES_PATH) + + # Remove duplicates just in case + keyboard_devices = list(set(all_devices)) n_devices = len(keyboard_devices) if n_devices == 0: raise ValueError(f"Couldn't find a keyboard in '{INPUT_DEVICES_PATH}'") + if n_devices == 1: logging.info(f"Found keyboard: {keyboard_devices[0]}") return keyboard_devices[0] - + # Use native Python input for user selection print("Select a device:") - for idx, device in enumerate(keyboard_devices, start=1): + for idx, device in enumerate(sorted(keyboard_devices), start=1): print(f"{idx}. {device}") - + selected_idx = -1 while selected_idx < 1 or selected_idx > n_devices: try: @@ -28,9 +32,8 @@ def retrieve_keyboard_name() -> str: print(f"Please select a number between 1 and {n_devices}") except ValueError: print("Please enter a valid number") - + return keyboard_devices[selected_idx - 1] def abs_keyboard_path(device: str) -> str: return os.path.join(INPUT_DEVICES_PATH, device) - From ebcdb71f59e044d15bde623ca3aac54016486831 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 11:13:52 -0400 Subject: [PATCH 05/55] Update chattering_fix.sh --- chattering_fix.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chattering_fix.sh b/chattering_fix.sh index b993122..15238a0 100644 --- a/chattering_fix.sh +++ b/chattering_fix.sh @@ -1,3 +1,6 @@ #!/bin/bash # Change the line below to the absolute path of the folder +# You can append `--keys KEY_A,KEY_SPACE` at the very end to ONLY filter those specific keys. +# (If using modern Python, you may need to run: sudo pip3 install -r requirements.txt --break-system-packages) + cd && sudo python3 -m src -k -t From e11d56a9da92e5a32024569b2c40bd5657e5a1ab Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:19:06 -0400 Subject: [PATCH 06/55] Create mouse_filtering.py This is the mouse version of the filter. It instantly forwards movement data so your cursor remains flawlessly smooth, and only applies the chatter filter to button clicks (like BTN_LEFT, BTN_RIGHT, etc.). --- src/mouse_filtering.py | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/mouse_filtering.py diff --git a/src/mouse_filtering.py b/src/mouse_filtering.py new file mode 100644 index 0000000..bc5f313 --- /dev/null +++ b/src/mouse_filtering.py @@ -0,0 +1,76 @@ +import logging +from collections import defaultdict +from typing import DefaultDict, Dict, NoReturn, List +import time + +import libevdev + +def filter_mouse_chattering(evdev: libevdev.Device, threshold: int, buttons_to_filter: List[libevdev.EventCode] = None) -> NoReturn: + # Small delay to ensure clean startup + time.sleep(1) + + # Grab the mouse device + evdev.grab() + + # Create the virtual uinput mouse + ui_dev = evdev.create_uinput_device() + + logging.info("Listening to mouse events...") + + if not buttons_to_filter: + buttons_to_filter = [] + + while True: + for e in evdev.events(): + if _from_click(e, threshold, buttons_to_filter): + ui_dev.send_events([e, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) + + +def _from_click(event: libevdev.InputEvent, threshold: int, buttons_to_filter: List[libevdev.EventCode]) -> bool: + global _last_btn_code + + if event.matches(libevdev.EV_SYN) or event.matches(libevdev.EV_MSC): + return False + + # CRITICAL MOUSE FIX: Immediately forward all relative (EV_REL) and absolute (EV_ABS) movement. + # This includes X/Y cursor movement and scroll wheel movement. Do not filter these! + if event.matches(libevdev.EV_REL) or event.matches(libevdev.EV_ABS): + return True + + # In Linux, mouse buttons are technically classified as EV_KEY. + # If it's not a button/key, just forward it. + if not event.matches(libevdev.EV_KEY): + return True + + # SPECIFIC BUTTON FILTERING (e.g., BTN_LEFT, BTN_RIGHT) + if buttons_to_filter and event.code not in buttons_to_filter: + logging.debug(f'FORWARDING {event.code} (not in targeted filter list)') + return True + + # Values: 0 for Button Up, 1 for Button Down + if event.value == 0: + if _btn_pressed[event.code]: + logging.debug(f'FORWARDING {event.code} up') + _last_btn_up[event.code] = event.sec * 1E6 + event.usec + _btn_pressed[event.code] = False + return True + else: + logging.info(f'FILTERING {event.code} up: button not pressed beforehand') + return False + + prev = _last_btn_up.get(event.code) + now = event.sec * 1E6 + event.usec + + # Check against the last button pressed so alternating clicks (Left -> Right -> Left) don't get filtered + if prev is None or now - prev > threshold * 1E3 or _last_btn_code != event.code: + logging.debug(f'FORWARDING {event.code} down') + _btn_pressed[event.code] = True + _last_btn_code = event.code + return True + + logging.info(f'FILTERED {event.code} down: last button up event happened {(now - prev) / 1E3} ms ago') + return False + +_last_btn_up: Dict[libevdev.EventCode, int] = {} +_btn_pressed: DefaultDict[libevdev.EventCode, bool] = defaultdict(bool) +_last_btn_code = None From 6b501dc3b10497daeae30e910f6b65cd51032dd1 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:19:58 -0400 Subject: [PATCH 07/55] Create mouse_retrieval.py Mouse devices end in -event-mouse. This file searches specifically for mice. --- src/mouse_retrieval.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/mouse_retrieval.py diff --git a/src/mouse_retrieval.py b/src/mouse_retrieval.py new file mode 100644 index 0000000..4cccd5f --- /dev/null +++ b/src/mouse_retrieval.py @@ -0,0 +1,37 @@ +import logging +import os +from typing import Final + +INPUT_DEVICES_PATH: Final = '/dev/input/by-id' + +def retrieve_mouse_name() -> str: + all_devices = os.listdir(INPUT_DEVICES_PATH) + + # Filter only for mouse devices + mouse_devices = list(set([d for d in all_devices if 'event-mouse' in d])) + n_devices = len(mouse_devices) + + if n_devices == 0: + raise ValueError(f"Couldn't find a mouse ending with 'event-mouse' in '{INPUT_DEVICES_PATH}'. You may need to provide it manually using -m.") + + if n_devices == 1: + logging.info(f"Found mouse: {mouse_devices[0]}") + return mouse_devices[0] + + print("Select a mouse device:") + for idx, device in enumerate(sorted(mouse_devices), start=1): + print(f"{idx}. {device}") + + selected_idx = -1 + while selected_idx < 1 or selected_idx > n_devices: + try: + selected_idx = int(input("Enter your choice (number): ")) + if selected_idx < 1 or selected_idx > n_devices: + print(f"Please select a number between 1 and {n_devices}") + except ValueError: + print("Please enter a valid number") + + return mouse_devices[selected_idx - 1] + +def abs_mouse_path(device: str) -> str: + return os.path.join(INPUT_DEVICES_PATH, device) From 6d9dba2a5a5649f0f2e4057dc7acfd0fa760a751 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:25:08 -0400 Subject: [PATCH 08/55] Create mouse_main.py This is the entry point for the mouse fix. You run this instead of python3 -m src. Read from the new mouse_config.py, exactly how we did for the keyboard. --- src/mouse_main.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/mouse_main.py diff --git a/src/mouse_main.py b/src/mouse_main.py new file mode 100644 index 0000000..8af8fc4 --- /dev/null +++ b/src/mouse_main.py @@ -0,0 +1,70 @@ +import argparse +import logging +import sys +import os +from contextlib import contextmanager + +import libevdev + +from src.mouse_filtering import filter_mouse_chattering +from src.mouse_retrieval import retrieve_mouse_name, INPUT_DEVICES_PATH, abs_mouse_path + +# Import the config file. If missing/broken, default to empty safely. +try: + from src.mouse_config import FILTERED_BUTTONS +except ImportError: + FILTERED_BUTTONS = set() + +@contextmanager +def get_device_handle(mouse_name: str) -> libevdev.Device: + device_path = abs_mouse_path(mouse_name) + + if not os.path.exists(device_path): + logging.critical(f"Mouse device {mouse_name} not connected. Exiting to prevent CPU loop.") + sys.exit(0) + + fd = open(device_path, 'rb') + evdev = libevdev.Device(fd) + try: + yield evdev + finally: + fd.close() + +def parse_buttons(buttons_str): + if not buttons_str: + return [] + return [btn.strip() for btn in buttons_str.split(',')] + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('-m', '--mouse', type=str, default=str(), + help=f"Name of your chattering mouse device as listed in {INPUT_DEVICES_PATH}.") + parser.add_argument('-t', '--threshold', type=int, default=30, help="Filter time threshold in milliseconds. Default=30ms.") + parser.add_argument('--buttons', type=parse_buttons, default=[], help="Comma-separated list of buttons to filter. e.g BTN_LEFT,BTN_RIGHT") + parser.add_argument('-v', '--verbosity', type=int, default=1, choices=[0, 1, 2]) + args = parser.parse_args() + + logging.basicConfig(level={0: logging.CRITICAL, 1: logging.INFO, 2: logging.DEBUG}[args.verbosity], + handlers=[logging.StreamHandler(sys.stdout)], + format="%(asctime)s - %(message)s", datefmt="%H:%M:%S") + + # PRECEDENCE LOGIC FOR TARGETED BUTTONS: + buttons_list = [] + if args.buttons: + logging.info("Using targeted buttons from command line argument --buttons") + buttons_list = args.buttons + elif FILTERED_BUTTONS: + logging.info("Using targeted buttons from src/mouse_config.py") + buttons_list = list(FILTERED_BUTTONS) + else: + logging.info("No specific buttons targeted. Filtering ALL buttons.") + + buttons_to_filter = [] + for btn in buttons_list: + try: + buttons_to_filter.append(libevdev.evbit(btn)) + except Exception as e: + logging.warning(f"Button '{btn}' not recognized by libevdev and will be ignored. Error: {e}") + + with get_device_handle(args.mouse or retrieve_mouse_name()) as device: + filter_mouse_chattering(device, args.threshold, buttons_to_filter) From 2b41225316d876d1578a66188d5a782d0fec835d Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:26:04 -0400 Subject: [PATCH 09/55] Create mouse_config.py This file holds the configuration for the mouse and lists all the possible button values you can pass to it. --- src/mouse_config.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/mouse_config.py diff --git a/src/mouse_config.py b/src/mouse_config.py new file mode 100644 index 0000000..d6fbf25 --- /dev/null +++ b/src/mouse_config.py @@ -0,0 +1,35 @@ +""" +Constants for mouse chattering filter configuration. + +PRECEDENCE RULES: +1. Command Line (--buttons): Highest priority. If provided, this file is ignored. +2. This File (FILTERED_BUTTONS): Used if no command line argument is provided. +3. Empty: If BOTH the command line and this list are empty, ALL buttons will be filtered. +""" + +# To filter specific buttons, add them to this set. +# Example: FILTERED_BUTTONS = {"BTN_LEFT", "BTN_SIDE"} +# Leave it empty as set() to filter ALL mouse buttons by default. +FILTERED_BUTTONS = set() + + +# ========================================== +# REFERENCE: COMMON MOUSE BUTTON VALUES +# ========================================== +# Standard Clicks: +# BTN_LEFT (Standard Left Click) +# BTN_RIGHT (Standard Right Click) +# BTN_MIDDLE (Scroll Wheel Click) +# +# Side / Gaming Buttons (Thumb buttons): +# BTN_SIDE (Often defaults to "Back" in browsers) +# BTN_EXTRA (Often defaults to "Forward" in browsers) +# BTN_FORWARD (Alternative Forward) +# BTN_BACK (Alternative Back) +# BTN_TASK (Sometimes used for DPI shifts or task views) +# +# Numbered Extra Buttons (For MMO mice like Razer Naga / Corsair Scimitar): +# BTN_0, BTN_1, BTN_2, BTN_3, BTN_4, BTN_5, BTN_6, BTN_7, BTN_8, BTN_9 +# +# Note: Scroll wheel *scrolling* (up/down) is treated as movement (EV_REL), +# not a button press, so it is natively bypassed by our script to prevent lag! From 4843e07065ceba292d285fcd13e11708e95324d5 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:27:14 -0400 Subject: [PATCH 10/55] Create mouse_fix.sh --- src/mouse_fix.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/mouse_fix.sh diff --git a/src/mouse_fix.sh b/src/mouse_fix.sh new file mode 100644 index 0000000..dd54f2b --- /dev/null +++ b/src/mouse_fix.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Change the line below to the absolute path of the folder +# You can append `--buttons BTN_LEFT,BTN_SIDE` at the very end to ONLY filter those specific buttons. + +cd && sudo python3 mouse_main.py -m -t From 4b34237560a5778481e724c305070fbc4d17d6f3 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:28:33 -0400 Subject: [PATCH 11/55] Delete src/mouse_fix.sh --- src/mouse_fix.sh | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 src/mouse_fix.sh diff --git a/src/mouse_fix.sh b/src/mouse_fix.sh deleted file mode 100644 index dd54f2b..0000000 --- a/src/mouse_fix.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -# Change the line below to the absolute path of the folder -# You can append `--buttons BTN_LEFT,BTN_SIDE` at the very end to ONLY filter those specific buttons. - -cd && sudo python3 mouse_main.py -m -t From b4b360bb790062e1c49906eed7f534476ac7f84d Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:29:34 -0400 Subject: [PATCH 12/55] Create mouse_chattering.sh --- mouse_chattering.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 mouse_chattering.sh diff --git a/mouse_chattering.sh b/mouse_chattering.sh new file mode 100644 index 0000000..dd54f2b --- /dev/null +++ b/mouse_chattering.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Change the line below to the absolute path of the folder +# You can append `--buttons BTN_LEFT,BTN_SIDE` at the very end to ONLY filter those specific buttons. + +cd && sudo python3 mouse_main.py -m -t From 567f5fa5b0de2c3828db80e3cb6d3c4dd25c4395 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:31:10 -0400 Subject: [PATCH 13/55] Create mouse_fix.service --- mouse_fix.service | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 mouse_fix.service diff --git a/mouse_fix.service b/mouse_fix.service new file mode 100644 index 0000000..b46e18e --- /dev/null +++ b/mouse_fix.service @@ -0,0 +1,12 @@ +[Unit] +Description=Mouse Chattering Fix service + +[Service] +# Change ExecStart to the absolute path of the file, executing mouse_fix.sh +ExecStart= + +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target From bb6e45c5690378e244bfe2bf454a9b59ccd64052 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:32:03 -0400 Subject: [PATCH 14/55] Rename chattering_fix.sh to keyboard_chattering.sh --- chattering_fix.sh => keyboard_chattering.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename chattering_fix.sh => keyboard_chattering.sh (100%) diff --git a/chattering_fix.sh b/keyboard_chattering.sh similarity index 100% rename from chattering_fix.sh rename to keyboard_chattering.sh From 558a87138bd03a336065bc199925ec074885da54 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:33:21 -0400 Subject: [PATCH 15/55] Rename chattering_fix.service to keyboard_chattering.service --- chattering_fix.service => keyboard_chattering.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename chattering_fix.service => keyboard_chattering.service (89%) diff --git a/chattering_fix.service b/keyboard_chattering.service similarity index 89% rename from chattering_fix.service rename to keyboard_chattering.service index 1306e9f..aca3460 100644 --- a/chattering_fix.service +++ b/keyboard_chattering.service @@ -9,4 +9,4 @@ Restart=always RestartSec=5 [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target From 0811deb43f810710014ab0b7220fb847e7bb9a76 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:33:41 -0400 Subject: [PATCH 16/55] Rename mouse_fix.service to mouse_chattering.service --- mouse_fix.service => mouse_chattering.service | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mouse_fix.service => mouse_chattering.service (100%) diff --git a/mouse_fix.service b/mouse_chattering.service similarity index 100% rename from mouse_fix.service rename to mouse_chattering.service From c01a759ddd5ea030f629aa04f962663d6efee956 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:34:18 -0400 Subject: [PATCH 17/55] Rename config.py to keyboard_config.py --- src/{config.py => keyboard_config.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{config.py => keyboard_config.py} (100%) diff --git a/src/config.py b/src/keyboard_config.py similarity index 100% rename from src/config.py rename to src/keyboard_config.py From c845b70a51e5ce38c771d02fa2f121065a4e0ae3 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:34:40 -0400 Subject: [PATCH 18/55] Rename filtering.py to keyboard_filtering.py --- src/{filtering.py => keyboard_filtering.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{filtering.py => keyboard_filtering.py} (100%) diff --git a/src/filtering.py b/src/keyboard_filtering.py similarity index 100% rename from src/filtering.py rename to src/keyboard_filtering.py From 65f321a87e372fe841a68fbcab2f2c79585d44cb Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:41:35 -0400 Subject: [PATCH 19/55] Update keyboard_filtering.py --- src/keyboard_filtering.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/keyboard_filtering.py b/src/keyboard_filtering.py index 6936fb9..b343484 100644 --- a/src/keyboard_filtering.py +++ b/src/keyboard_filtering.py @@ -4,25 +4,19 @@ import time import libevdev -import time + def filter_chattering(evdev: libevdev.Device, threshold: int, keys_to_filter: List[libevdev.EventCode] = None) -> NoReturn: - # Add delay to allow the Enter key to release after executing the script via terminal - time.sleep(1) - - # Grab the device - now only we see the events it emits + time.sleep(1) # Delay to allow Enter key to release natively evdev.grab() - - # Create a virtual uinput device - this will emit the filtered events to the OS ui_dev = evdev.create_uinput_device() - logging.info("Listening to input events...") + logging.info("Listening to keyboard input events...") if not keys_to_filter: keys_to_filter = [] while True: - # Descriptor is blocking; this waits until events are available for e in evdev.events(): if _from_keystroke(e, threshold, keys_to_filter): ui_dev.send_events([e, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) @@ -31,22 +25,19 @@ def filter_chattering(evdev: libevdev.Device, threshold: int, keys_to_filter: Li def _from_keystroke(event: libevdev.InputEvent, threshold: int, keys_to_filter: List[libevdev.EventCode]) -> bool: global _last_key_code - # No need to relay sync/misc events - libevdev uinput handles syncing if event.matches(libevdev.EV_SYN) or event.matches(libevdev.EV_MSC): return False - # Fix for Modifiers/Combinations: If the event isn't a key, or it's a "hold" event (value > 1), forward it. - # Holding a key naturally spams events; we don't want to filter those. + # Do not filter modifier combinations or held keys if not event.matches(libevdev.EV_KEY) or event.value > 1: logging.debug(f'FORWARDING {event.code}') return True - # SPECIFIC KEY FILTERING: If the user provided specific keys to fix, and this isn't one of them, forward it. + # SPECIFIC KEY FILTERING if keys_to_filter and event.code not in keys_to_filter: logging.debug(f'FORWARDING {event.code} (not in targeted filter list)') return True - # Values: 0 for Key Up, 1 for Key Down, 2 for Key Hold if event.value == 0: if _key_pressed[event.code]: logging.debug(f'FORWARDING {event.code} up') @@ -60,9 +51,7 @@ def _from_keystroke(event: libevdev.InputEvent, threshold: int, keys_to_filter: prev = _last_key_up.get(event.code) now = event.sec * 1E6 + event.usec - # We now check `_last_key_code != event.code`. - # If you type fast (e.g. e -> v -> e), the second 'e' won't be filtered just because it was fast, - # because 'v' was pressed in between! + # Check _last_key_code to prevent filtering fast alternating letters (e.g., e -> v -> e) if prev is None or now - prev > threshold * 1E3 or _last_key_code != event.code: logging.debug(f'FORWARDING {event.code} down') _key_pressed[event.code] = True @@ -72,7 +61,6 @@ def _from_keystroke(event: libevdev.InputEvent, threshold: int, keys_to_filter: logging.info(f'FILTERED {event.code} down: last key up event happened {(now - prev) / 1E3} ms ago') return False - _last_key_up: Dict[libevdev.EventCode, int] = {} _key_pressed: DefaultDict[libevdev.EventCode, bool] = defaultdict(bool) _last_key_code = None From 179ca9c19ecf122ce2a4715a5f414339408e73e2 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:42:31 -0400 Subject: [PATCH 20/55] Update keyboard_retrieval.py --- src/keyboard_retrieval.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/keyboard_retrieval.py b/src/keyboard_retrieval.py index b457e29..7a60fe4 100644 --- a/src/keyboard_retrieval.py +++ b/src/keyboard_retrieval.py @@ -1,14 +1,11 @@ import logging import os -from typing import Final, List +from typing import Final INPUT_DEVICES_PATH: Final = '/dev/input/by-id' def retrieve_keyboard_name() -> str: - # List all devices in the directory all_devices = os.listdir(INPUT_DEVICES_PATH) - - # Remove duplicates just in case keyboard_devices = list(set(all_devices)) n_devices = len(keyboard_devices) @@ -19,8 +16,7 @@ def retrieve_keyboard_name() -> str: logging.info(f"Found keyboard: {keyboard_devices[0]}") return keyboard_devices[0] - # Use native Python input for user selection - print("Select a device:") + print("Select a keyboard device:") for idx, device in enumerate(sorted(keyboard_devices), start=1): print(f"{idx}. {device}") From f1656656cac9267c03d833286c5a02febedf54d2 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:43:17 -0400 Subject: [PATCH 21/55] Update and rename __main__.py to keyboard_main.py --- src/__main__.py | 93 -------------------------------------------- src/keyboard_main.py | 56 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 93 deletions(-) delete mode 100755 src/__main__.py create mode 100755 src/keyboard_main.py diff --git a/src/__main__.py b/src/__main__.py deleted file mode 100755 index 46da4fe..0000000 --- a/src/__main__.py +++ /dev/null @@ -1,93 +0,0 @@ -import argparse -import logging -import sys -import os -from contextlib import contextmanager - -import libevdev - -from src.filtering import filter_chattering -from src.keyboard_retrieval import retrieve_keyboard_name, INPUT_DEVICES_PATH, abs_keyboard_path - -# Import the config file. If missing/broken, default to empty safely. -try: - from src.config import FILTERED_KEYS -except ImportError: - FILTERED_KEYS = set() - - -@contextmanager -def get_device_handle(keyboard_name: str) -> libevdev.Device: - """ Safely get an evdev device handle. """ - device_path = abs_keyboard_path(keyboard_name) - - # If the physical keyboard is disconnected/undocked, the script - # used to crash and loop at 100% CPU. Now, it checks if the path exists. - # If not, it cleanly exits (status 0). Systemd will try to restart it later safely. - if not os.path.exists(device_path): - logging.critical(f"Keyboard device {keyboard_name} not connected. Exiting to prevent CPU loop.") - sys.exit(0) - - fd = open(device_path, 'rb') - evdev = libevdev.Device(fd) - try: - yield evdev - finally: - fd.close() - - -def parse_keys(keys_str): - """Parse a comma-separated list of keys into a list of strings.""" - if not keys_str: - return [] - return [key.strip() for key in keys_str.split(',')] - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('-k', '--keyboard', type=str, default=str(), - help=f"Name of your chattering keyboard device as listed in {INPUT_DEVICES_PATH}. " - f"If left unset, will be attempted to be retrieved automatically.") - parser.add_argument('-t', '--threshold', type=int, default=30, help="Filter time threshold in milliseconds. " - "Default=30ms.") - parser.add_argument('--keys', type=parse_keys, default=[], help="Comma-separated list of keys to filter. Default All. e.g KEY_A,KEY_SPACE") - parser.add_argument('-v', '--verbosity', type=int, default=1, choices=[0, 1, 2]) - args = parser.parse_args() - - logging.basicConfig( - level={ - 0: logging.CRITICAL, - 1: logging.INFO, - 2: logging.DEBUG - }[args.verbosity], - handlers=[ - logging.StreamHandler(sys.stdout) - ], - format="%(asctime)s - %(message)s", - datefmt="%H:%M:%S" - ) - - # PRECEDENCE LOGIC FOR TARGETED KEYS: - # 1. Use --keys argument if provided. - # 2. Use src/config.py if no argument is provided. - # 3. If both are empty, list stays empty (Filter ALL keys natively). - keys_list = [] - if args.keys: - logging.info("Using targeted keys from command line argument --keys") - keys_list = args.keys - elif FILTERED_KEYS: - logging.info("Using targeted keys from src/config.py") - keys_list = list(FILTERED_KEYS) - else: - logging.info("No specific keys targeted. Filtering ALL keys.") - - # Convert requested string keys to libevdev.EventCode objects - keys_to_filter = [] - for key in keys_list: - try: - keys_to_filter.append(libevdev.evbit(key)) - except Exception as e: - logging.warning(f"Key '{key}' not recognized by libevdev and will be ignored. Error: {e}") - - with get_device_handle(args.keyboard or retrieve_keyboard_name()) as device: - filter_chattering(device, args.threshold, keys_to_filter) diff --git a/src/keyboard_main.py b/src/keyboard_main.py new file mode 100755 index 0000000..cc944d4 --- /dev/null +++ b/src/keyboard_main.py @@ -0,0 +1,56 @@ +import argparse +import logging +import sys +import os +from contextlib import contextmanager +import libevdev + +from src.keyboard_filtering import filter_chattering +from src.keyboard_retrieval import retrieve_keyboard_name, INPUT_DEVICES_PATH, abs_keyboard_path + +try: + from src.keyboard_config import FILTERED_KEYS +except ImportError: + FILTERED_KEYS = set() + +@contextmanager +def get_device_handle(keyboard_name: str) -> libevdev.Device: + device_path = abs_keyboard_path(keyboard_name) + if not os.path.exists(device_path): + logging.critical(f"Keyboard {keyboard_name} not connected. Exiting to prevent CPU loop.") + sys.exit(0) + + fd = open(device_path, 'rb') + evdev = libevdev.Device(fd) + try: + yield evdev + finally: + fd.close() + +def parse_keys(keys_str): + if not keys_str: return [] + return [key.strip() for key in keys_str.split(',')] + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('-k', '--keyboard', type=str, default=str()) + parser.add_argument('-t', '--threshold', type=int, default=30) + parser.add_argument('--keys', type=parse_keys, default=[]) + parser.add_argument('-v', '--verbosity', type=int, default=1, choices=[0, 1, 2]) + args = parser.parse_args() + + logging.basicConfig(level={0: logging.CRITICAL, 1: logging.INFO, 2: logging.DEBUG}[args.verbosity], + handlers=[logging.StreamHandler(sys.stdout)], + format="%(asctime)s - %(message)s", datefmt="%H:%M:%S") + + keys_list = args.keys if args.keys else list(FILTERED_KEYS) + keys_to_filter = [] + + for key in keys_list: + try: + keys_to_filter.append(libevdev.evbit(key)) + except Exception as e: + logging.warning(f"Key '{key}' ignored: {e}") + + with get_device_handle(args.keyboard or retrieve_keyboard_name()) as device: + filter_chattering(device, args.threshold, keys_to_filter) From f38bc08a2013691d15fc4f4d5fc4cd4c940ebdd3 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:44:10 -0400 Subject: [PATCH 22/55] Update mouse_filtering.py --- src/mouse_filtering.py | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/mouse_filtering.py b/src/mouse_filtering.py index bc5f313..195a556 100644 --- a/src/mouse_filtering.py +++ b/src/mouse_filtering.py @@ -2,17 +2,11 @@ from collections import defaultdict from typing import DefaultDict, Dict, NoReturn, List import time - import libevdev def filter_mouse_chattering(evdev: libevdev.Device, threshold: int, buttons_to_filter: List[libevdev.EventCode] = None) -> NoReturn: - # Small delay to ensure clean startup time.sleep(1) - - # Grab the mouse device evdev.grab() - - # Create the virtual uinput mouse ui_dev = evdev.create_uinput_device() logging.info("Listening to mouse events...") @@ -25,50 +19,40 @@ def filter_mouse_chattering(evdev: libevdev.Device, threshold: int, buttons_to_f if _from_click(e, threshold, buttons_to_filter): ui_dev.send_events([e, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) - def _from_click(event: libevdev.InputEvent, threshold: int, buttons_to_filter: List[libevdev.EventCode]) -> bool: global _last_btn_code if event.matches(libevdev.EV_SYN) or event.matches(libevdev.EV_MSC): return False - # CRITICAL MOUSE FIX: Immediately forward all relative (EV_REL) and absolute (EV_ABS) movement. - # This includes X/Y cursor movement and scroll wheel movement. Do not filter these! + # IMMEDIATELY FORWARD MOUSE MOVEMENT AND SCROLLING (EV_REL / EV_ABS) if event.matches(libevdev.EV_REL) or event.matches(libevdev.EV_ABS): return True - # In Linux, mouse buttons are technically classified as EV_KEY. - # If it's not a button/key, just forward it. - if not event.matches(libevdev.EV_KEY): + # If it isn't a button, or it's a held click natively, forward it + if not event.matches(libevdev.EV_KEY) or event.value > 1: return True - # SPECIFIC BUTTON FILTERING (e.g., BTN_LEFT, BTN_RIGHT) if buttons_to_filter and event.code not in buttons_to_filter: - logging.debug(f'FORWARDING {event.code} (not in targeted filter list)') return True - # Values: 0 for Button Up, 1 for Button Down if event.value == 0: if _btn_pressed[event.code]: - logging.debug(f'FORWARDING {event.code} up') _last_btn_up[event.code] = event.sec * 1E6 + event.usec _btn_pressed[event.code] = False return True else: - logging.info(f'FILTERING {event.code} up: button not pressed beforehand') return False prev = _last_btn_up.get(event.code) now = event.sec * 1E6 + event.usec - # Check against the last button pressed so alternating clicks (Left -> Right -> Left) don't get filtered if prev is None or now - prev > threshold * 1E3 or _last_btn_code != event.code: - logging.debug(f'FORWARDING {event.code} down') _btn_pressed[event.code] = True _last_btn_code = event.code return True - logging.info(f'FILTERED {event.code} down: last button up event happened {(now - prev) / 1E3} ms ago') + logging.info(f'FILTERED {event.code} down: last up event {(now - prev) / 1E3} ms ago') return False _last_btn_up: Dict[libevdev.EventCode, int] = {} From 6aeb24223e0105c1c2c6a61836c0257f01422a7e Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:44:52 -0400 Subject: [PATCH 23/55] Update mouse_retrieval.py --- src/mouse_retrieval.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/mouse_retrieval.py b/src/mouse_retrieval.py index 4cccd5f..7cf49ab 100644 --- a/src/mouse_retrieval.py +++ b/src/mouse_retrieval.py @@ -6,13 +6,11 @@ def retrieve_mouse_name() -> str: all_devices = os.listdir(INPUT_DEVICES_PATH) - - # Filter only for mouse devices mouse_devices = list(set([d for d in all_devices if 'event-mouse' in d])) n_devices = len(mouse_devices) if n_devices == 0: - raise ValueError(f"Couldn't find a mouse ending with 'event-mouse' in '{INPUT_DEVICES_PATH}'. You may need to provide it manually using -m.") + raise ValueError(f"Couldn't find a mouse ending with 'event-mouse'. Please provide it manually with -m.") if n_devices == 1: logging.info(f"Found mouse: {mouse_devices[0]}") From 33c7e80408fd05ec546dfc48a139512780f61494 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:45:56 -0400 Subject: [PATCH 24/55] Update mouse_main.py --- src/mouse_main.py | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/mouse_main.py b/src/mouse_main.py index 8af8fc4..4fce8dd 100644 --- a/src/mouse_main.py +++ b/src/mouse_main.py @@ -3,13 +3,11 @@ import sys import os from contextlib import contextmanager - import libevdev from src.mouse_filtering import filter_mouse_chattering from src.mouse_retrieval import retrieve_mouse_name, INPUT_DEVICES_PATH, abs_mouse_path -# Import the config file. If missing/broken, default to empty safely. try: from src.mouse_config import FILTERED_BUTTONS except ImportError: @@ -18,9 +16,8 @@ @contextmanager def get_device_handle(mouse_name: str) -> libevdev.Device: device_path = abs_mouse_path(mouse_name) - if not os.path.exists(device_path): - logging.critical(f"Mouse device {mouse_name} not connected. Exiting to prevent CPU loop.") + logging.critical(f"Mouse {mouse_name} not connected. Exiting to prevent CPU loop.") sys.exit(0) fd = open(device_path, 'rb') @@ -31,16 +28,14 @@ def get_device_handle(mouse_name: str) -> libevdev.Device: fd.close() def parse_buttons(buttons_str): - if not buttons_str: - return [] + if not buttons_str: return [] return [btn.strip() for btn in buttons_str.split(',')] if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('-m', '--mouse', type=str, default=str(), - help=f"Name of your chattering mouse device as listed in {INPUT_DEVICES_PATH}.") - parser.add_argument('-t', '--threshold', type=int, default=30, help="Filter time threshold in milliseconds. Default=30ms.") - parser.add_argument('--buttons', type=parse_buttons, default=[], help="Comma-separated list of buttons to filter. e.g BTN_LEFT,BTN_RIGHT") + parser.add_argument('-m', '--mouse', type=str, default=str()) + parser.add_argument('-t', '--threshold', type=int, default=30) + parser.add_argument('--buttons', type=parse_buttons, default=[]) parser.add_argument('-v', '--verbosity', type=int, default=1, choices=[0, 1, 2]) args = parser.parse_args() @@ -48,23 +43,14 @@ def parse_buttons(buttons_str): handlers=[logging.StreamHandler(sys.stdout)], format="%(asctime)s - %(message)s", datefmt="%H:%M:%S") - # PRECEDENCE LOGIC FOR TARGETED BUTTONS: - buttons_list = [] - if args.buttons: - logging.info("Using targeted buttons from command line argument --buttons") - buttons_list = args.buttons - elif FILTERED_BUTTONS: - logging.info("Using targeted buttons from src/mouse_config.py") - buttons_list = list(FILTERED_BUTTONS) - else: - logging.info("No specific buttons targeted. Filtering ALL buttons.") - + buttons_list = args.buttons if args.buttons else list(FILTERED_BUTTONS) buttons_to_filter = [] + for btn in buttons_list: try: buttons_to_filter.append(libevdev.evbit(btn)) except Exception as e: - logging.warning(f"Button '{btn}' not recognized by libevdev and will be ignored. Error: {e}") + logging.warning(f"Button '{btn}' ignored: {e}") with get_device_handle(args.mouse or retrieve_mouse_name()) as device: filter_mouse_chattering(device, args.threshold, buttons_to_filter) From d413d3f542edd88ccde70930466792093ee480a1 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:53:48 -0400 Subject: [PATCH 25/55] Update keyboard_chattering.sh --- keyboard_chattering.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/keyboard_chattering.sh b/keyboard_chattering.sh index 15238a0..0f4ca04 100644 --- a/keyboard_chattering.sh +++ b/keyboard_chattering.sh @@ -2,5 +2,4 @@ # Change the line below to the absolute path of the folder # You can append `--keys KEY_A,KEY_SPACE` at the very end to ONLY filter those specific keys. # (If using modern Python, you may need to run: sudo pip3 install -r requirements.txt --break-system-packages) - -cd && sudo python3 -m src -k -t +cd && sudo python3 -m src.keyboard_main -k -t 30 From 3d66341affa00549998c57e3b3a6e3a97100c65f Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:54:24 -0400 Subject: [PATCH 26/55] Update mouse_chattering.sh --- mouse_chattering.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mouse_chattering.sh b/mouse_chattering.sh index dd54f2b..cf8967d 100644 --- a/mouse_chattering.sh +++ b/mouse_chattering.sh @@ -2,4 +2,4 @@ # Change the line below to the absolute path of the folder # You can append `--buttons BTN_LEFT,BTN_SIDE` at the very end to ONLY filter those specific buttons. -cd && sudo python3 mouse_main.py -m -t +cd && sudo python3 -m src.mouse_main -m -t 30 From 6071ce548f7fa32e51cede680df1c3ed86807741 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 13:55:21 -0400 Subject: [PATCH 27/55] Update mouse_chattering.sh --- mouse_chattering.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mouse_chattering.sh b/mouse_chattering.sh index cf8967d..f79bfb6 100644 --- a/mouse_chattering.sh +++ b/mouse_chattering.sh @@ -1,5 +1,5 @@ #!/bin/bash # Change the line below to the absolute path of the folder # You can append `--buttons BTN_LEFT,BTN_SIDE` at the very end to ONLY filter those specific buttons. - +# (If using modern Python, you may need to run: sudo pip3 install -r requirements.txt --break-system-packages) cd && sudo python3 -m src.mouse_main -m -t 30 From 32903fa1f911228f90667834b836cd5e74432918 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:00:34 -0400 Subject: [PATCH 28/55] Update mouse_chattering.service --- mouse_chattering.service | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mouse_chattering.service b/mouse_chattering.service index b46e18e..d300b5e 100644 --- a/mouse_chattering.service +++ b/mouse_chattering.service @@ -1,9 +1,9 @@ [Unit] -Description=Mouse Chattering Fix service +Description=Mouse Chattering service [Service] -# Change ExecStart to the absolute path of the file, executing mouse_fix.sh -ExecStart= +# Change ExecStart to the absolute path of the file, executing mouse_chattering.sh +ExecStart= Restart=always RestartSec=5 From b107a6b16a7e680d005060afe0a5d898faca6f4e Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:01:13 -0400 Subject: [PATCH 29/55] Update keyboard_chattering.service --- keyboard_chattering.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keyboard_chattering.service b/keyboard_chattering.service index aca3460..a51856b 100644 --- a/keyboard_chattering.service +++ b/keyboard_chattering.service @@ -2,8 +2,8 @@ Description=Keyboard Chattering Fix service [Service] -# Change ExecStart to the absolute path of the file, executing chattering_fix.sh -ExecStart= +# Change ExecStart to the absolute path of the file, executing keyboard_chattering.sh +ExecStart= Restart=always RestartSec=5 From 193105bfb4207586f81904d774f1e0c63248949f Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:16:24 -0400 Subject: [PATCH 30/55] Update README.md --- README.md | 68 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 6deec25..144d90c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# __Keyboard Chattering Fix for Linux__ +# __Keyboard & Mouse Chattering Fix for Linux__ [![GitHub](https://img.shields.io/github/license/w2sv/KeyboardChatteringFix-Linux?)](LICENSE) -__A tool for filtering mechanical keyboard chattering on Linux__ +__A tool for filtering mechanical keyboard and mouse chattering on Linux__ ## The problem Switches on mechanical keyboards occasionally start to "chatter", meaning when you press a key with a faulty switch it erroneously detects -two or even more key presses. +two or even more key presses. Similarly, mechanical switches on mice (especially gaming mice) frequently develop "double-click" issues where a single click registers as multiple clicks. ## The existing solutions -Apart from buying a new keyboard, there have been ways to deal +Apart from buying a new keyboard or mouse, there have been ways to deal with this problem using software methods. The idea is to filter key presses that occur faster than a certain threshold. "Keyboard Chattering Fix v 0.0.1" is a tool I had been using on Windows for a long time, and these days you also have @@ -30,13 +30,15 @@ this is bound to happen eventually and interfere with fast repeated key presses. ## This project's solution This tool attempts to solve any such problems that may arise by having full low-level access -and control over all keyboard events. -Using `libevdev`'s Python bindings, it grabs your keyboard's event device and processes its events, -then outputs the result back to the system using `/dev/uinput`, effectively emulating a keyboard - +and control over all input events. +Using `libevdev`'s Python bindings, it grabs your keyboard's (or mouse's) event device and processes its events, +then outputs the result back to the system using `/dev/uinput`, effectively emulating a keyboard or mouse - one that doesn't chatter, unlike your real one! This also means it works across the system, without depending on X. +*Note for Mice:* To ensure your mouse cursor remains flawlessly smooth, this tool uses separate logic for mice. It natively bypasses X/Y cursor movement and scrolling, applying the chatter filter *only* to physical button clicks. + As for the filtering rule, what seems to work well is the time between the last key up event and the current key down event. When the key chatters, that time seems to be very low - around 10 ms. By filtering such anomalies, we can hopefully remove chatter without impeding actual fast key presses. @@ -45,16 +47,24 @@ By filtering such anomalies, we can hopefully remove chatter without impeding ac Download the repository as a zip and extract the file. The dependencies are listed in the requirements.txt. And you can install it with the command below. +*(Note: According to PEP 668, newer Linux distributions may require the `--break-system-packages` flag, or the use of a python `venv`)*. + ```shell -sudo pip3 install -r requirements.txt +sudo pip3 install -r requirements.txt --break-system-packages ``` ## Usage -`cd` inside the location of the KeyboardChatteringFix-Linux-master extracted folder and enter the command below to run. +`cd` inside the location of the extracted folder. Because keyboards and mice are handled differently by the OS, they are executed as separate modules. Enter the commands below to run them. + +**For Keyboard:** +```shell +sudo python3 -m src.keyboard_main +``` +**For Mouse:** ```shell -sudo python3 -m src +sudo python3 -m src.mouse_main ``` ### Customization Options @@ -63,39 +73,47 @@ sudo python3 -m src - Name of your chattering keyboard device as listed in /dev/input/by-id. If left unset, will be attempted to be retrieved automatically. The device is captured `by-id`, and therefore in a persistent way. +- -m MOUSE, --mouse MOUSE + - Name of your chattering mouse device. Works identically to the keyboard argument above. + - -t THRESHOLD, --threshold THRESHOLD - Filter time threshold in milliseconds. Default=30ms. Note: This does not denote the time between key presses, but - between a key being - released and pressed again, so the number should probably be lower than you might think. For reference, if you - press the key really fast this delay is around 50 ms. + between a key being released and pressed again, so the number should probably be lower than you might think. For reference, if you press the key really fast this delay is around 50 ms. + +- --keys KEYS (For Keyboard) + - Comma-separated list of specific keys to filter (e.g., `KEY_A,KEY_SPACE`). If provided, *only* these keys will be filtered, leaving the rest of your keyboard untouched. You can also permanently define these in `src/keyboard_config.py`. + +- --buttons BUTTONS (For Mouse) + - Comma-separated list of specific buttons to filter (e.g., `BTN_LEFT,BTN_RIGHT`). You can also permanently define these in `src/mouse_config.py`. - -v {0,1,2}, --verbosity {0,1,2} ## Automation Starting the script manually every time doesn't sound like the greatest idea, so -you should probably consider something that does it for you. Modify the `chattering_fix.sh` to `cd` into the absolute path of the downloaded folder and input the keyboard id and the desired threshold. For example: +you should probably consider something that does it for you. Modify `keyboard_chattering.sh` and/or `mouse_chattering.sh` to `cd` into the absolute path of the downloaded folder and input the device id and the desired threshold. For example: ```shell -cd /home/foouser/Downloads/KeyboardChatteringFix-Linux-master/ && sudo python3 -m src -k usb-SINO_WEALTH_USB_KEYBOARD-event-kbd -t 50 +cd /home/foouser/Downloads/KeyboardChatteringFix-Linux-master/ && sudo python3 -m src.keyboard_main -k usb-SINO_WEALTH_USB_KEYBOARD-event-kbd -t 50 ``` -Also, make sure to change the file permission of `chattering_fix.sh` so that it is executable. +Also, make sure to change the file permission of the `.sh` scripts so that they are executable. ```shell -chmod +x chattering_fix.sh +chmod +x keyboard_chattering.sh mouse_chattering.sh ``` -The `chattering_fix.service` file should also be edited. The `ExecStart` should be the absolute path of the `chattering_fix.sh`. For example: +The `.service` files should also be edited. The `ExecStart` should be the absolute path of the respective `.sh` file. For example: ```shell -ExecStart=/home/foouser/Downloads/KeyboardChatteringFix-Linux-master/chattering_fix.sh +ExecStart=/home/foouser/Downloads/KeyboardChatteringFix-Linux-master/keyboard_chattering.sh ``` -Then, copy the `chattering_fix.service` to `/etc/systemd/system/` and enable it with the command below. +Then, copy the `.service` files to `/etc/systemd/system/` and enable them with the commands below. ```shell -systemctl enable --now chattering_fix +sudo systemctl enable --now keyboard_chattering +sudo systemctl enable --now mouse_chattering ``` -You can check if the systemd unit file is properly working using +You can check if the systemd unit files are properly working using ```shell -systemctl status chattering_fix.service +systemctl status keyboard_chattering.service ``` You can also use ```shell -journalctl -xeu chattering_fix.service +journalctl -xeu keyboard_chattering.service ``` -just to make sure that there are no errors. \ No newline at end of file +just to make sure that there are no errors. *(Note: If your device disconnects or goes to sleep, the service will safely pause and wait for it to reconnect without consuming CPU).* From d72fb357def1769984fd409121b09d2223d43c30 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:24:04 -0400 Subject: [PATCH 31/55] Update README.md --- README.md | 135 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 76 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 144d90c..c7e594d 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,32 @@ -# __Keyboard & Mouse Chattering Fix for Linux__ +# __Keyboard Chattering & Mouse Double-Click Fix for Linux__ [![GitHub](https://img.shields.io/github/license/w2sv/KeyboardChatteringFix-Linux?)](LICENSE) -__A tool for filtering mechanical keyboard and mouse chattering on Linux__ +__A tool for filtering mechanical keyboard chattering and mouse double-clicking on Linux__ ## The problem -Switches on mechanical keyboards occasionally start to "chatter", -meaning when you press a key with a faulty switch it erroneously detects -two or even more key presses. Similarly, mechanical switches on mice (especially gaming mice) frequently develop "double-click" issues where a single click registers as multiple clicks. +Switches on mechanical keyboards occasionally start to "chatter" or "bounce", meaning when you press a key with a faulty switch it erroneously detects two or even more key presses. Similarly, mechanical switches on mice (especially gaming mice) frequently develop "double-click" issues where a single physical click registers as multiple rapid clicks. ## The existing solutions -Apart from buying a new keyboard or mouse, there have been ways to deal -with this problem using software methods. The idea is to filter key presses -that occur faster than a certain threshold. "Keyboard Chattering Fix v 0.0.1" -is a tool I had been using on Windows for a long time, and these days you also have -[Keyboard Chatter Blocker](https://github.com/mcmonkeyprojects/KeyboardChatterBlocker), -which is a nice open source tool with some additional functionality. It's actually what -I use myself when I use Windows. - -Unfortunately, all existing tools only work on Windows. -On Linux, the answer everyone seems to give is to use the Bounce Keys feature of X, -but it's not really useful in this way. For one, it resets the delay even on filtered -key presses, meaning that if you press the key fast enough, -*none* of the presses with pass through, ever. And if the key chatters, -this is bound to happen eventually and interfere with fast repeated key presses. +Apart from buying new hardware, there have been ways to deal with this problem using software methods. The idea is to filter inputs that occur faster than a certain threshold. "Keyboard Chattering Fix v 0.0.1" is a tool I had been using on Windows for a long time, and these days you also have [Keyboard Chatter Blocker](https://github.com/mcmonkeyprojects/KeyboardChatterBlocker). + +Unfortunately, all existing tools only work on Windows. On Linux, the answer everyone seems to give is to use the Bounce Keys feature of X, but it's not really useful in this way. For one, it resets the delay even on filtered key presses, meaning that if you press the key fast enough, *none* of the presses will pass through, ever. ## This project's solution -This tool attempts to solve any such problems that may arise by having full low-level access -and control over all input events. -Using `libevdev`'s Python bindings, it grabs your keyboard's (or mouse's) event device and processes its events, -then outputs the result back to the system using `/dev/uinput`, effectively emulating a keyboard or mouse - -one that doesn't chatter, unlike your real one! +This tool attempts to solve these hardware problems by having full low-level access and control over all input events. Using `libevdev`'s Python bindings, it grabs your device and processes its events, then outputs the result back to the system using `/dev/uinput`. This effectively emulates a flawless keyboard and mouse that doesn't chatter or double-click, unlike your real ones! -This also means it works across the system, without depending on X. +This also means it works across the whole system, without depending on X11 or Wayland. -*Note for Mice:* To ensure your mouse cursor remains flawlessly smooth, this tool uses separate logic for mice. It natively bypasses X/Y cursor movement and scrolling, applying the chatter filter *only* to physical button clicks. +*Note for Mice:* To ensure your mouse cursor remains flawlessly smooth, this tool uses completely separate logic for mice. It natively bypasses X/Y cursor movement and scrolling, applying the chatter filter *only* to physical button clicks (Left click, Right click, Side buttons, etc.). -As for the filtering rule, what seems to work well is the time between the last key up event -and the current key down event. When the key chatters, that time seems to be very low - around 10 ms. -By filtering such anomalies, we can hopefully remove chatter without impeding actual fast key presses. +As for the filtering rule, what seems to work well is the time between the last "key up" event and the current "key down" event. When the switch chatters, that time is very low - around 10 ms. By filtering such anomalies, we remove chatter without impeding actual fast typing or clicking. ## Installation -Download the repository as a zip and extract the file. The dependencies are listed in the requirements.txt. And you can install it with the command below. +Download the repository and extract the files. The dependencies are listed in `requirements.txt`. You can install them with the command below. *(Note: According to PEP 668, newer Linux distributions may require the `--break-system-packages` flag, or the use of a python `venv`)*. @@ -55,65 +36,101 @@ sudo pip3 install -r requirements.txt --break-system-packages ## Usage -`cd` inside the location of the extracted folder. Because keyboards and mice are handled differently by the OS, they are executed as separate modules. Enter the commands below to run them. +`cd` inside the location of the extracted folder. Because keyboards and mice are handled differently by the OS, they are executed as separate modules. Enter the commands below to run them manually: -**For Keyboard:** +**To run the Keyboard fix:** ```shell sudo python3 -m src.keyboard_main ``` -**For Mouse:** +**To run the Mouse fix:** ```shell sudo python3 -m src.mouse_main ``` ### Customization Options -- -k KEYBOARD, --keyboard KEYBOARD - - Name of your chattering keyboard device as listed in /dev/input/by-id. If left unset, will be attempted to be retrieved - automatically. The device is captured `by-id`, and therefore in a persistent way. +- `-k KEYBOARD`, `--keyboard KEYBOARD` + - Name of your chattering keyboard device as listed in `/dev/input/by-id`. If left unset, it will attempt to retrieve it automatically. +- `-m MOUSE`, `--mouse MOUSE` + - Name of your double-clicking mouse device. Works identically to the keyboard argument above. +- `-t THRESHOLD`, `--threshold THRESHOLD` + - Filter time threshold in milliseconds. Default=30ms. Note: This denotes the time between a key/button being *released* and pressed again. For reference, if you click really fast, this delay is around 50 ms. +- `--keys KEYS` (For Keyboard) + - Comma-separated list of specific keys to filter (e.g., `KEY_A,KEY_SPACE`). If provided, *only* these keys will be filtered, leaving the rest of your keyboard untouched. You can also permanently define these in `src/keyboard_config.py`. +- `--buttons BUTTONS` (For Mouse) + - Comma-separated list of specific buttons to filter (e.g., `BTN_LEFT,BTN_RIGHT`). You can also permanently define these in `src/mouse_config.py`. +- `-v {0,1,2}`, `--verbosity {0,1,2}` -- -m MOUSE, --mouse MOUSE - - Name of your chattering mouse device. Works identically to the keyboard argument above. +## Automation -- -t THRESHOLD, --threshold THRESHOLD - - Filter time threshold in milliseconds. Default=30ms. Note: This does not denote the time between key presses, but - between a key being released and pressed again, so the number should probably be lower than you might think. For reference, if you press the key really fast this delay is around 50 ms. +Starting the scripts manually every time is not ideal. You should set them up as background Systemd services. Because the keyboard and mouse scripts are separate, they can run concurrently in the background without interfering with one another. -- --keys KEYS (For Keyboard) - - Comma-separated list of specific keys to filter (e.g., `KEY_A,KEY_SPACE`). If provided, *only* these keys will be filtered, leaving the rest of your keyboard untouched. You can also permanently define these in `src/keyboard_config.py`. +### Step 1: Configure the shell scripts +Modify `keyboard_chattering.sh` and/or `mouse_chattering.sh` to `cd` into the absolute path of your downloaded folder, and input your device IDs and desired thresholds. -- --buttons BUTTONS (For Mouse) - - Comma-separated list of specific buttons to filter (e.g., `BTN_LEFT,BTN_RIGHT`). You can also permanently define these in `src/mouse_config.py`. +**Example `keyboard_chattering.sh`:** +```shell +cd /home/foouser/Downloads/HardwareChatteringFix-Linux/ && sudo python3 -m src.keyboard_main -k usb-SINO_WEALTH_USB_KEYBOARD-event-kbd -t 40 --keys KEY_E,KEY_SPACE +``` -- -v {0,1,2}, --verbosity {0,1,2} +**Example `mouse_chattering.sh`:** +```shell +cd /home/foouser/Downloads/HardwareChatteringFix-Linux/ && sudo python3 -m src.mouse_main -m usb-Logitech_Gaming_Mouse-event-mouse -t 50 --buttons BTN_LEFT,BTN_RIGHT +``` -## Automation +Make sure to change the file permissions so they are executable: +```shell +chmod +x keyboard_chattering.sh mouse_chattering.sh +``` + +### Step 2: Configure the service files +Edit `keyboard_chattering.service` and `mouse_chattering.service`. The `ExecStart` should be the absolute path of the respective `.sh` file. + +**Example:** +```shell +ExecStart=/home/foouser/Downloads/HardwareChatteringFix-Linux/keyboard_chattering.sh +``` -Starting the script manually every time doesn't sound like the greatest idea, so -you should probably consider something that does it for you. Modify `keyboard_chattering.sh` and/or `mouse_chattering.sh` to `cd` into the absolute path of the downloaded folder and input the device id and the desired threshold. For example: +### Step 3: Enable the Services (Separately or Combined) + +Copy the `.service` files to your systemd folder: ```shell -cd /home/foouser/Downloads/KeyboardChatteringFix-Linux-master/ && sudo python3 -m src.keyboard_main -k usb-SINO_WEALTH_USB_KEYBOARD-event-kbd -t 50 +sudo cp keyboard_chattering.service /etc/systemd/system/ +sudo cp mouse_chattering.service /etc/systemd/system/ ``` -Also, make sure to change the file permission of the `.sh` scripts so that they are executable. + +**To enable ONLY the keyboard fix:** ```shell -chmod +x keyboard_chattering.sh mouse_chattering.sh +sudo systemctl enable --now keyboard_chattering ``` -The `.service` files should also be edited. The `ExecStart` should be the absolute path of the respective `.sh` file. For example: + +**To enable ONLY the mouse fix:** ```shell -ExecStart=/home/foouser/Downloads/KeyboardChatteringFix-Linux-master/keyboard_chattering.sh +sudo systemctl enable --now mouse_chattering ``` -Then, copy the `.service` files to `/etc/systemd/system/` and enable them with the commands below. + +**To run BOTH concurrently:** +Simply run both enable commands! They operate completely independently of one another. ```shell sudo systemctl enable --now keyboard_chattering sudo systemctl enable --now mouse_chattering ``` -You can check if the systemd unit files are properly working using + +### Step 4: Checking Status and Logs + +You can check if the scripts are running properly by checking their independent statuses: + +**For the Keyboard:** ```shell systemctl status keyboard_chattering.service +journalctl -xeu keyboard_chattering.service ``` -You can also use + +**For the Mouse:** ```shell -journalctl -xeu keyboard_chattering.service +systemctl status mouse_chattering.service +journalctl -xeu mouse_chattering.service ``` -just to make sure that there are no errors. *(Note: If your device disconnects or goes to sleep, the service will safely pause and wait for it to reconnect without consuming CPU).* + +*(Note: If your device disconnects, is unplugged, or goes to sleep, the service will safely pause and wait for it to reconnect without crashing or consuming CPU).* From 4f11429700c64a3023d6da6f685e0399df5f73f1 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:33:59 -0400 Subject: [PATCH 32/55] Update keyboard_chattering.sh --- keyboard_chattering.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/keyboard_chattering.sh b/keyboard_chattering.sh index 0f4ca04..5ddb8b0 100644 --- a/keyboard_chattering.sh +++ b/keyboard_chattering.sh @@ -1,5 +1,6 @@ #!/bin/bash -# Change the line below to the absolute path of the folder +# Change the line below to the absolute path of the folder. # You can append `--keys KEY_A,KEY_SPACE` at the very end to ONLY filter those specific keys. -# (If using modern Python, you may need to run: sudo pip3 install -r requirements.txt --break-system-packages) +# (If using modern Linux/Python, you may need to run: sudo pip3 install -r requirements.txt --break-system-packages) + cd && sudo python3 -m src.keyboard_main -k -t 30 From 746c1e84a6ae6352db5301005b0bc56f22de7427 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:34:34 -0400 Subject: [PATCH 33/55] Update mouse_chattering.sh --- mouse_chattering.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mouse_chattering.sh b/mouse_chattering.sh index f79bfb6..6ffcd3c 100644 --- a/mouse_chattering.sh +++ b/mouse_chattering.sh @@ -1,5 +1,6 @@ #!/bin/bash -# Change the line below to the absolute path of the folder +# Change the line below to the absolute path of the folder. # You can append `--buttons BTN_LEFT,BTN_SIDE` at the very end to ONLY filter those specific buttons. -# (If using modern Python, you may need to run: sudo pip3 install -r requirements.txt --break-system-packages) +# (If using modern Linux/Python, you may need to run: sudo pip3 install -r requirements.txt --break-system-packages) + cd && sudo python3 -m src.mouse_main -m -t 30 From 566701dcfe793132bf69a7fc06b0f1af41bf46bb Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:35:17 -0400 Subject: [PATCH 34/55] Update keyboard_filtering.py --- src/keyboard_filtering.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/keyboard_filtering.py b/src/keyboard_filtering.py index b343484..d722e9a 100644 --- a/src/keyboard_filtering.py +++ b/src/keyboard_filtering.py @@ -2,13 +2,18 @@ from collections import defaultdict from typing import DefaultDict, Dict, NoReturn, List import time - import libevdev def filter_chattering(evdev: libevdev.Device, threshold: int, keys_to_filter: List[libevdev.EventCode] = None) -> NoReturn: - time.sleep(1) # Delay to allow Enter key to release natively + # Delay to allow the Enter key (used to execute the terminal command) + # to release natively before we grab the device. Prevents a "stuck" Enter key. + time.sleep(1) + + # Grab the physical device so only we see the events it emits evdev.grab() + + # Create a virtual uinput device to emit our cleaned events back to the OS ui_dev = evdev.create_uinput_device() logging.info("Listening to keyboard input events...") @@ -17,6 +22,7 @@ def filter_chattering(evdev: libevdev.Device, threshold: int, keys_to_filter: Li keys_to_filter = [] while True: + # Descriptor is blocking; waits until physical events are available for e in evdev.events(): if _from_keystroke(e, threshold, keys_to_filter): ui_dev.send_events([e, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) @@ -25,19 +31,22 @@ def filter_chattering(evdev: libevdev.Device, threshold: int, keys_to_filter: Li def _from_keystroke(event: libevdev.InputEvent, threshold: int, keys_to_filter: List[libevdev.EventCode]) -> bool: global _last_key_code + # Ignore sync/misc events. libevdev uinput handles syncing natively. if event.matches(libevdev.EV_SYN) or event.matches(libevdev.EV_MSC): return False - # Do not filter modifier combinations or held keys + # MODIFIER FIX: If the event isn't a key, or it's a "hold" event (event.value > 1), forward it immediately. + # This ensures held keys (like Shift or Ctrl) don't get interrupted by the chatter filter. if not event.matches(libevdev.EV_KEY) or event.value > 1: logging.debug(f'FORWARDING {event.code}') return True - # SPECIFIC KEY FILTERING + # TARGETED FILTERING: If the user provided specific keys to fix, and this isn't one of them, forward it. if keys_to_filter and event.code not in keys_to_filter: logging.debug(f'FORWARDING {event.code} (not in targeted filter list)') return True + # Process standard Key Up (0) and Key Down (1) events if event.value == 0: if _key_pressed[event.code]: logging.debug(f'FORWARDING {event.code} up') @@ -51,7 +60,9 @@ def _from_keystroke(event: libevdev.InputEvent, threshold: int, keys_to_filter: prev = _last_key_up.get(event.code) now = event.sec * 1E6 + event.usec - # Check _last_key_code to prevent filtering fast alternating letters (e.g., e -> v -> e) + # DOUBLE-LETTER FIX: Check `_last_key_code != event.code`. + # If a user types fast alternating letters (e.g. e -> v -> e), the second 'e' won't be + # mistakenly filtered, because the 'v' reset the _last_key_code. if prev is None or now - prev > threshold * 1E3 or _last_key_code != event.code: logging.debug(f'FORWARDING {event.code} down') _key_pressed[event.code] = True @@ -61,6 +72,7 @@ def _from_keystroke(event: libevdev.InputEvent, threshold: int, keys_to_filter: logging.info(f'FILTERED {event.code} down: last key up event happened {(now - prev) / 1E3} ms ago') return False +# Global state trackers _last_key_up: Dict[libevdev.EventCode, int] = {} _key_pressed: DefaultDict[libevdev.EventCode, bool] = defaultdict(bool) _last_key_code = None From 1bdf5c890df5e39b57713e6ea023f3109c8d0b16 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:36:05 -0400 Subject: [PATCH 35/55] Update keyboard_main.py --- src/keyboard_main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/keyboard_main.py b/src/keyboard_main.py index cc944d4..4321124 100755 --- a/src/keyboard_main.py +++ b/src/keyboard_main.py @@ -8,6 +8,7 @@ from src.keyboard_filtering import filter_chattering from src.keyboard_retrieval import retrieve_keyboard_name, INPUT_DEVICES_PATH, abs_keyboard_path +# Safely import the config file if it exists try: from src.keyboard_config import FILTERED_KEYS except ImportError: @@ -16,6 +17,10 @@ @contextmanager def get_device_handle(keyboard_name: str) -> libevdev.Device: device_path = abs_keyboard_path(keyboard_name) + + # DISCONNECT FIX: Prevent 100% CPU exhaustion loop. + # If the keyboard is unplugged/sleeps, the path disappears. We exit cleanly (0). + # Systemd (Restart=always) will quietly check every 5 seconds until it returns. if not os.path.exists(device_path): logging.critical(f"Keyboard {keyboard_name} not connected. Exiting to prevent CPU loop.") sys.exit(0) @@ -28,6 +33,7 @@ def get_device_handle(keyboard_name: str) -> libevdev.Device: fd.close() def parse_keys(keys_str): + """Parses comma-separated CLI arguments into a list of strings.""" if not keys_str: return [] return [key.strip() for key in keys_str.split(',')] @@ -43,9 +49,11 @@ def parse_keys(keys_str): handlers=[logging.StreamHandler(sys.stdout)], format="%(asctime)s - %(message)s", datefmt="%H:%M:%S") + # CONFIG PRECEDENCE: CLI args > keyboard_config.py > Empty (Filter All) keys_list = args.keys if args.keys else list(FILTERED_KEYS) keys_to_filter = [] + # Convert string key names (e.g., "KEY_A") to libevdev.EventCode objects for key in keys_list: try: keys_to_filter.append(libevdev.evbit(key)) From 02f51845af7a1c25f868c4136a08c8bc680813a2 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:36:36 -0400 Subject: [PATCH 36/55] Update mouse_filtering.py --- src/mouse_filtering.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/mouse_filtering.py b/src/mouse_filtering.py index 195a556..c418b26 100644 --- a/src/mouse_filtering.py +++ b/src/mouse_filtering.py @@ -5,7 +5,7 @@ import libevdev def filter_mouse_chattering(evdev: libevdev.Device, threshold: int, buttons_to_filter: List[libevdev.EventCode] = None) -> NoReturn: - time.sleep(1) + time.sleep(1) # Delay for clean startup evdev.grab() ui_dev = evdev.create_uinput_device() @@ -25,17 +25,23 @@ def _from_click(event: libevdev.InputEvent, threshold: int, buttons_to_filter: L if event.matches(libevdev.EV_SYN) or event.matches(libevdev.EV_MSC): return False - # IMMEDIATELY FORWARD MOUSE MOVEMENT AND SCROLLING (EV_REL / EV_ABS) + # CRITICAL MOUSE FIX: Immediately forward all movement data. + # EV_REL = Relative movement (standard X/Y cursor movement and scroll wheel) + # EV_ABS = Absolute movement (drawing tablets, touchpads) + # Skipping this prevents the cursor from freezing or stuttering. if event.matches(libevdev.EV_REL) or event.matches(libevdev.EV_ABS): return True - # If it isn't a button, or it's a held click natively, forward it + # In Linux, mouse clicks are classified as EV_KEY. + # If it isn't an EV_KEY, or it's a natively held click (value > 1), forward it. if not event.matches(libevdev.EV_KEY) or event.value > 1: return True + # TARGETED FILTERING: If the user provided specific buttons to fix, forward everything else. if buttons_to_filter and event.code not in buttons_to_filter: return True + # Process Button Up (0) and Button Down (1) if event.value == 0: if _btn_pressed[event.code]: _last_btn_up[event.code] = event.sec * 1E6 + event.usec @@ -47,6 +53,7 @@ def _from_click(event: libevdev.InputEvent, threshold: int, buttons_to_filter: L prev = _last_btn_up.get(event.code) now = event.sec * 1E6 + event.usec + # Check _last_btn_code to allow fast alternating clicks (e.g. Left -> Right -> Left) if prev is None or now - prev > threshold * 1E3 or _last_btn_code != event.code: _btn_pressed[event.code] = True _last_btn_code = event.code @@ -55,6 +62,7 @@ def _from_click(event: libevdev.InputEvent, threshold: int, buttons_to_filter: L logging.info(f'FILTERED {event.code} down: last up event {(now - prev) / 1E3} ms ago') return False +# Global state trackers _last_btn_up: Dict[libevdev.EventCode, int] = {} _btn_pressed: DefaultDict[libevdev.EventCode, bool] = defaultdict(bool) _last_btn_code = None From a930fb196fefa0bb67e872250ccec8aac31e174a Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:36:59 -0400 Subject: [PATCH 37/55] Update mouse_main.py --- src/mouse_main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/mouse_main.py b/src/mouse_main.py index 4fce8dd..90d5186 100644 --- a/src/mouse_main.py +++ b/src/mouse_main.py @@ -8,6 +8,7 @@ from src.mouse_filtering import filter_mouse_chattering from src.mouse_retrieval import retrieve_mouse_name, INPUT_DEVICES_PATH, abs_mouse_path +# Safely import the config file if it exists try: from src.mouse_config import FILTERED_BUTTONS except ImportError: @@ -16,6 +17,8 @@ @contextmanager def get_device_handle(mouse_name: str) -> libevdev.Device: device_path = abs_mouse_path(mouse_name) + + # DISCONNECT FIX: Prevent 100% CPU exhaustion loop if mouse is turned off/unplugged. if not os.path.exists(device_path): logging.critical(f"Mouse {mouse_name} not connected. Exiting to prevent CPU loop.") sys.exit(0) @@ -28,6 +31,7 @@ def get_device_handle(mouse_name: str) -> libevdev.Device: fd.close() def parse_buttons(buttons_str): + """Parses comma-separated CLI arguments into a list of strings.""" if not buttons_str: return [] return [btn.strip() for btn in buttons_str.split(',')] @@ -43,9 +47,11 @@ def parse_buttons(buttons_str): handlers=[logging.StreamHandler(sys.stdout)], format="%(asctime)s - %(message)s", datefmt="%H:%M:%S") + # CONFIG PRECEDENCE: CLI args > mouse_config.py > Empty (Filter All) buttons_list = args.buttons if args.buttons else list(FILTERED_BUTTONS) buttons_to_filter = [] + # Convert string button names (e.g., "BTN_LEFT") to libevdev.EventCode objects for btn in buttons_list: try: buttons_to_filter.append(libevdev.evbit(btn)) From bc84357edc1bd4dca5ac6f26addd8a49089ab771 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:40:26 -0400 Subject: [PATCH 38/55] Update keyboard_retrieval.py --- src/keyboard_retrieval.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/keyboard_retrieval.py b/src/keyboard_retrieval.py index 7a60fe4..1142087 100644 --- a/src/keyboard_retrieval.py +++ b/src/keyboard_retrieval.py @@ -2,25 +2,37 @@ import os from typing import Final +# We use 'by-id' because these device names are persistent. +# If we used standard '/dev/input/eventX', the ID might change every time you reboot or plug in a USB. INPUT_DEVICES_PATH: Final = '/dev/input/by-id' def retrieve_keyboard_name() -> str: + """Attempts to find the connected keyboard automatically, or prompts the user to select one.""" + + # Look through the input directory where persistent device IDs are stored all_devices = os.listdir(INPUT_DEVICES_PATH) + + # Deduplicate the list natively using a set keyboard_devices = list(set(all_devices)) n_devices = len(keyboard_devices) + # If no devices are found in the folder at all, abort if n_devices == 0: raise ValueError(f"Couldn't find a keyboard in '{INPUT_DEVICES_PATH}'") + # If exactly one device is found, automatically select it without bothering the user if n_devices == 1: logging.info(f"Found keyboard: {keyboard_devices[0]}") return keyboard_devices[0] + # If multiple devices are found, present an interactive selection menu in the terminal print("Select a keyboard device:") for idx, device in enumerate(sorted(keyboard_devices), start=1): print(f"{idx}. {device}") selected_idx = -1 + + # Loop until the user inputs a valid number corresponding to the list while selected_idx < 1 or selected_idx > n_devices: try: selected_idx = int(input("Enter your choice (number): ")) @@ -29,7 +41,9 @@ def retrieve_keyboard_name() -> str: except ValueError: print("Please enter a valid number") + # Return the string name of the selected device (subtracting 1 because arrays are 0-indexed) return keyboard_devices[selected_idx - 1] def abs_keyboard_path(device: str) -> str: + """Combines the folder path and the device name into a full absolute path.""" return os.path.join(INPUT_DEVICES_PATH, device) From 9c39f93e702cab28572554a914e1e9462bd62cd6 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 14:40:45 -0400 Subject: [PATCH 39/55] Update mouse_retrieval.py --- src/mouse_retrieval.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/mouse_retrieval.py b/src/mouse_retrieval.py index 7cf49ab..f72c01e 100644 --- a/src/mouse_retrieval.py +++ b/src/mouse_retrieval.py @@ -2,25 +2,37 @@ import os from typing import Final +# We use 'by-id' because these device names are persistent. INPUT_DEVICES_PATH: Final = '/dev/input/by-id' def retrieve_mouse_name() -> str: + """Attempts to find the connected mouse automatically, or prompts the user to select one.""" + + # Look through the input directory where persistent device IDs are stored all_devices = os.listdir(INPUT_DEVICES_PATH) + + # TARGETED FILTER: We only care about devices that have 'event-mouse' in their name. + # This hides all the keyboards, webcams, and other USB devices from the prompt. mouse_devices = list(set([d for d in all_devices if 'event-mouse' in d])) n_devices = len(mouse_devices) + # If no mouse devices are found, abort and tell the user they might need to provide it manually if n_devices == 0: raise ValueError(f"Couldn't find a mouse ending with 'event-mouse'. Please provide it manually with -m.") + # If exactly one mouse is found, automatically select it if n_devices == 1: logging.info(f"Found mouse: {mouse_devices[0]}") return mouse_devices[0] + # If multiple mice are found, present an interactive selection menu in the terminal print("Select a mouse device:") for idx, device in enumerate(sorted(mouse_devices), start=1): print(f"{idx}. {device}") selected_idx = -1 + + # Loop until the user inputs a valid number corresponding to the list while selected_idx < 1 or selected_idx > n_devices: try: selected_idx = int(input("Enter your choice (number): ")) @@ -29,7 +41,9 @@ def retrieve_mouse_name() -> str: except ValueError: print("Please enter a valid number") + # Return the string name of the selected device (subtracting 1 because arrays are 0-indexed) return mouse_devices[selected_idx - 1] def abs_mouse_path(device: str) -> str: + """Combines the folder path and the device name into a full absolute path.""" return os.path.join(INPUT_DEVICES_PATH, device) From 88505863bfb6c49b92f6de76b6dd1d6c8ecf6c8b Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 15:36:24 -0400 Subject: [PATCH 40/55] Update keyboard_retrieval.py --- src/keyboard_retrieval.py | 49 +++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/keyboard_retrieval.py b/src/keyboard_retrieval.py index 1142087..2021723 100644 --- a/src/keyboard_retrieval.py +++ b/src/keyboard_retrieval.py @@ -2,48 +2,63 @@ import os from typing import Final -# We use 'by-id' because these device names are persistent. -# If we used standard '/dev/input/eventX', the ID might change every time you reboot or plug in a USB. +# We use the 'by-id' folder because the device names here are persistent. +# If we used the standard '/dev/input/eventX', the numbers might change every time you reboot or plug in a USB. INPUT_DEVICES_PATH: Final = '/dev/input/by-id' + def retrieve_keyboard_name() -> str: - """Attempts to find the connected keyboard automatically, or prompts the user to select one.""" + """ + Lists all devices in the input directory and prompts the user to select one. + This is triggered when the script is run without the `-k` argument. + """ - # Look through the input directory where persistent device IDs are stored + # Read the directory to get a list of all connected input devices all_devices = os.listdir(INPUT_DEVICES_PATH) - # Deduplicate the list natively using a set + # Deduplicate the list natively using a set to ensure clean output keyboard_devices = list(set(all_devices)) n_devices = len(keyboard_devices) - # If no devices are found in the folder at all, abort + # If no devices are found in the folder at all, abort the script if n_devices == 0: - raise ValueError(f"Couldn't find a keyboard in '{INPUT_DEVICES_PATH}'") - - # If exactly one device is found, automatically select it without bothering the user - if n_devices == 1: - logging.info(f"Found keyboard: {keyboard_devices[0]}") - return keyboard_devices[0] + raise ValueError(f"Couldn't find any devices in '{INPUT_DEVICES_PATH}'") - # If multiple devices are found, present an interactive selection menu in the terminal + # Present an interactive selection menu in the terminal. + # We list EVERYTHING because modern gaming keyboards and mice often register as multiple + # virtual devices (e.g. separate endpoints for macro keys, RGB controllers, etc). print("Select a keyboard device:") + + # Sort the devices alphabetically so they are easy to read for idx, device in enumerate(sorted(keyboard_devices), start=1): print(f"{idx}. {device}") selected_idx = -1 - # Loop until the user inputs a valid number corresponding to the list + # Loop continuously until the user inputs a valid number corresponding to the list while selected_idx < 1 or selected_idx > n_devices: try: + # Capture keyboard input from the user selected_idx = int(input("Enter your choice (number): ")) + + # Warn the user if they pick a number outside the valid range if selected_idx < 1 or selected_idx > n_devices: print(f"Please select a number between 1 and {n_devices}") + except ValueError: + # Warn the user if they type letters instead of numbers print("Please enter a valid number") - # Return the string name of the selected device (subtracting 1 because arrays are 0-indexed) - return keyboard_devices[selected_idx - 1] + # Return the string name of the selected device. + # We subtract 1 because our visual list started at 1, but Python arrays start at 0. + # We also must sort the list here identically to how we printed it, so the index matches. + sorted_devices = sorted(keyboard_devices) + return sorted_devices[selected_idx - 1] + def abs_keyboard_path(device: str) -> str: - """Combines the folder path and the device name into a full absolute path.""" + """ + Helper function that combines the folder path and the device name + into a full absolute path (e.g., /dev/input/by-id/usb-keyboard-name) + """ return os.path.join(INPUT_DEVICES_PATH, device) From 8c85f60b3b3743a3630cde7a65754bacb78b301b Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 15:36:43 -0400 Subject: [PATCH 41/55] Update mouse_retrieval.py --- src/mouse_retrieval.py | 50 +++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/mouse_retrieval.py b/src/mouse_retrieval.py index f72c01e..c765890 100644 --- a/src/mouse_retrieval.py +++ b/src/mouse_retrieval.py @@ -2,48 +2,62 @@ import os from typing import Final -# We use 'by-id' because these device names are persistent. +# We use the 'by-id' folder because the device names here are persistent. INPUT_DEVICES_PATH: Final = '/dev/input/by-id' + def retrieve_mouse_name() -> str: - """Attempts to find the connected mouse automatically, or prompts the user to select one.""" + """ + Lists all devices in the input directory and prompts the user to select one. + This is triggered when the script is run without the `-m` argument. + """ - # Look through the input directory where persistent device IDs are stored + # Read the directory to get a list of all connected input devices all_devices = os.listdir(INPUT_DEVICES_PATH) - # TARGETED FILTER: We only care about devices that have 'event-mouse' in their name. - # This hides all the keyboards, webcams, and other USB devices from the prompt. - mouse_devices = list(set([d for d in all_devices if 'event-mouse' in d])) + # We intentionally do NOT filter for the word 'mouse' here. + # Advanced gaming mice (like Razer or Logitech) often split their buttons into virtual + # keyboard endpoints (e.g. '-if01-event-kbd'). Showing all devices ensures you can find it. + mouse_devices = list(set(all_devices)) n_devices = len(mouse_devices) - # If no mouse devices are found, abort and tell the user they might need to provide it manually + # If no devices are found, abort and tell the user they might need to provide it manually if n_devices == 0: - raise ValueError(f"Couldn't find a mouse ending with 'event-mouse'. Please provide it manually with -m.") - - # If exactly one mouse is found, automatically select it - if n_devices == 1: - logging.info(f"Found mouse: {mouse_devices[0]}") - return mouse_devices[0] + raise ValueError(f"Couldn't find any devices in '{INPUT_DEVICES_PATH}'. Please provide it manually with -m.") - # If multiple mice are found, present an interactive selection menu in the terminal + # Present an interactive selection menu in the terminal. print("Select a mouse device:") + + # Sort the devices alphabetically so they are easy to read for idx, device in enumerate(sorted(mouse_devices), start=1): print(f"{idx}. {device}") selected_idx = -1 - # Loop until the user inputs a valid number corresponding to the list + # Loop continuously until the user inputs a valid number corresponding to the list while selected_idx < 1 or selected_idx > n_devices: try: + # Capture keyboard input from the user selected_idx = int(input("Enter your choice (number): ")) + + # Warn the user if they pick a number outside the valid range if selected_idx < 1 or selected_idx > n_devices: print(f"Please select a number between 1 and {n_devices}") + except ValueError: + # Warn the user if they type letters instead of numbers print("Please enter a valid number") - # Return the string name of the selected device (subtracting 1 because arrays are 0-indexed) - return mouse_devices[selected_idx - 1] + # Return the string name of the selected device. + # We subtract 1 because our visual list started at 1, but Python arrays start at 0. + # We also must sort the list here identically to how we printed it, so the index matches. + sorted_devices = sorted(mouse_devices) + return sorted_devices[selected_idx - 1] + def abs_mouse_path(device: str) -> str: - """Combines the folder path and the device name into a full absolute path.""" + """ + Helper function that combines the folder path and the device name + into a full absolute path (e.g., /dev/input/by-id/usb-mouse-name) + """ return os.path.join(INPUT_DEVICES_PATH, device) From eb3d9df5a015c8a73a4082a9726be796618bc487 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 17:51:57 -0400 Subject: [PATCH 42/55] Update README.md --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index c7e594d..268201b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,19 @@ This also means it works across the whole system, without depending on X11 or Wa As for the filtering rule, what seems to work well is the time between the last "key up" event and the current "key down" event. When the switch chatters, that time is very low - around 10 ms. By filtering such anomalies, we remove chatter without impeding actual fast typing or clicking. +### Understanding Linux Input Devices (Which one do I pick?) + +Modern gaming peripherals (like Corsair, Razer, or Logitech) are "composite USB devices". This means a single physical mouse might tell Linux it is actually 4 different devices! When you run the scripts manually, you will see a list of endpoints ending in different suffixes. + +Here is a guide on which one to choose: + +- **`-event-kbd`**: The primary endpoint for standard keystrokes. For keyboards, select this to fix chattering on standard keys (A-Z, 0-9). +- **`-event-mouse`**: The primary endpoint for standard mouse clicks (Left, Right, Middle) and X/Y movement. Select this to fix standard mouse double-clicking. +- **`-ifXX-event-kbd` (Virtual Mouse Keyboards)**: Advanced gaming mice often register a "virtual keyboard" to handle macro side-buttons. If your mouse's side buttons are double-clicking, you may need to point the mouse script at this endpoint instead of the standard mouse endpoint! +- **`-event-ifXX` (Interfaces)**: These handle multimedia controls (Volume wheels, Play/Pause) or vendor-specific data (RGB lighting). You rarely need to select these unless your volume wheel is bouncing. + +*Note: Legacy raw nodes (like those ending simply in `-mouse` or `-kbd` without the word `event`) are legacy X11 nodes and cannot be read by `libevdev`.* + ## Installation Download the repository and extract the files. The dependencies are listed in `requirements.txt`. You can install them with the command below. From e6d8313695ce55a8e1d14feff10ea6d12ea1ea35 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 17:58:11 -0400 Subject: [PATCH 43/55] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 268201b..2328b73 100644 --- a/README.md +++ b/README.md @@ -100,11 +100,16 @@ chmod +x keyboard_chattering.sh mouse_chattering.sh ### Step 2: Configure the service files Edit `keyboard_chattering.service` and `mouse_chattering.service`. The `ExecStart` should be the absolute path of the respective `.sh` file. -**Example:** +**Example keyboard_chattering.service:** ```shell ExecStart=/home/foouser/Downloads/HardwareChatteringFix-Linux/keyboard_chattering.sh ``` +**Example mouse_chattering.service:** +```shell +ExecStart=/home/foouser/Downloads/HardwareChatteringFix-Linux/mouse_chattering.sh +``` + ### Step 3: Enable the Services (Separately or Combined) Copy the `.service` files to your systemd folder: From 23ab86e9781fe6aa4a2243127b139b943ef438fa Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 18:09:28 -0400 Subject: [PATCH 44/55] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2328b73..aa56681 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ Here is a guide on which one to choose: - **`-ifXX-event-kbd` (Virtual Mouse Keyboards)**: Advanced gaming mice often register a "virtual keyboard" to handle macro side-buttons. If your mouse's side buttons are double-clicking, you may need to point the mouse script at this endpoint instead of the standard mouse endpoint! - **`-event-ifXX` (Interfaces)**: These handle multimedia controls (Volume wheels, Play/Pause) or vendor-specific data (RGB lighting). You rarely need to select these unless your volume wheel is bouncing. +**Troubleshooting Manual Testing:** +If you run the script manually in the terminal and receive a `[Errno 16] Device or resource busy` error, it means you have a background Systemd service currently running! The script requires an exclusive lock on the hardware. Simply run `sudo systemctl stop keyboard_chattering` or `sudo systemctl stop mouse_chattering` to release the lock before testing manually. + *Note: Legacy raw nodes (like those ending simply in `-mouse` or `-kbd` without the word `event`) are legacy X11 nodes and cannot be read by `libevdev`.* ## Installation From 05a37b5eaad9ce65c4b3523067dc9f11bfc2155d Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 18:17:11 -0400 Subject: [PATCH 45/55] Update keyboard_retrieval.py --- src/keyboard_retrieval.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/keyboard_retrieval.py b/src/keyboard_retrieval.py index 2021723..5ba1fe1 100644 --- a/src/keyboard_retrieval.py +++ b/src/keyboard_retrieval.py @@ -12,6 +12,11 @@ def retrieve_keyboard_name() -> str: Lists all devices in the input directory and prompts the user to select one. This is triggered when the script is run without the `-k` argument. """ + + # Filter to ONLY show valid modern event nodes. This safely hides legacy raw nodes (like '-mouse' or '-kbd') which would crash libevdev, but keeps all virtual '-event-kbd' and '-event-mouse' nodes visible. + #valid_devices = [d for d in all_devices if '-event-' in d] + #device_list = list(set(valid_devices)) + #n_devices = len(device_list) # Read the directory to get a list of all connected input devices all_devices = os.listdir(INPUT_DEVICES_PATH) From f983e5fdb8fc4449b10902557368c34c0b3a29db Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 18:18:05 -0400 Subject: [PATCH 46/55] Update mouse_retrieval.py --- src/mouse_retrieval.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/mouse_retrieval.py b/src/mouse_retrieval.py index c765890..e8b8b9c 100644 --- a/src/mouse_retrieval.py +++ b/src/mouse_retrieval.py @@ -11,6 +11,11 @@ def retrieve_mouse_name() -> str: Lists all devices in the input directory and prompts the user to select one. This is triggered when the script is run without the `-m` argument. """ + + # Filter to ONLY show valid modern event nodes. This safely hides legacy raw nodes (like '-mouse' or '-kbd') which would crash libevdev, but keeps all virtual '-event-kbd' and '-event-mouse' nodes visible. + #valid_devices = [d for d in all_devices if '-event-' in d] + #device_list = list(set(valid_devices)) + #n_devices = len(device_list) # Read the directory to get a list of all connected input devices all_devices = os.listdir(INPUT_DEVICES_PATH) From e6a07077dff777aae64304b75db52dd2772b1dfe Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 18:19:40 -0400 Subject: [PATCH 47/55] Update mouse_retrieval.py --- src/mouse_retrieval.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mouse_retrieval.py b/src/mouse_retrieval.py index e8b8b9c..4b12dad 100644 --- a/src/mouse_retrieval.py +++ b/src/mouse_retrieval.py @@ -12,7 +12,9 @@ def retrieve_mouse_name() -> str: This is triggered when the script is run without the `-m` argument. """ - # Filter to ONLY show valid modern event nodes. This safely hides legacy raw nodes (like '-mouse' or '-kbd') which would crash libevdev, but keeps all virtual '-event-kbd' and '-event-mouse' nodes visible. + # Filter to ONLY show valid modern event nodes. + #This safely hides legacy raw nodes (like '-mouse' or '-kbd') which would crash libevdev, + #but keeps all virtual '-event-kbd' and '-event-mouse' nodes visible. #valid_devices = [d for d in all_devices if '-event-' in d] #device_list = list(set(valid_devices)) #n_devices = len(device_list) From ec37dd6600c45169e205e5324570726038934dec Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 18:20:06 -0400 Subject: [PATCH 48/55] Update keyboard_retrieval.py --- src/keyboard_retrieval.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/keyboard_retrieval.py b/src/keyboard_retrieval.py index 5ba1fe1..7ac2052 100644 --- a/src/keyboard_retrieval.py +++ b/src/keyboard_retrieval.py @@ -13,7 +13,9 @@ def retrieve_keyboard_name() -> str: This is triggered when the script is run without the `-k` argument. """ - # Filter to ONLY show valid modern event nodes. This safely hides legacy raw nodes (like '-mouse' or '-kbd') which would crash libevdev, but keeps all virtual '-event-kbd' and '-event-mouse' nodes visible. + # Filter to ONLY show valid modern event nodes. + #This safely hides legacy raw nodes (like '-mouse' or '-kbd') which would crash libevdev + #but keeps all virtual '-event-kbd' and '-event-mouse' nodes visible. #valid_devices = [d for d in all_devices if '-event-' in d] #device_list = list(set(valid_devices)) #n_devices = len(device_list) From 90a59b2bd5a81915d74c41f7163900a700d219af Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 18:37:43 -0400 Subject: [PATCH 49/55] Update keyboard_filtering.py --- src/keyboard_filtering.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/keyboard_filtering.py b/src/keyboard_filtering.py index d722e9a..ef71360 100644 --- a/src/keyboard_filtering.py +++ b/src/keyboard_filtering.py @@ -23,9 +23,17 @@ def filter_chattering(evdev: libevdev.Device, threshold: int, keys_to_filter: Li while True: # Descriptor is blocking; waits until physical events are available - for e in evdev.events(): - if _from_keystroke(e, threshold, keys_to_filter): - ui_dev.send_events([e, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) + try: + for e in evdev.events(): + if _from_keystroke(e, threshold, keys_to_filter): + ui_dev.send_events([e, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) + except OSError as err: + # Errno 19 means "No such device". This happens if the USB is suddenly unplugged. + if err.errno == 19: + logging.critical("Keyboard disconnected while listening. Exiting gracefully.") + sys.exit(0) + else: + raise err def _from_keystroke(event: libevdev.InputEvent, threshold: int, keys_to_filter: List[libevdev.EventCode]) -> bool: From ca50a2868bfcf4c65a75bd2101d8262b314fd1b0 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 18:38:39 -0400 Subject: [PATCH 50/55] Update mouse_filtering.py --- src/mouse_filtering.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/mouse_filtering.py b/src/mouse_filtering.py index c418b26..61292e4 100644 --- a/src/mouse_filtering.py +++ b/src/mouse_filtering.py @@ -15,9 +15,17 @@ def filter_mouse_chattering(evdev: libevdev.Device, threshold: int, buttons_to_f buttons_to_filter = [] while True: - for e in evdev.events(): - if _from_click(e, threshold, buttons_to_filter): - ui_dev.send_events([e, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) + try: + for e in evdev.events(): + if _from_click(e, threshold, buttons_to_filter): + ui_dev.send_events([e, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) + except OSError as err: + # Errno 19 means "No such device". This happens if the USB is suddenly unplugged. + if err.errno == 19: + logging.critical("Mouse disconnected while listening. Exiting gracefully.") + sys.exit(0) + else: + raise err def _from_click(event: libevdev.InputEvent, threshold: int, buttons_to_filter: List[libevdev.EventCode]) -> bool: global _last_btn_code From e2a6dc3e264a28187b6aeba6a490a087967a35a7 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 18:42:17 -0400 Subject: [PATCH 51/55] Update keyboard_filtering.py --- src/keyboard_filtering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/keyboard_filtering.py b/src/keyboard_filtering.py index ef71360..dc23ec2 100644 --- a/src/keyboard_filtering.py +++ b/src/keyboard_filtering.py @@ -3,7 +3,7 @@ from typing import DefaultDict, Dict, NoReturn, List import time import libevdev - +import sys def filter_chattering(evdev: libevdev.Device, threshold: int, keys_to_filter: List[libevdev.EventCode] = None) -> NoReturn: # Delay to allow the Enter key (used to execute the terminal command) From b0907c4fa28011cf8c5c23c9ebe254987bc0a794 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 18:42:37 -0400 Subject: [PATCH 52/55] Update mouse_filtering.py --- src/mouse_filtering.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mouse_filtering.py b/src/mouse_filtering.py index 61292e4..44d0bd0 100644 --- a/src/mouse_filtering.py +++ b/src/mouse_filtering.py @@ -3,6 +3,7 @@ from typing import DefaultDict, Dict, NoReturn, List import time import libevdev +import sys def filter_mouse_chattering(evdev: libevdev.Device, threshold: int, buttons_to_filter: List[libevdev.EventCode] = None) -> NoReturn: time.sleep(1) # Delay for clean startup From 7352b51c883f831bbcd800bdfd3a5109d56e4236 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 19:01:24 -0400 Subject: [PATCH 53/55] Update README.md --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index aa56681..c1a1cf6 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,19 @@ Download the repository and extract the files. The dependencies are listed in `r sudo pip3 install -r requirements.txt --break-system-packages ``` +### Python Virtual Environment +Using the built-in `venv` module is the safest and cleanest way to run this tool. +```shell +# 1. Create a virtual environment named 'venv' inside the project folder +python3 -m venv venv + +# 2. Activate the virtual environment +source venv/bin/activate + +# 3. Install the dependencies inside the isolated environment +pip install -r requirements.txt +``` + ## Usage `cd` inside the location of the extracted folder. Because keyboards and mice are handled differently by the OS, they are executed as separate modules. Enter the commands below to run them manually: From c194a64186932ba7b6dd97eee612aa22bb22d02f Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 19:03:34 -0400 Subject: [PATCH 54/55] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c1a1cf6..f466112 100644 --- a/README.md +++ b/README.md @@ -47,14 +47,14 @@ Download the repository and extract the files. The dependencies are listed in `r *(Note: According to PEP 668, newer Linux distributions may require the `--break-system-packages` flag, or the use of a python `venv`)*. ```shell -sudo pip3 install -r requirements.txt --break-system-packages +sudo pip install -r requirements.txt --break-system-packages ``` ### Python Virtual Environment Using the built-in `venv` module is the safest and cleanest way to run this tool. ```shell # 1. Create a virtual environment named 'venv' inside the project folder -python3 -m venv venv +python -m venv venv # 2. Activate the virtual environment source venv/bin/activate @@ -69,12 +69,12 @@ pip install -r requirements.txt **To run the Keyboard fix:** ```shell -sudo python3 -m src.keyboard_main +sudo python -m src.keyboard_main ``` **To run the Mouse fix:** ```shell -sudo python3 -m src.mouse_main +sudo python -m src.mouse_main ``` ### Customization Options From 90a358c9ee5c72a660fa6da56ac8906af4fe3da8 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Sat, 9 May 2026 19:10:42 -0400 Subject: [PATCH 55/55] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f466112..3f8c466 100644 --- a/README.md +++ b/README.md @@ -167,4 +167,4 @@ systemctl status mouse_chattering.service journalctl -xeu mouse_chattering.service ``` -*(Note: If your device disconnects, is unplugged, or goes to sleep, the service will safely pause and wait for it to reconnect without crashing or consuming CPU).* +*(Note: If your device disconnects, is unplugged, or goes to sleep, the Python script will gracefully exit. Systemd will then safely attempt to restart it every 5 seconds in the background until the device is reconnected, ensuring 0% CPU waste!)*