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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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!)* From ed5d5a2517366ac249767f19ef02ae8534347726 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 19 May 2026 23:55:45 -0400 Subject: [PATCH 56/79] Update keyboard_chattering.service --- keyboard_chattering.service | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/keyboard_chattering.service b/keyboard_chattering.service index a51856b..d989225 100644 --- a/keyboard_chattering.service +++ b/keyboard_chattering.service @@ -1,7 +1,13 @@ [Unit] Description=Keyboard Chattering Fix service +# Tell systemd to stop this service before going to sleep, and start it after waking up +After=suspend.target +After=hibernate.target +After=hybrid-sleep.target [Service] +# Wait 3 seconds before starting the script to give the USB bus and Wayland time to fully wake up +ExecStartPre=/bin/sleep 3 # Change ExecStart to the absolute path of the file, executing keyboard_chattering.sh ExecStart= @@ -10,3 +16,6 @@ RestartSec=5 [Install] WantedBy=multi-user.target +WantedBy=suspend.target +WantedBy=hibernate.target +WantedBy=hybrid-sleep.target From ad424b2f09b296b810fda24a1b415cc4629a9f76 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 19 May 2026 23:56:28 -0400 Subject: [PATCH 57/79] Update mouse_chattering.service --- mouse_chattering.service | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mouse_chattering.service b/mouse_chattering.service index d300b5e..eb3a999 100644 --- a/mouse_chattering.service +++ b/mouse_chattering.service @@ -1,7 +1,13 @@ [Unit] Description=Mouse Chattering service +# Tell systemd to stop this service before going to sleep, and start it after waking up +After=suspend.target +After=hibernate.target +After=hybrid-sleep.target [Service] +# Wait 3 seconds before starting the script to give the USB bus and Wayland time to fully wake up +ExecStartPre=/bin/sleep 3 # Change ExecStart to the absolute path of the file, executing mouse_chattering.sh ExecStart= @@ -10,3 +16,6 @@ RestartSec=5 [Install] WantedBy=multi-user.target +WantedBy=suspend.target +WantedBy=hibernate.target +WantedBy=hybrid-sleep.target From 1822851e47cc29cae62c99573bd692ba41f165e4 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Wed, 20 May 2026 00:58:42 -0400 Subject: [PATCH 58/79] Update README.md --- README.md | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f8c466..cd0ef1c 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ sudo python -m src.mouse_main - 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 +## Automation (Systemd) 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. @@ -167,4 +167,111 @@ systemctl status mouse_chattering.service journalctl -xeu mouse_chattering.service ``` +## Automation (Non-Systemd & BSD) + +Because the Python scripts rely natively on the OS Kernel (`evdev` and `uinput`), the code works perfectly on non-systemd distributions and BSD variants. Ensure your `.sh` scripts are configured and executable (`chmod +x`), then use the guide below for your specific init system. + +**PRO-TIP FOR LOGGING:** Since non-systemd systems lack `journalctl`, you should modify your `.sh` scripts to redirect output so you can read the logs. Append this to the execution lines in your `.sh` files: +`... -t 30 >> /var/log/keyboard_fix.log 2>&1` (Do the same for `mouse_fix.log`). +You can then read your logs anytime using `cat /var/log/keyboard_fix.log`. + +### Cron (Universal Fallback) +The easiest way to run the scripts on any system without Systemd is using `cron`'s `@reboot` directive. +1. Open the root crontab: `sudo crontab -e` +2. Add both scripts to run in the background (using `&`): + ```text + @reboot /absolute/path/to/keyboard_chattering.sh & + @reboot /absolute/path/to/mouse_chattering.sh & + ``` +- **Start Right Now:** Run `sudo /absolute/path/to/keyboard_chattering.sh &` in your terminal. +- **Status/Logs:** Run `ps aux | grep python3` to ensure they are running. View the `.log` files defined in your script. +- **Restart:** Run `sudo pkill -f keyboard_main` (or `mouse_main`), then manually start them again. + +### OpenRC (Artix, Alpine, Gentoo) +1. Create two files: `/etc/init.d/keyboard_fix` and `/etc/init.d/mouse_fix` +2. Paste this template (Adjust names/paths for the mouse version!): + ```bash + #!/sbin/openrc-run + name="Keyboard Chattering Fix" + command="/absolute/path/to/keyboard_chattering.sh" + command_background=true + pidfile="/run/keyboard_fix.pid" + depend() { need localmount } + ``` +3. Make them executable: `sudo chmod +x /etc/init.d/keyboard_fix /etc/init.d/mouse_fix` +4. Enable at boot: `sudo rc-update add keyboard_fix default` and `sudo rc-update add mouse_fix default` +- **Start Right Now / Restart:** `sudo rc-service keyboard_fix start` (or `restart`) +- **Status:** `sudo rc-service keyboard_fix status` + +### Runit (Void Linux) +1. Create service directories: `sudo mkdir -p /etc/sv/keyboard_fix /etc/sv/mouse_fix` +2. Create a run file for the keyboard: `sudo nano /etc/sv/keyboard_fix/run` + ```bash + #!/bin/sh + exec /absolute/path/to/keyboard_chattering.sh + ``` +3. Create a run file for the mouse: `sudo nano /etc/sv/mouse_fix/run` + ```bash + #!/bin/sh + exec /absolute/path/to/mouse_chattering.sh + ``` +4. Make both executable: `sudo chmod +x /etc/sv/keyboard_fix/run /etc/sv/mouse_fix/run` +5. Enable them: `sudo ln -s /etc/sv/keyboard_fix /var/service/` and `sudo ln -s /etc/sv/mouse_fix /var/service/` +- **Start Right Now:** Runit detects the symlinks and starts them automatically! +- **Status:** `sudo sv status keyboard_fix mouse_fix` +- **Restart:** `sudo sv restart keyboard_fix mouse_fix` + +### SysVinit (Devuan, Older Distros) +Simply add the executable scripts to your `/etc/rc.local` file before the `exit 0` line: +```bash +/absolute/path/to/keyboard_chattering.sh & +/absolute/path/to/mouse_chattering.sh & +exit 0 +``` +- **Start Right Now:** Run `sudo /etc/rc.local` +- **Status / Restart:** Use `ps aux | grep python3` to check status, and `kill` them to stop them. + +### FreeBSD / BSD Family +FreeBSD has native support for `evdev`, but you must load the modules and adjust device paths. + +**1. Load evdev modules:** Add these to `/boot/loader.conf` and reboot (or `kldload` them now): +```text +evdev_load="YES" +uinput_load="YES" +``` + +**2. Find your Device Path:** +FreeBSD does not use Linux's `udev` naming conventions. The folder `/dev/input/by-id/` does not exist on BSD! Instead, FreeBSD lists devices as raw event nodes (`/dev/input/event0`, `event1`, etc.). +Update your `.sh` scripts to pass the raw absolute path directly to `-k` or `-m` (which overrides the auto-search scripts): +```bash +# Example keyboard_chattering.sh +cd /path/to/folder && sudo python3 -m src.keyboard_main -k /dev/input/event0 -t 30 >> /var/log/keyboard_fix.log 2>&1 +``` + +**3. Automate using `rc.d` scripts:** +Create two files at `/usr/local/etc/rc.d/keyboard_fix` and `/usr/local/etc/rc.d/mouse_fix`. Here is the keyboard template (duplicate and adjust variables for the mouse): +```bash +#!/bin/sh +# REQUIRE: DAEMON +# PROVIDE: keyboard_fix + +. /etc/rc.subr + +name="keyboard_fix" +rcvar="keyboard_fix_enable" +# Use daemon to securely background the python script +command="/usr/sbin/daemon" +command_args="-p /var/run/keyboard_fix.pid -f /absolute/path/to/keyboard_chattering.sh" + +load_rc_config $name +run_rc_command "$1" +``` +Make them executable (`sudo chmod +x /usr/local/etc/rc.d/*_fix`), then enable them in your `/etc/rc.conf`: +```text +keyboard_fix_enable="YES" +mouse_fix_enable="YES" +``` +- **Start Right Now / Restart:** `sudo service keyboard_fix start` (or `restart`) +- **Status:** `sudo service keyboard_fix status` + *(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!)* From bf94ecf1f474ed1ed0e1e7d220c6f78a27d9618c Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Wed, 20 May 2026 01:04:36 -0400 Subject: [PATCH 59/79] Update README.md --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index cd0ef1c..86775e4 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,24 @@ systemctl status mouse_chattering.service journalctl -xeu mouse_chattering.service ``` +### Step 5: Applying Changes + +If you modify the service files, reload the daemon and restart the services to apply the changes: + +**For the Keyboard:** +```shell +sudo systemctl daemon-reload +sudo systemctl reenable keyboard_chattering.service +sudo systemctl restart keyboard_chattering.service +``` + +**For the Mouse:** +```shell +sudo systemctl daemon-reload +sudo systemctl reenable mouse_chattering.service +sudo systemctl restart mouse_chattering.service +``` + ## Automation (Non-Systemd & BSD) Because the Python scripts rely natively on the OS Kernel (`evdev` and `uinput`), the code works perfectly on non-systemd distributions and BSD variants. Ensure your `.sh` scripts are configured and executable (`chmod +x`), then use the guide below for your specific init system. From e688161c0226d1c3b68ee0788ebf416448f3e12f Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 12:47:37 -0400 Subject: [PATCH 60/79] Update README.md --- README.md | 340 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 265 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 86775e4..cf707b7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# __Keyboard Chattering & Mouse Double-Click Fix for Linux__ +# __Keyboard & Mouse Chattering Fix for Linux__ [![GitHub](https://img.shields.io/github/license/w2sv/KeyboardChatteringFix-Linux?)](LICENSE) @@ -6,7 +6,7 @@ __A tool for filtering mechanical keyboard chattering and mouse double-clicking ## The problem -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. +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, and faulty scroll wheel encoders will randomly glitch and scroll in the wrong direction. ## The existing solutions @@ -24,37 +24,17 @@ 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. - -**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 -Download the repository and extract the files. The dependencies are listed in `requirements.txt`. You can install them with the command below. +Download the repository and extract the files. `cd` into the extracted folder. -*(Note: According to PEP 668, newer Linux distributions may require the `--break-system-packages` flag, or the use of a python `venv`)*. +Due to PEP 668 on modern Linux distributions, globally installing Python packages via `pip` is restricted to prevent breaking system tools. You have two options to install the required `libevdev` dependency: -```shell -sudo pip install -r requirements.txt --break-system-packages -``` - -### Python Virtual Environment +### Option 1: Python Virtual Environment (Recommended) 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 -python -m venv venv +python3 -m venv venv # 2. Activate the virtual environment source venv/bin/activate @@ -63,18 +43,27 @@ source venv/bin/activate pip install -r requirements.txt ``` +### Option 2: Global Install (Quickest) +If you do not want to use a virtual environment, you can override the system protection flag. +```shell +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 manually: +Because keyboards and mice are handled differently by the OS, they are executed as separate modules. -**To run the Keyboard fix:** +**If you used a Virtual Environment (Option 1):** +Because `sudo` drops your local path, you must point `sudo` directly to your virtual environment's Python binary: ```shell -sudo python -m src.keyboard_main +sudo venv/bin/python3 -m src.keyboard_main +sudo venv/bin/python3 -m src.mouse_main ``` -**To run the Mouse fix:** +**If you installed Globally (Option 2):** ```shell -sudo python -m src.mouse_main +sudo python3 -m src.keyboard_main +sudo python3 -m src.mouse_main ``` ### Customization Options @@ -85,27 +74,56 @@ sudo python -m src.mouse_main - 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. +- `-r`, `--reconnect` + - Runs an infinite retry loop to wait for disconnected devices. Use this if running manually in a terminal or using `cron`, but **DO NOT** use this if using Systemd! Systemd natively handles restarts much cleaner in the background. - `--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}` + +### Advanced Mouse & Sensor Filtering Features +If you have a faulty mouse sensor or a broken scroll wheel, you can pass these additional arguments to `mouse_main`: +- `-sr SCROLL_REV`, `--scroll-reverse SCROLL_REV` + - Fixes scroll wheels that jump in the opposite direction. Filters direction changes that occur faster than the threshold. Default=0 (Disabled). Try `100` to `150` for glitchy wheels. +- `-sd SCROLL_DBL`, `--scroll-double SCROLL_DBL` + - Fixes worn encoders firing two ticks for one physical notch. Filters identical scrolls happening too fast. Default=0 (Disabled). *(Caution: Do not use this if your mouse has an infinite free-spinning scroll wheel!)* +- `-jl JUMP`, `--jump-limit JUMP` + - Blocks massive teleporting cursor jumps caused by dirty laser sensors or hairs. Drops frames exceeding X pixels (e.g., `300`). + +### Hotplugging, Remapping & Per-Key Thresholds +The configuration files (`src/keyboard_config.py` and `src/mouse_config.py`) contain powerful advanced options: +- **Per-Key Thresholds:** Keys physically wear differently. You can set your heavy Spacebar to a `50ms` delay to prevent chatter, while leaving your `A` key at `15ms` for fast gaming. +- **Remapping / Macros:** Because this intercepts kernel events, you can natively remap buttons (e.g. swap `KEY_CAPSLOCK` to `KEY_LEFTCTRL`, or `BTN_SIDE` to `BTN_MIDDLE`). This works flawlessly on both X11 and Wayland. + +### 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 a mouse, a keyboard, and a multimedia controller all at once! + +Because of this, both the keyboard and mouse scripts will list *all* available event endpoints to give you maximum flexibility. 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 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. + +**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 service currently running! The script requires an exclusive lock on the hardware. Stop your background service to release the lock before testing manually. ## Automation (Systemd) -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. +Starting the scripts manually every time is not ideal. You should set them up as background Systemd services. ### 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. +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. *(Note: If using a venv, replace `python3` with `venv/bin/python3`).* **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 +cd /home/foouser/Downloads/HardwareChatteringFix-Linux/ && sudo python3 -m src.keyboard_main -k usb-Logitech_Keyboard-event-kbd -t 40 --keys KEY_E,KEY_SPACE ``` **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 +cd /home/foouser/Downloads/HardwareChatteringFix-Linux/ && sudo python3 -m src.mouse_main -m usb-Logitech_Mouse-event-mouse -t 50 --buttons BTN_LEFT,BTN_RIGHT ``` Make sure to change the file permissions so they are executable: @@ -114,7 +132,7 @@ 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. +Edit `keyboard_chattering.service` and `mouse_chattering.service` to point `ExecStart` to the absolute path of your `.sh` files. **Example keyboard_chattering.service:** ```shell @@ -185,44 +203,133 @@ sudo systemctl reenable mouse_chattering.service sudo systemctl restart mouse_chattering.service ``` +--- + ## Automation (Non-Systemd & BSD) Because the Python scripts rely natively on the OS Kernel (`evdev` and `uinput`), the code works perfectly on non-systemd distributions and BSD variants. Ensure your `.sh` scripts are configured and executable (`chmod +x`), then use the guide below for your specific init system. -**PRO-TIP FOR LOGGING:** Since non-systemd systems lack `journalctl`, you should modify your `.sh` scripts to redirect output so you can read the logs. Append this to the execution lines in your `.sh` files: -`... -t 30 >> /var/log/keyboard_fix.log 2>&1` (Do the same for `mouse_fix.log`). -You can then read your logs anytime using `cat /var/log/keyboard_fix.log`. +> **💡 PRO-TIP FOR LOGGING:** Since non-systemd systems lack `journalctl`, you should modify your `.sh` scripts to redirect output so you can read the logs. Append a redirect to the execution lines in your `.sh` files: +> ```bash +> # For the keyboard script: +> cd /absolute/path/to/project && sudo python3 -m src.keyboard_main -k -t 30 >> /var/log/keyboard_fix.log 2>&1 +> +> # For the mouse script: +> cd /absolute/path/to/project && sudo python3 -m src.mouse_main -m -t 30 >> /var/log/mouse_fix.log 2>&1 +> ``` +> You can then read your logs anytime using `cat /var/log/keyboard_fix.log` or `cat /var/log/mouse_fix.log`. + +--- ### Cron (Universal Fallback) + The easiest way to run the scripts on any system without Systemd is using `cron`'s `@reboot` directive. -1. Open the root crontab: `sudo crontab -e` -2. Add both scripts to run in the background (using `&`): + +1. Because `cron` does not auto-restart failed scripts, you **MUST** add the `-r` flag to your `.sh` scripts so they survive hardware disconnects! + ```bash + # Example addition inside your .sh files: + python3 -m src.keyboard_main -k -t 30 -r + python3 -m src.mouse_main -m -t 30 -r + ``` +2. Open the root crontab: `sudo crontab -e` +3. Add both scripts to run in the background (using `&`): ```text @reboot /absolute/path/to/keyboard_chattering.sh & @reboot /absolute/path/to/mouse_chattering.sh & ``` -- **Start Right Now:** Run `sudo /absolute/path/to/keyboard_chattering.sh &` in your terminal. -- **Status/Logs:** Run `ps aux | grep python3` to ensure they are running. View the `.log` files defined in your script. -- **Restart:** Run `sudo pkill -f keyboard_main` (or `mouse_main`), then manually start them again. + +**Operational Commands:** +* **Start Right Now:** + ```bash + sudo /absolute/path/to/keyboard_chattering.sh & + sudo /absolute/path/to/mouse_chattering.sh & + ``` +* **Status/Logs:** + ```bash + ps aux | grep -E 'keyboard_main|mouse_main' + cat /var/log/keyboard_fix.log + cat /var/log/mouse_fix.log + ``` +* **Restart:** + ```bash + sudo pkill -f keyboard_main; sudo /absolute/path/to/keyboard_chattering.sh & + sudo pkill -f mouse_main; sudo /absolute/path/to/mouse_chattering.sh & + ``` + +--- ### OpenRC (Artix, Alpine, Gentoo) -1. Create two files: `/etc/init.d/keyboard_fix` and `/etc/init.d/mouse_fix` -2. Paste this template (Adjust names/paths for the mouse version!): + +OpenRC supports native respawning, so you do **not** need the `-r` flag. + +1. Create two files: `sudo nano /etc/init.d/keyboard_fix` and `sudo nano /etc/init.d/mouse_fix` +2. Paste the appropriate template below into each file: + +**Keyboard Template (`/etc/init.d/keyboard_fix`):** +```bash +#!/sbin/openrc-run +name="Keyboard Chattering Fix" +command="/absolute/path/to/keyboard_chattering.sh" +command_background=true +pidfile="/run/keyboard_fix.pid" + +# Enable native systemd-like auto-restarts! +respawn=true +respawn_delay=5 + +depend() { need localmount } +``` + +**Mouse Template (`/etc/init.d/mouse_fix`):** +```bash +#!/sbin/openrc-run +name="Mouse Chattering Fix" +command="/absolute/path/to/mouse_chattering.sh" +command_background=true +pidfile="/run/mouse_fix.pid" + +respawn=true +respawn_delay=5 + +depend() { need localmount } +``` + +3. Make them executable: `sudo chmod +x /etc/init.d/keyboard_fix /etc/init.d/mouse_fix` +4. Enable at boot: ```bash - #!/sbin/openrc-run - name="Keyboard Chattering Fix" - command="/absolute/path/to/keyboard_chattering.sh" - command_background=true - pidfile="/run/keyboard_fix.pid" - depend() { need localmount } + sudo rc-update add keyboard_fix default + sudo rc-update add mouse_fix default ``` -3. Make them executable: `sudo chmod +x /etc/init.d/keyboard_fix /etc/init.d/mouse_fix` -4. Enable at boot: `sudo rc-update add keyboard_fix default` and `sudo rc-update add mouse_fix default` -- **Start Right Now / Restart:** `sudo rc-service keyboard_fix start` (or `restart`) -- **Status:** `sudo rc-service keyboard_fix status` + +**Operational Commands:** +* **Start Right Now:** + ```bash + sudo rc-service keyboard_fix start + sudo rc-service mouse_fix start + ``` +* **Status/Logs:** + ```bash + sudo rc-service keyboard_fix status + sudo rc-service mouse_fix status + cat /var/log/keyboard_fix.log + cat /var/log/mouse_fix.log + ``` +* **Restart:** + ```bash + sudo rc-service keyboard_fix restart + sudo rc-service mouse_fix restart + ``` + +--- ### Runit (Void Linux) -1. Create service directories: `sudo mkdir -p /etc/sv/keyboard_fix /etc/sv/mouse_fix` + +Runit also supports native respawning, so you do **not** need the `-r` flag. + +1. Create service directories: + ```bash + sudo mkdir -p /etc/sv/keyboard_fix /etc/sv/mouse_fix + ``` 2. Create a run file for the keyboard: `sudo nano /etc/sv/keyboard_fix/run` ```bash #!/bin/sh @@ -233,23 +340,64 @@ The easiest way to run the scripts on any system without Systemd is using `cron` #!/bin/sh exec /absolute/path/to/mouse_chattering.sh ``` -4. Make both executable: `sudo chmod +x /etc/sv/keyboard_fix/run /etc/sv/mouse_fix/run` -5. Enable them: `sudo ln -s /etc/sv/keyboard_fix /var/service/` and `sudo ln -s /etc/sv/mouse_fix /var/service/` -- **Start Right Now:** Runit detects the symlinks and starts them automatically! -- **Status:** `sudo sv status keyboard_fix mouse_fix` -- **Restart:** `sudo sv restart keyboard_fix mouse_fix` +4. Make both executable: + ```bash + sudo chmod +x /etc/sv/keyboard_fix/run /etc/sv/mouse_fix/run + ``` +5. Enable them (symlink to runit's service directory): + ```bash + sudo ln -s /etc/sv/keyboard_fix /var/service/ + sudo ln -s /etc/sv/mouse_fix /var/service/ + ``` + +**Operational Commands:** +* **Start Right Now:** Runit detects the symlinks and starts them automatically! +* **Status/Logs:** + ```bash + sudo sv status keyboard_fix mouse_fix + cat /var/log/keyboard_fix.log + cat /var/log/mouse_fix.log + ``` +* **Restart:** + ```bash + sudo sv restart keyboard_fix + sudo sv restart mouse_fix + ``` + +--- ### SysVinit (Devuan, Older Distros) + +SysVinit does not restart scripts natively. You **MUST** add the `-r` flag to your `.sh` scripts (as shown in the Cron section). + Simply add the executable scripts to your `/etc/rc.local` file before the `exit 0` line: ```bash /absolute/path/to/keyboard_chattering.sh & /absolute/path/to/mouse_chattering.sh & exit 0 ``` -- **Start Right Now:** Run `sudo /etc/rc.local` -- **Status / Restart:** Use `ps aux | grep python3` to check status, and `kill` them to stop them. + +**Operational Commands:** +* **Start Right Now:** + ```bash + sudo /etc/rc.local + ``` +* **Status/Logs:** + ```bash + ps aux | grep -E 'keyboard_main|mouse_main' + cat /var/log/keyboard_fix.log + cat /var/log/mouse_fix.log + ``` +* **Restart:** + ```bash + sudo pkill -f keyboard_main; sudo /absolute/path/to/keyboard_chattering.sh & + sudo pkill -f mouse_main; sudo /absolute/path/to/mouse_chattering.sh & + ``` + +--- ### FreeBSD / BSD Family + FreeBSD has native support for `evdev`, but you must load the modules and adjust device paths. **1. Load evdev modules:** Add these to `/boot/loader.conf` and reboot (or `kldload` them now): @@ -264,10 +412,15 @@ Update your `.sh` scripts to pass the raw absolute path directly to `-k` or `-m` ```bash # Example keyboard_chattering.sh cd /path/to/folder && sudo python3 -m src.keyboard_main -k /dev/input/event0 -t 30 >> /var/log/keyboard_fix.log 2>&1 + +# Example mouse_chattering.sh +cd /path/to/folder && sudo python3 -m src.mouse_main -m /dev/input/event1 -t 30 >> /var/log/mouse_fix.log 2>&1 ``` **3. Automate using `rc.d` scripts:** -Create two files at `/usr/local/etc/rc.d/keyboard_fix` and `/usr/local/etc/rc.d/mouse_fix`. Here is the keyboard template (duplicate and adjust variables for the mouse): +Create two files: `sudo nano /usr/local/etc/rc.d/keyboard_fix` and `sudo nano /usr/local/etc/rc.d/mouse_fix`. + +**Keyboard Template (`/usr/local/etc/rc.d/keyboard_fix`):** ```bash #!/bin/sh # REQUIRE: DAEMON @@ -277,19 +430,56 @@ Create two files at `/usr/local/etc/rc.d/keyboard_fix` and `/usr/local/etc/rc.d/ name="keyboard_fix" rcvar="keyboard_fix_enable" -# Use daemon to securely background the python script + +# Use daemon to securely background the script. +# We pass the -r flag to 'daemon' so it auto-restarts the script if it dies! command="/usr/sbin/daemon" -command_args="-p /var/run/keyboard_fix.pid -f /absolute/path/to/keyboard_chattering.sh" +command_args="-r -P /var/run/keyboard_fix.pid -f /absolute/path/to/keyboard_chattering.sh" load_rc_config $name run_rc_command "$1" ``` -Make them executable (`sudo chmod +x /usr/local/etc/rc.d/*_fix`), then enable them in your `/etc/rc.conf`: -```text -keyboard_fix_enable="YES" -mouse_fix_enable="YES" + +**Mouse Template (`/usr/local/etc/rc.d/mouse_fix`):** +```bash +#!/bin/sh +# REQUIRE: DAEMON +# PROVIDE: mouse_fix + +. /etc/rc.subr + +name="mouse_fix" +rcvar="mouse_fix_enable" + +command="/usr/sbin/daemon" +command_args="-r -P /var/run/mouse_fix.pid -f /absolute/path/to/mouse_chattering.sh" + +load_rc_config $name +run_rc_command "$1" ``` -- **Start Right Now / Restart:** `sudo service keyboard_fix start` (or `restart`) -- **Status:** `sudo service keyboard_fix status` -*(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!)* +4. Make them executable: `sudo chmod +x /usr/local/etc/rc.d/keyboard_fix /usr/local/etc/rc.d/mouse_fix` +5. Enable them in your `/etc/rc.conf`: + ```text + keyboard_fix_enable="YES" + mouse_fix_enable="YES" + ``` + +**Operational Commands:** +* **Start Right Now:** + ```bash + sudo service keyboard_fix start + sudo service mouse_fix start + ``` +* **Status/Logs:** + ```bash + sudo service keyboard_fix status + sudo service mouse_fix status + cat /var/log/keyboard_fix.log + cat /var/log/mouse_fix.log + ``` +* **Restart:** + ```bash + sudo service keyboard_fix restart + sudo service mouse_fix restart + ``` From f2375623c08c78b46a87f49ab1dd4c949119c84a Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 15:27:36 -0400 Subject: [PATCH 61/79] Update README.md --- README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cf707b7..8f5566d 100644 --- a/README.md +++ b/README.md @@ -187,17 +187,46 @@ journalctl -xeu mouse_chattering.service ### Step 5: Applying Changes -If you modify the service files, reload the daemon and restart the services to apply the changes: +How you apply changes depends on which files you modified. -**For the Keyboard:** +**Scenario A: You edited the Python code, `config.py`, or the `.sh` shell scripts** + +Systemd doesn't need to reload its own configuration; it just needs to restart the service to execute the newly saved scripts. ```shell -sudo systemctl daemon-reload -sudo systemctl reenable keyboard_chattering.service +# For the Keyboard: sudo systemctl restart keyboard_chattering.service + +# For the Mouse: +sudo systemctl restart mouse_chattering.service ``` -**For the Mouse:** +**Scenario B: You edited the `.service` files themselves** + +If you changed settings inside the `.service` files (like `Restart=`, `ExecStart=`, etc.), you must tell systemd to re-read those files from disk before restarting. +```shell +# For the Keyboard: +sudo systemctl stop keyboard_chattering.service # Safely stops the current running instance +sudo systemctl daemon-reload # Tells systemd to read the updated .service file +sudo systemctl restart keyboard_chattering.service # Starts the service using the new configuration + +# For the Mouse: +sudo systemctl stop mouse_chattering.service +sudo systemctl daemon-reload +sudo systemctl restart mouse_chattering.service +``` + +**Scenario C: You edited the `[Install]` section of the `.service` files** + +The `[Install]` section dictates *when* and *how* the service starts at boot (via `WantedBy=`). If you changed this section, you must re-enable the service to update the boot symlinks. ```shell +# For the Keyboard: +sudo systemctl stop keyboard_chattering.service +sudo systemctl daemon-reload +sudo systemctl reenable keyboard_chattering.service # Removes old boot symlinks and creates new ones +sudo systemctl restart keyboard_chattering.service + +# For the Mouse: +sudo systemctl stop mouse_chattering.service sudo systemctl daemon-reload sudo systemctl reenable mouse_chattering.service sudo systemctl restart mouse_chattering.service @@ -244,6 +273,11 @@ The easiest way to run the scripts on any system without Systemd is using `cron` sudo /absolute/path/to/keyboard_chattering.sh & sudo /absolute/path/to/mouse_chattering.sh & ``` +* **Stop:** + ```bash + sudo pkill -f keyboard_main + sudo pkill -f mouse_main + ``` * **Status/Logs:** ```bash ps aux | grep -E 'keyboard_main|mouse_main' @@ -255,6 +289,7 @@ The easiest way to run the scripts on any system without Systemd is using `cron` sudo pkill -f keyboard_main; sudo /absolute/path/to/keyboard_chattering.sh & sudo pkill -f mouse_main; sudo /absolute/path/to/mouse_chattering.sh & ``` +* **Reenable (Boot Integration):** Run `sudo crontab -e` and update the `@reboot` lines. Cron applies changes automatically on next boot. --- @@ -307,6 +342,11 @@ depend() { need localmount } sudo rc-service keyboard_fix start sudo rc-service mouse_fix start ``` +* **Stop:** + ```bash + sudo rc-service keyboard_fix stop + sudo rc-service mouse_fix stop + ``` * **Status/Logs:** ```bash sudo rc-service keyboard_fix status @@ -319,6 +359,11 @@ depend() { need localmount } sudo rc-service keyboard_fix restart sudo rc-service mouse_fix restart ``` +* **Reenable (Boot Integration):** + ```bash + sudo rc-update del keyboard_fix default && sudo rc-update add keyboard_fix default + sudo rc-update del mouse_fix default && sudo rc-update add mouse_fix default + ``` --- @@ -352,6 +397,11 @@ Runit also supports native respawning, so you do **not** need the `-r` flag. **Operational Commands:** * **Start Right Now:** Runit detects the symlinks and starts them automatically! +* **Stop:** + ```bash + sudo sv stop keyboard_fix + sudo sv stop mouse_fix + ``` * **Status/Logs:** ```bash sudo sv status keyboard_fix mouse_fix @@ -363,6 +413,11 @@ Runit also supports native respawning, so you do **not** need the `-r` flag. sudo sv restart keyboard_fix sudo sv restart mouse_fix ``` +* **Reenable (Boot Integration):** + ```bash + sudo rm /var/service/keyboard_fix && sudo ln -s /etc/sv/keyboard_fix /var/service/ + sudo rm /var/service/mouse_fix && sudo ln -s /etc/sv/mouse_fix /var/service/ + ``` --- @@ -382,6 +437,11 @@ exit 0 ```bash sudo /etc/rc.local ``` +* **Stop:** + ```bash + sudo pkill -f keyboard_main + sudo pkill -f mouse_main + ``` * **Status/Logs:** ```bash ps aux | grep -E 'keyboard_main|mouse_main' @@ -393,6 +453,11 @@ exit 0 sudo pkill -f keyboard_main; sudo /absolute/path/to/keyboard_chattering.sh & sudo pkill -f mouse_main; sudo /absolute/path/to/mouse_chattering.sh & ``` +* **Reenable (Boot Integration):** *(If using `update-rc.d` instead of `rc.local`)* + ```bash + sudo update-rc.d -f keyboard_fix remove && sudo update-rc.d keyboard_fix defaults + sudo update-rc.d -f mouse_fix remove && sudo update-rc.d mouse_fix defaults + ``` --- @@ -471,6 +536,11 @@ run_rc_command "$1" sudo service keyboard_fix start sudo service mouse_fix start ``` +* **Stop:** + ```bash + sudo service keyboard_fix stop + sudo service mouse_fix stop + ``` * **Status/Logs:** ```bash sudo service keyboard_fix status @@ -483,3 +553,8 @@ run_rc_command "$1" sudo service keyboard_fix restart sudo service mouse_fix restart ``` +* **Reenable (Boot Integration):** Ensure `/etc/rc.conf` contains the enable variables. You can quickly force them using: + ```bash + sudo sysrc keyboard_fix_enable="YES" + sudo sysrc mouse_fix_enable="YES" + ``` From 2c1ec8234ba66d59dc9d7a193bafd09ba3ce8105 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:49:26 -0400 Subject: [PATCH 62/79] Update keyboard_chattering.service --- keyboard_chattering.service | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/keyboard_chattering.service b/keyboard_chattering.service index d989225..233eb6e 100644 --- a/keyboard_chattering.service +++ b/keyboard_chattering.service @@ -15,7 +15,4 @@ Restart=always RestartSec=5 [Install] -WantedBy=multi-user.target -WantedBy=suspend.target -WantedBy=hibernate.target -WantedBy=hybrid-sleep.target +WantedBy=multi-user.target suspend.target hibernate.target hybrid-sleep.target From 8c9334de06725d7d691e9c31060f668fdafcdc68 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:49:47 -0400 Subject: [PATCH 63/79] Update mouse_chattering.service --- mouse_chattering.service | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mouse_chattering.service b/mouse_chattering.service index eb3a999..289a44b 100644 --- a/mouse_chattering.service +++ b/mouse_chattering.service @@ -15,7 +15,4 @@ Restart=always RestartSec=5 [Install] -WantedBy=multi-user.target -WantedBy=suspend.target -WantedBy=hibernate.target -WantedBy=hybrid-sleep.target +WantedBy=multi-user.target suspend.target hibernate.target hybrid-sleep.target From 50e51954252699d59dd6f84230a00f520d1780f8 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:50:24 -0400 Subject: [PATCH 64/79] Update keyboard_config.py --- src/keyboard_config.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/keyboard_config.py b/src/keyboard_config.py index d217b2e..3f2c487 100644 --- a/src/keyboard_config.py +++ b/src/keyboard_config.py @@ -2,16 +2,46 @@ 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. +1. Command Line (--keys / -k): Highest priority. If provided, this file is ignored. +2. This File: Used if no command line argument is provided. +3. Interactive Prompt: Used ONLY if both CLI and this file are empty. """ +import libevdev +# ========================================== +# 0. DEFAULT DEVICE +# ========================================== +# Set this to avoid the interactive prompt on boot/startup. +# Leave as "" to be asked every time if running manually. +# Example: DEVICE_NAME = "usb-Corsair_Corsair_K70_RGB-event-kbd" +DEVICE_NAME = "" + +# ========================================== +# 1. SPECIFIC KEY FILTERING (Allowlist) +# ========================================== # To filter specific keys, add them to this set. -# Example: FILTERED_KEYS = {"KEY_A", "KEY_SPACE", "KEY_ENTER"} +# Example: FILTERED_KEYS = {"KEY_A", "KEY_SPACE"} # Leave it empty as set() to filter ALL keys by default. FILTERED_KEYS = set() +# ========================================== +# 2. PER-KEY THRESHOLDS +# ========================================== +# Override the default threshold for specific keys. +# Great for heavy spacebars (needs more debounce) vs light gaming keys (needs less lag). +KEY_THRESHOLDS = { + # "KEY_SPACE": 50, + # "KEY_A": 15, +} + +# ========================================== +# 3. KEY REMAPPING / MACROS +# ========================================== +# Swap keys at the kernel level. Format: {"KEY_PRESSED": "KEY_OUTPUT"} +KEY_MAP = { + # "KEY_CAPSLOCK": "KEY_LEFTCTRL", # Example: Make CapsLock act as Left Ctrl +} + # ========================================== # REFERENCE: COMMON KEY VALUES TO COPY/PASTE From 6438bb0d523a1a6d5ae7ddd5ac7f39a363172752 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:50:50 -0400 Subject: [PATCH 65/79] Update mouse_config.py --- src/mouse_config.py | 53 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/mouse_config.py b/src/mouse_config.py index d6fbf25..a8672f3 100644 --- a/src/mouse_config.py +++ b/src/mouse_config.py @@ -2,19 +2,57 @@ 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. +1. Command Line (--buttons / -m): Highest priority. If provided, this file is ignored. +2. This File: Used if no command line argument is provided. +3. Interactive Prompt: Used ONLY if both CLI and this file are empty. """ +import libevdev +# ========================================== +# 0. DEFAULT DEVICE +# ========================================== +# Set this to avoid the interactive prompt on boot/startup. +# Leave as "" to be asked every time if running manually. +# Example: DEVICE_NAME = "usb-Logitech_Gaming_Mouse-event-mouse" +DEVICE_NAME = "" + +# ========================================== +# 1. SPECIFIC BUTTON FILTERING (Allowlist) +# ========================================== # 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() +# ========================================== +# 2. SCROLL AXIS FILTERING (Allowlist) +# ========================================== +# To filter specific scroll directions, add their libevdev codes to this set. +# Leave it empty as set() to filter ALL scroll axes by default. +# Example: If your Logitech MX horizontal wheel glitches, but vertical is fine: +# FILTERED_SCROLL_AXES = {"REL_HWHEEL", "REL_HWHEEL_HI_RES"} +FILTERED_SCROLL_AXES = set() + +# ========================================== +# 3. PER-BUTTON THRESHOLDS +# ========================================== +# Override the default threshold for specific buttons. +BUTTON_THRESHOLDS = { + # "BTN_LEFT": 30, + # "BTN_SIDE": 50, +} + +# ========================================== +# 4. BUTTON REMAPPING / MACROS +# ========================================== +# Swap buttons at the kernel level. Format: {"BUTTON_PRESSED": "BUTTON_OUTPUT"} +BUTTON_MAP = { + # "BTN_SIDE": "BTN_MIDDLE", # Example: Make a side thumb button act as a middle click +} + # ========================================== -# REFERENCE: COMMON MOUSE BUTTON VALUES +# REFERENCE: COMMON MOUSE BUTTON & AXIS VALUES # ========================================== # Standard Clicks: # BTN_LEFT (Standard Left Click) @@ -31,5 +69,8 @@ # 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! +# Scroll Axes: +# REL_WHEEL (Standard Vertical Scroll) +# REL_HWHEEL (Standard Horizontal Scroll) +# REL_WHEEL_HI_RES (High-Resolution Vertical Scroll) +# REL_HWHEEL_HI_RES (High-Resolution Horizontal Scroll) From 4638b16ea6bdda96e4fac5d6931bf1708f19a66d Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:51:27 -0400 Subject: [PATCH 66/79] Update keyboard_main.py --- src/keyboard_main.py | 97 ++++++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 25 deletions(-) diff --git a/src/keyboard_main.py b/src/keyboard_main.py index 4321124..623441f 100755 --- a/src/keyboard_main.py +++ b/src/keyboard_main.py @@ -2,7 +2,7 @@ import logging import sys import os -from contextlib import contextmanager +import time import libevdev from src.keyboard_filtering import filter_chattering @@ -10,27 +10,9 @@ # Safely import the config file if it exists try: - from src.keyboard_config import FILTERED_KEYS + from src.keyboard_config import DEVICE_NAME, FILTERED_KEYS, KEY_THRESHOLDS, KEY_MAP except ImportError: - FILTERED_KEYS = set() - -@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) - - fd = open(device_path, 'rb') - evdev = libevdev.Device(fd) - try: - yield evdev - finally: - fd.close() + DEVICE_NAME, FILTERED_KEYS, KEY_THRESHOLDS, KEY_MAP = "", set(), {}, {} def parse_keys(keys_str): """Parses comma-separated CLI arguments into a list of strings.""" @@ -41,6 +23,7 @@ def parse_keys(keys_str): parser = argparse.ArgumentParser() parser.add_argument('-k', '--keyboard', type=str, default=str()) parser.add_argument('-t', '--threshold', type=int, default=30) + parser.add_argument('-r', '--reconnect', action='store_true', help="Loop infinitely and wait for device reconnection instead of exiting") 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() @@ -49,16 +32,80 @@ 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) + # PREVENT SILENT FAILURES: Validate config thresholds/maps at startup + for key_name in list(KEY_THRESHOLDS.keys()): + try: + libevdev.evbit(key_name) + except Exception: + logging.warning(f"KEY_THRESHOLDS typo or invalid key: '{key_name}' will be ignored.") + + for key_name in list(KEY_MAP.keys()): + try: + libevdev.evbit(key_name) + except Exception: + logging.warning(f"KEY_MAP typo or invalid key: '{key_name}' will be ignored.") + 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)) + if key: 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) + if KEY_MAP: logging.info(f"Loaded {len(KEY_MAP)} Key Mappings: {KEY_MAP}") + if KEY_THRESHOLDS: logging.info(f"Loaded {len(KEY_THRESHOLDS)} Custom Thresholds: {KEY_THRESHOLDS}") + + # CONFIG PRECEDENCE: CLI args > keyboard_config.py > Interactive Prompt + # This prevents Systemd from freezing at boot if it hits an input() prompt! + if args.keyboard: + device_name_str = args.keyboard + elif DEVICE_NAME: + device_name_str = DEVICE_NAME + else: + logging.warning("No device specified in CLI or config. Falling back to interactive prompt.") + device_name_str = retrieve_keyboard_name() + + device_path = abs_keyboard_path(device_name_str) + + if args.reconnect: + # INFINITE RETRY LOOP (Best for Cron, SysVinit, and Manual Terminal usage) + while True: + if not os.path.exists(device_path): + logging.info(f"Waiting for keyboard '{device_name_str}' to connect...") + time.sleep(2) + continue + + try: + with open(device_path, 'rb') as fd: + device = libevdev.Device(fd) + logging.info(f"Successfully connected to '{device_name_str}'") + filter_chattering(device, args.threshold, keys_to_filter, KEY_THRESHOLDS, KEY_MAP) + except OSError as e: + if e.errno == 19: + logging.warning("Keyboard disconnected. Waiting for reconnect...") + else: + logging.error(f"OS Error: {e}. Retrying in 2s...") + time.sleep(2) + except Exception as e: + logging.error(f"Unexpected Error: {e}. Retrying in 2s...") + time.sleep(2) + else: + # FAIL & EXIT MODE (Default: Best for Systemd, OpenRC, and Runit) + if not os.path.exists(device_path): + logging.critical(f"Keyboard '{device_name_str}' not found. Exiting cleanly.") + sys.exit(0) + + try: + with open(device_path, 'rb') as fd: + device = libevdev.Device(fd) + logging.info(f"Successfully connected to '{device_name_str}'") + filter_chattering(device, args.threshold, keys_to_filter, KEY_THRESHOLDS, KEY_MAP) + except OSError as e: + if e.errno == 19: + logging.critical("Keyboard disconnected. Exiting cleanly.") + sys.exit(0) + else: + raise e From 8faa8fbe42af97337a9baad3b6858f51701646a7 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:51:49 -0400 Subject: [PATCH 67/79] Update mouse_main.py --- src/mouse_main.py | 125 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 94 insertions(+), 31 deletions(-) diff --git a/src/mouse_main.py b/src/mouse_main.py index 90d5186..b24e7fb 100644 --- a/src/mouse_main.py +++ b/src/mouse_main.py @@ -2,7 +2,7 @@ import logging import sys import os -from contextlib import contextmanager +import time import libevdev from src.mouse_filtering import filter_mouse_chattering @@ -10,36 +10,24 @@ # Safely import the config file if it exists try: - from src.mouse_config import FILTERED_BUTTONS + from src.mouse_config import DEVICE_NAME, FILTERED_BUTTONS, FILTERED_SCROLL_AXES, BUTTON_THRESHOLDS, BUTTON_MAP except ImportError: - FILTERED_BUTTONS = set() + DEVICE_NAME, FILTERED_BUTTONS, FILTERED_SCROLL_AXES, BUTTON_THRESHOLDS, BUTTON_MAP = "", set(), set(), {}, {} -@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) - - fd = open(device_path, 'rb') - evdev = libevdev.Device(fd) - try: - yield evdev - finally: - fd.close() - -def parse_buttons(buttons_str): +def parse_list(data_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(',')] + if not data_str: return [] + return [d.strip() for d in data_str.split(',')] if __name__ == "__main__": parser = argparse.ArgumentParser() 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('-sr', '--scroll-reverse', type=int, default=0, help="Block scroll reversing directions (ms)") + parser.add_argument('-sd', '--scroll-double', type=int, default=0, help="Block scroll same direction (ms)") + parser.add_argument('-jl', '--jump-limit', type=int, default=0, help="Block teleporting cursor >= X pixels") + parser.add_argument('-r', '--reconnect', action='store_true', help="Loop infinitely and wait for device reconnection instead of exiting") + parser.add_argument('--buttons', type=parse_list, default=[]) parser.add_argument('-v', '--verbosity', type=int, default=1, choices=[0, 1, 2]) args = parser.parse_args() @@ -47,16 +35,91 @@ 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) + # PREVENT SILENT FAILURES: Validate config thresholds/maps at startup + for btn_name in list(BUTTON_THRESHOLDS.keys()): + try: + libevdev.evbit(btn_name) + except Exception: + logging.warning(f"BUTTON_THRESHOLDS typo or invalid key: '{btn_name}' will be ignored.") + + for btn_name in list(BUTTON_MAP.keys()): + try: + libevdev.evbit(btn_name) + except Exception: + logging.warning(f"BUTTON_MAP typo or invalid key: '{btn_name}' will be ignored.") + buttons_list = args.buttons if args.buttons else list(FILTERED_BUTTONS) - buttons_to_filter = [] + axes_list = list(FILTERED_SCROLL_AXES) - # Convert string button names (e.g., "BTN_LEFT") to libevdev.EventCode objects - for btn in buttons_list: + buttons_to_filter = [] + for b in buttons_list: + try: + if b: buttons_to_filter.append(libevdev.evbit(b)) + except Exception as e: + logging.warning(f"Button '{b}' ignored: {e}") + + axes_to_filter = [] + for a in axes_list: try: - buttons_to_filter.append(libevdev.evbit(btn)) + if a: axes_to_filter.append(libevdev.evbit(a)) except Exception as e: - logging.warning(f"Button '{btn}' ignored: {e}") + logging.warning(f"Axis '{a}' ignored: {e}") + + if BUTTON_MAP: logging.info(f"Loaded {len(BUTTON_MAP)} Button Mappings: {BUTTON_MAP}") + if BUTTON_THRESHOLDS: logging.info(f"Loaded {len(BUTTON_THRESHOLDS)} Custom Thresholds: {BUTTON_THRESHOLDS}") + + # CONFIG PRECEDENCE: CLI args > mouse_config.py > Interactive Prompt + # This prevents Systemd from freezing at boot if it hits an input() prompt! + if args.mouse: + device_name_str = args.mouse + elif DEVICE_NAME: + device_name_str = DEVICE_NAME + else: + logging.warning("No device specified in CLI or config. Falling back to interactive prompt.") + device_name_str = retrieve_mouse_name() - with get_device_handle(args.mouse or retrieve_mouse_name()) as device: - filter_mouse_chattering(device, args.threshold, buttons_to_filter) + device_path = abs_mouse_path(device_name_str) + + if args.reconnect: + # INFINITE RETRY LOOP (Best for Cron, SysVinit, and Manual Terminal usage) + while True: + if not os.path.exists(device_path): + logging.info(f"Waiting for mouse '{device_name_str}' to connect...") + time.sleep(2) + continue + + try: + with open(device_path, 'rb') as fd: + device = libevdev.Device(fd) + logging.info(f"Successfully connected to '{device_name_str}'") + filter_mouse_chattering(device, args.threshold, args.scroll_reverse, args.scroll_double, + args.jump_limit, buttons_to_filter, axes_to_filter, + BUTTON_THRESHOLDS, BUTTON_MAP) + except OSError as e: + if e.errno == 19: + logging.warning("Mouse disconnected. Waiting for reconnect...") + else: + logging.error(f"OS Error: {e}. Retrying in 2s...") + time.sleep(2) + except Exception as e: + logging.error(f"Unexpected Error: {e}. Retrying in 2s...") + time.sleep(2) + else: + # FAIL & EXIT MODE (Default: Best for Systemd, OpenRC, and Runit) + if not os.path.exists(device_path): + logging.critical(f"Mouse '{device_name_str}' not found. Exiting cleanly.") + sys.exit(0) + + try: + with open(device_path, 'rb') as fd: + device = libevdev.Device(fd) + logging.info(f"Successfully connected to '{device_name_str}'") + filter_mouse_chattering(device, args.threshold, args.scroll_reverse, args.scroll_double, + args.jump_limit, buttons_to_filter, axes_to_filter, + BUTTON_THRESHOLDS, BUTTON_MAP) + except OSError as e: + if e.errno == 19: + logging.critical("Mouse disconnected. Exiting cleanly.") + sys.exit(0) + else: + raise e From ce1143530f24a08ce49dadbe6344ca2f89061cdb Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:52:17 -0400 Subject: [PATCH 68/79] Update keyboard_chattering.sh --- keyboard_chattering.sh | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/keyboard_chattering.sh b/keyboard_chattering.sh index 5ddb8b0..dc02a9e 100644 --- a/keyboard_chattering.sh +++ b/keyboard_chattering.sh @@ -1,6 +1,16 @@ #!/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 Linux/Python, you may need to run: sudo pip3 install -r requirements.txt --break-system-packages) +# ============================================================================== +# KEYBOARD CHATTERING FIX - STARTUP SCRIPT +# ============================================================================== +# Change the `cd` path below to the absolute path of this project folder. +# Ensure this script is executable: chmod +x keyboard_chattering.sh +# +# AVAILABLE ARGUMENTS: +# -k "usb-id-here" : The device ID of your keyboard in /dev/input/by-id/ +# -t 30 : Bounce filter threshold in milliseconds (Default: 30) +# --keys KEY_A,KEY_SPACE : Explicitly filter only these keys (Leave blank for all) +# -r : Enable auto-reconnect infinite loop (Required for Cron/SysVinit) +# DO NOT USE `-r` if using Systemd! Systemd handles restarts natively. +# ============================================================================== -cd && sudo python3 -m src.keyboard_main -k -t 30 +cd /absolute/path/to/project && sudo python3 -m src.keyboard_main -k -t 30 From 3615bc8eab72ab1d9bd37c7bd14bf795193e1e50 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:52:42 -0400 Subject: [PATCH 69/79] Update mouse_chattering.sh --- mouse_chattering.sh | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/mouse_chattering.sh b/mouse_chattering.sh index 6ffcd3c..625c1d9 100644 --- a/mouse_chattering.sh +++ b/mouse_chattering.sh @@ -1,6 +1,22 @@ #!/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 Linux/Python, you may need to run: sudo pip3 install -r requirements.txt --break-system-packages) +# ============================================================================== +# MOUSE CHATTERING FIX - STARTUP SCRIPT +# ============================================================================== +# Change the `cd` path below to the absolute path of this project folder. +# Ensure this script is executable: chmod +x mouse_chattering.sh +# +# AVAILABLE ARGUMENTS: +# -m "usb-id-here" : The device ID of your mouse in /dev/input/by-id/ +# -t 30 : Button double-click threshold in ms (Default: 30) +# --buttons BTN_LEFT,BTN_SIDE : Explicitly filter only these buttons (Leave blank for all) +# +# ADVANCED SENSOR ARGUMENTS: +# -sr 150 : Block scroll wheel jumping in reverse direction (ms) +# -sd 30 : Block scroll wheel firing twice in same direction (ms) +# -jl 300 : Block massive cursor teleports exceeding X pixels per frame +# +# -r : Enable auto-reconnect infinite loop (Required for Cron/SysVinit) +# DO NOT USE `-r` if using Systemd! Systemd handles restarts natively. +# ============================================================================== -cd && sudo python3 -m src.mouse_main -m -t 30 +cd /absolute/path/to/project && sudo python3 -m src.mouse_main -m -t 30 From c7fb32fa61f1389fe1a989ba824364c1212ee32a Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:54:22 -0400 Subject: [PATCH 70/79] Update mouse_filtering.py --- src/mouse_filtering.py | 135 +++++++++++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 39 deletions(-) diff --git a/src/mouse_filtering.py b/src/mouse_filtering.py index 44d0bd0..37b3e37 100644 --- a/src/mouse_filtering.py +++ b/src/mouse_filtering.py @@ -3,61 +3,117 @@ 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 +def filter_mouse_chattering(evdev: libevdev.Device, default_threshold: int, scroll_rev_threshold: int, + scroll_interval: int, jump_limit: int, + buttons_to_filter: List[libevdev.EventCode], scroll_axes_to_filter: List[libevdev.EventCode], + btn_thresholds: dict, btn_map: dict) -> NoReturn: + + # Reset global states on reconnect to prevent ghost "stuck" buttons + global _last_btn_code + _last_btn_up.clear() + _btn_pressed.clear() + _last_btn_code = None + _last_scroll_time.clear() + _last_scroll_dir.clear() + + time.sleep(1) evdev.grab() ui_dev = evdev.create_uinput_device() - logging.info("Listening to mouse events...") - if not buttons_to_filter: - buttons_to_filter = [] - while True: 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: + processed_event = _from_event(e, default_threshold, scroll_rev_threshold, scroll_interval, jump_limit, + buttons_to_filter, scroll_axes_to_filter, btn_thresholds, btn_map) + if processed_event: + ui_dev.send_events([processed_event, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) + + except libevdev.EventsDroppedException: + # High-polling-rate gaming mice (1000Hz+) can overflow the kernel buffer. + # We catch the exception, resync the buffer, and process the recovered events natively. + logging.debug("Kernel buffer overflowed (high polling rate). Resyncing...") + for e in evdev.sync(): + processed_event = _from_event(e, default_threshold, scroll_rev_threshold, scroll_interval, jump_limit, + buttons_to_filter, scroll_axes_to_filter, btn_thresholds, btn_map) + if processed_event: + ui_dev.send_events([processed_event, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) + +def _from_event(event: libevdev.InputEvent, default_threshold: int, scroll_rev_threshold: int, scroll_interval: int, + jump_limit: int, buttons_to_filter: List[libevdev.EventCode], + scroll_axes_to_filter: List[libevdev.EventCode], btn_thresholds: dict, btn_map: dict): global _last_btn_code if event.matches(libevdev.EV_SYN) or event.matches(libevdev.EV_MSC): - return False + return None + + # === SCROLL & MOVEMENT (EV_REL) === + if event.matches(libevdev.EV_REL): + # 1. Jump filtering for Cursor X/Y + if event.code in (libevdev.EV_REL.REL_X, libevdev.EV_REL.REL_Y): + if jump_limit > 0 and abs(event.value) >= jump_limit: + logging.warning(f"BLOCKED TELEPORT: Cursor jumped {event.value} pixels in 1 frame!") + return None + return event - # 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 + # 2. Scroll Wheel Filtering + if event.code in (libevdev.EV_REL.REL_WHEEL, libevdev.EV_REL.REL_HWHEEL, + libevdev.EV_REL.REL_WHEEL_HI_RES, libevdev.EV_REL.REL_HWHEEL_HI_RES): + + if scroll_axes_to_filter and event.code not in scroll_axes_to_filter: + return event - # 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 + axis = event.code + now = event.sec * 1E6 + event.usec + direction = 1 if event.value > 0 else -1 + + last_time = _last_scroll_time.get(axis) + last_dir = _last_scroll_dir.get(axis) + + # Scroll Reverse Glitch (Faulty encoder bouncing backwards) + if last_dir != direction and last_time is not None: + if scroll_rev_threshold > 0 and (now - last_time) < scroll_rev_threshold * 1E3: + logging.info(f"BLOCKED REVERSE SCROLL: Encoder glitched backwards!") + return None + + # Scroll Double-Action (Worn encoder firing twice in the same direction) + if last_dir == direction and last_time is not None: + if scroll_interval > 0 and (now - last_time) < scroll_interval * 1E3: + logging.info(f"BLOCKED DOUBLE-SCROLL: Same direction too fast!") + return None + + _last_scroll_time[axis] = now + _last_scroll_dir[axis] = direction + return event + + return event + + # === BUTTON CLICKS (EV_KEY) === + # REMAPPING: Safely construct a brand new event object + if event.matches(libevdev.EV_KEY) and event.code.name in btn_map: + target_btn = btn_map[event.code.name] + event = libevdev.InputEvent(libevdev.evbit(target_btn), event.value, event.sec, event.usec) + if event.value == 1: + logging.debug(f'REMAPPED to {target_btn}') + + # Do not filter Native held clicks (value > 1) or Absolute movement (EV_ABS) + if event.matches(libevdev.EV_ABS) or not event.matches(libevdev.EV_KEY) or event.value > 1: + return event - # 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 + return event + + # Check for custom button threshold, fallback to default + threshold = btn_thresholds.get(event.code.name, default_threshold) - # Process Button Up (0) and Button Down (1) + # Process Button Up (0) if event.value == 0: if _btn_pressed[event.code]: _last_btn_up[event.code] = event.sec * 1E6 + event.usec _btn_pressed[event.code] = False - return True - else: - return False + return event + return None prev = _last_btn_up.get(event.code) now = event.sec * 1E6 + event.usec @@ -66,12 +122,13 @@ def _from_click(event: libevdev.InputEvent, threshold: int, buttons_to_filter: L 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 - return True + return event - logging.info(f'FILTERED {event.code} down: last up event {(now - prev) / 1E3} ms ago') - return False + logging.info(f'FILTERED {event.code.name} double-click!') + return None -# Global state trackers _last_btn_up: Dict[libevdev.EventCode, int] = {} _btn_pressed: DefaultDict[libevdev.EventCode, bool] = defaultdict(bool) _last_btn_code = None +_last_scroll_time: Dict[libevdev.EventCode, int] = {} +_last_scroll_dir: Dict[libevdev.EventCode, int] = {} From cfcc14000cd89c40c54e59753f9fde6a37919bdf Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:54:53 -0400 Subject: [PATCH 71/79] Update keyboard_filtering.py --- src/keyboard_filtering.py | 93 ++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/src/keyboard_filtering.py b/src/keyboard_filtering.py index dc23ec2..586774c 100644 --- a/src/keyboard_filtering.py +++ b/src/keyboard_filtering.py @@ -3,84 +3,85 @@ 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) - # to release natively before we grab the device. Prevents a "stuck" Enter key. - time.sleep(1) +def filter_chattering(evdev: libevdev.Device, default_threshold: int, keys_to_filter: List[libevdev.EventCode], + key_thresholds: dict, key_map: dict) -> NoReturn: - # Grab the physical device so only we see the events it emits + # Reset global states on reconnect to prevent ghost "stuck" keys + global _last_key_code + _last_key_up.clear() + _key_pressed.clear() + _last_key_code = None + + time.sleep(1) # Delay to allow Enter key to release natively after running script 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...") - if not keys_to_filter: - keys_to_filter = [] - while True: - # Descriptor is blocking; waits until physical events are available 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 - + # Process the event. _from_keystroke returns either the (modified) event, or None to drop it. + processed_event = _from_keystroke(e, default_threshold, keys_to_filter, key_thresholds, key_map) + if processed_event: + ui_dev.send_events([processed_event, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) + except libevdev.EventsDroppedException: + # If the user presses too many keys simultaneously (NKRO overflow), resync the buffer + logging.debug("Kernel buffer overflowed. Resyncing...") + for e in evdev.sync(): + processed_event = _from_keystroke(e, default_threshold, keys_to_filter, key_thresholds, key_map) + if processed_event: + ui_dev.send_events([processed_event, libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, 0)]) -def _from_keystroke(event: libevdev.InputEvent, threshold: int, keys_to_filter: List[libevdev.EventCode]) -> bool: +def _from_keystroke(event: libevdev.InputEvent, default_threshold: int, keys_to_filter: List[libevdev.EventCode], + key_thresholds: dict, key_map: dict): global _last_key_code - # Ignore sync/misc events. libevdev uinput handles syncing natively. + # Ignore sync/misc events natively if event.matches(libevdev.EV_SYN) or event.matches(libevdev.EV_MSC): - return False + return None - # 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. + # REMAPPING: Safely construct a brand new event object to prevent mutating C-bindings + if event.matches(libevdev.EV_KEY) and event.code.name in key_map: + target_key_name = key_map[event.code.name] + event = libevdev.InputEvent(libevdev.evbit(target_key_name), event.value, event.sec, event.usec) + if event.value == 1: # Only log on the key-down to prevent log spam + logging.debug(f'REMAPPED to {target_key_name}') + + # Do not filter modifier combinations or natively held keys (value > 1) if not event.matches(libevdev.EV_KEY) or event.value > 1: - logging.debug(f'FORWARDING {event.code}') - return True + return event - # TARGETED FILTERING: If the user provided specific keys to fix, and this isn't one of them, forward it. + # If an allowlist is provided and this key isn't in it, forward it without 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 + return event + + # PER-KEY THRESHOLDS: Check if this specific key has a custom delay, else use default. + threshold = key_thresholds.get(event.code.name, default_threshold) - # Process standard Key Up (0) and Key Down (1) events + # Process standard Key Up (0) events if event.value == 0: if _key_pressed[event.code]: - logging.debug(f'FORWARDING {event.code} up') + logging.debug(f'FORWARDING {event.code.name} up') _last_key_up[event.code] = event.sec * 1E6 + event.usec _key_pressed[event.code] = False - return True + return event else: - logging.info(f'FILTERING {event.code} up: key not pressed beforehand') - return False + return None prev = _last_key_up.get(event.code) now = event.sec * 1E6 + event.usec - # 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. + # 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') + logging.debug(f'FORWARDING {event.code.name} down') _key_pressed[event.code] = True _last_key_code = event.code - return True + return event - logging.info(f'FILTERED {event.code} down: last key up event happened {(now - prev) / 1E3} ms ago') - return False + logging.info(f'FILTERED {event.code.name} down: bounced within {threshold}ms') + return None -# Global state trackers _last_key_up: Dict[libevdev.EventCode, int] = {} _key_pressed: DefaultDict[libevdev.EventCode, bool] = defaultdict(bool) _last_key_code = None From f1c2103b6936815095f4c4632d4b750feeeec71e Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:55:31 -0400 Subject: [PATCH 72/79] Update mouse_retrieval.py --- src/mouse_retrieval.py | 47 ++++++++---------------------------------- 1 file changed, 9 insertions(+), 38 deletions(-) diff --git a/src/mouse_retrieval.py b/src/mouse_retrieval.py index 4b12dad..1167192 100644 --- a/src/mouse_retrieval.py +++ b/src/mouse_retrieval.py @@ -2,69 +2,40 @@ import os from typing import Final -# We use the 'by-id' folder because the device names here are persistent. +# Use 'by-id' because device names here are persistent across reboots/USB ports INPUT_DEVICES_PATH: Final = '/dev/input/by-id' - 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 + """Lists all valid input devices and prompts the user to select one.""" all_devices = os.listdir(INPUT_DEVICES_PATH) # 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) + # Advanced gaming mice often split their macro buttons into virtual + # keyboard endpoints. Showing all '-event-' nodes ensures you can find it. + valid_devices = [d for d in all_devices if '-event-' in d] + device_list = list(set(valid_devices)) + n_devices = len(device_list) - # 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 any devices in '{INPUT_DEVICES_PATH}'. Please provide it manually with -m.") - # 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): + for idx, device in enumerate(sorted(device_list), start=1): print(f"{idx}. {device}") selected_idx = -1 - # 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. - # 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) + sorted_devices = sorted(device_list) return sorted_devices[selected_idx - 1] - def abs_mouse_path(device: str) -> str: - """ - 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 874a5dda158afa2eb9caa27beb2ac136afff627d Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 17:56:07 -0400 Subject: [PATCH 73/79] Update keyboard_retrieval.py --- src/keyboard_retrieval.py | 49 ++++++++------------------------------- 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/src/keyboard_retrieval.py b/src/keyboard_retrieval.py index 7ac2052..99be65a 100644 --- a/src/keyboard_retrieval.py +++ b/src/keyboard_retrieval.py @@ -2,70 +2,41 @@ import os from typing import Final -# 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. +# Use 'by-id' because device names here are persistent across reboots/USB ports INPUT_DEVICES_PATH: Final = '/dev/input/by-id' - 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 + """Lists all valid input devices and prompts the user to select one.""" all_devices = os.listdir(INPUT_DEVICES_PATH) - # Deduplicate the list natively using a set to ensure clean output - keyboard_devices = list(set(all_devices)) - n_devices = len(keyboard_devices) + # Filter to ONLY show valid modern event nodes. + # 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) - # If no devices are found in the folder at all, abort the script if n_devices == 0: raise ValueError(f"Couldn't find any devices in '{INPUT_DEVICES_PATH}'") - # 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): + for idx, device in enumerate(sorted(device_list), start=1): print(f"{idx}. {device}") selected_idx = -1 - # 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. - # 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) + sorted_devices = sorted(device_list) return sorted_devices[selected_idx - 1] - def abs_keyboard_path(device: str) -> str: - """ - 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 66ec59334f7ca85c340f67cf2af75bc77b378850 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 18:05:16 -0400 Subject: [PATCH 74/79] Update keyboard_chattering.service --- keyboard_chattering.service | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/keyboard_chattering.service b/keyboard_chattering.service index 233eb6e..ab1d431 100644 --- a/keyboard_chattering.service +++ b/keyboard_chattering.service @@ -1,18 +1,19 @@ [Unit] Description=Keyboard Chattering Fix service -# Tell systemd to stop this service before going to sleep, and start it after waking up -After=suspend.target -After=hibernate.target -After=hybrid-sleep.target +# Ensures that if systemd restarts this during wake-up, the system is fully awake first. +After=sleep.target [Service] -# Wait 3 seconds before starting the script to give the USB bus and Wayland time to fully wake up +# Wait 3 seconds before starting the script to give the USB bus and Wayland/X11 time to fully wake up ExecStartPre=/bin/sleep 3 -# Change ExecStart to the absolute path of the file, executing keyboard_chattering.sh -ExecStart= +# Change ExecStart to the absolute path of your shell script +ExecStart=/absolute/path/to/keyboard_chattering.sh + +# Systemd will automatically restart the script if it crashes, or if it exits due to a USB sleep/disconnect Restart=always RestartSec=5 [Install] -WantedBy=multi-user.target suspend.target hibernate.target hybrid-sleep.target +# Start at normal boot. Systemd handles the restarts on wake automatically. +WantedBy=multi-user.target From 8c24ec897eec2b2bd5620ae9e5b7db1840235550 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 18:05:37 -0400 Subject: [PATCH 75/79] Update mouse_chattering.service --- mouse_chattering.service | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/mouse_chattering.service b/mouse_chattering.service index 289a44b..4c9b0ad 100644 --- a/mouse_chattering.service +++ b/mouse_chattering.service @@ -1,18 +1,19 @@ [Unit] -Description=Mouse Chattering service -# Tell systemd to stop this service before going to sleep, and start it after waking up -After=suspend.target -After=hibernate.target -After=hybrid-sleep.target +Description=Mouse Chattering Fix service +# Ensures that if systemd restarts this during wake-up, the system is fully awake first. +After=sleep.target [Service] -# Wait 3 seconds before starting the script to give the USB bus and Wayland time to fully wake up +# Wait 3 seconds before starting the script to give the USB bus and Wayland/X11 time to fully wake up ExecStartPre=/bin/sleep 3 -# Change ExecStart to the absolute path of the file, executing mouse_chattering.sh -ExecStart= +# Change ExecStart to the absolute path of your shell script +ExecStart=/absolute/path/to/mouse_chattering.sh + +# Systemd will automatically restart the script if it crashes, or if it exits due to a USB sleep/disconnect Restart=always RestartSec=5 [Install] -WantedBy=multi-user.target suspend.target hibernate.target hybrid-sleep.target +# Start at normal boot. Systemd handles the restarts on wake automatically. +WantedBy=multi-user.target From c9a6174f8c6a7edd87ab7cf28f479a8fc6b2c6bd Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Tue, 26 May 2026 18:10:33 -0400 Subject: [PATCH 76/79] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 8f5566d..2dd099e 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,13 @@ sudo systemctl restart mouse_chattering.service Because the Python scripts rely natively on the OS Kernel (`evdev` and `uinput`), the code works perfectly on non-systemd distributions and BSD variants. Ensure your `.sh` scripts are configured and executable (`chmod +x`), then use the guide below for your specific init system. +**IMPORTANT SUSPEND/SLEEP WARNING:** +Systemd natively restarts scripts when a PC wakes from sleep. Non-systemd init systems (Cron, Runit, OpenRC, SysVinit) **do not**. Therefore, if you use *any* of the methods below, you **MUST** append the `-r` (Auto-Reconnect) flag to the execution line inside your `.sh` scripts! +```bash +# Example: The -r flag ensures the script survives sleep/suspend cycles! +cd /path/to/folder && sudo python3 -m src.keyboard_main -k -t 30 -r +``` + > **💡 PRO-TIP FOR LOGGING:** Since non-systemd systems lack `journalctl`, you should modify your `.sh` scripts to redirect output so you can read the logs. Append a redirect to the execution lines in your `.sh` files: > ```bash > # For the keyboard script: From 57025555b6a1a6154917a25af8aea7ea49ad7088 Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Wed, 27 May 2026 11:13:25 -0400 Subject: [PATCH 77/79] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2dd099e..aab3f7c 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ sudo systemctl restart mouse_chattering.service Because the Python scripts rely natively on the OS Kernel (`evdev` and `uinput`), the code works perfectly on non-systemd distributions and BSD variants. Ensure your `.sh` scripts are configured and executable (`chmod +x`), then use the guide below for your specific init system. **IMPORTANT SUSPEND/SLEEP WARNING:** -Systemd natively restarts scripts when a PC wakes from sleep. Non-systemd init systems (Cron, Runit, OpenRC, SysVinit) **do not**. Therefore, if you use *any* of the methods below, you **MUST** append the `-r` (Auto-Reconnect) flag to the execution line inside your `.sh` scripts! +Systemd natively restarts scripts when a PC wakes from sleep. Non-systemd init systems **do not**. Therefore, if you use *any* of the methods below, you **MUST** append the `-r` (Auto-Reconnect) flag to the execution line inside your `.sh` scripts! ```bash # Example: The -r flag ensures the script survives sleep/suspend cycles! cd /path/to/folder && sudo python3 -m src.keyboard_main -k -t 30 -r From 1657ed48c2054dc34624522595768e12f29e99af Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Wed, 27 May 2026 11:17:55 -0400 Subject: [PATCH 78/79] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aab3f7c..715987b 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,7 @@ The easiest way to run the scripts on any system without Systemd is using `cron` ### OpenRC (Artix, Alpine, Gentoo) -OpenRC supports native respawning, so you do **not** need the `-r` flag. +Even though OpenRC supports native respawning, you **MUST** add the `-r` flag to your `.sh` scripts. This prevents the script from crash-looping when the USB bus briefly drops during sleep/wake cycles. 1. Create two files: `sudo nano /etc/init.d/keyboard_fix` and `sudo nano /etc/init.d/mouse_fix` 2. Paste the appropriate template below into each file: @@ -376,7 +376,7 @@ depend() { need localmount } ### Runit (Void Linux) -Runit also supports native respawning, so you do **not** need the `-r` flag. +Even though Runit supports native respawning, you **MUST** add the `-r` flag to your `.sh` scripts. This prevents the script from crash-looping when the USB bus briefly drops during sleep/wake cycles. 1. Create service directories: ```bash From 690e1289a3f5f663ffd84fbef58c2735d6bfd98d Mon Sep 17 00:00:00 2001 From: some1ataplace <99353321+some1ataplace@users.noreply.github.com> Date: Wed, 27 May 2026 11:21:49 -0400 Subject: [PATCH 79/79] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 715987b..98446bd 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,8 @@ cd /path/to/folder && sudo python3 -m src.keyboard_main -k -t 30 -r ### Cron (Universal Fallback) +Cron does not restart scripts natively. You **MUST** add the `-r` flag to your `.sh` scripts. + The easiest way to run the scripts on any system without Systemd is using `cron`'s `@reboot` directive. 1. Because `cron` does not auto-restart failed scripts, you **MUST** add the `-r` flag to your `.sh` scripts so they survive hardware disconnects! @@ -430,7 +432,7 @@ Even though Runit supports native respawning, you **MUST** add the `-r` flag to ### SysVinit (Devuan, Older Distros) -SysVinit does not restart scripts natively. You **MUST** add the `-r` flag to your `.sh` scripts (as shown in the Cron section). +SysVinit does not restart scripts natively. You **MUST** add the `-r` flag to your `.sh` scripts. Simply add the executable scripts to your `/etc/rc.local` file before the `exit 0` line: ```bash @@ -470,6 +472,8 @@ exit 0 ### FreeBSD / BSD Family +FreeBSD does not restart scripts natively. You **MUST** add the `-r` flag to your `.sh` scripts. + FreeBSD has native support for `evdev`, but you must load the modules and adjust device paths. **1. Load evdev modules:** Add these to `/boot/loader.conf` and reboot (or `kldload` them now):