Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
3b3d99c
Create config.py
some1ataplace May 9, 2026
bff5377
Update filtering.py
some1ataplace May 9, 2026
423882a
Update __main__.py
some1ataplace May 9, 2026
f7ebe34
Update keyboard_retrieval.py
some1ataplace May 9, 2026
ebcdb71
Update chattering_fix.sh
some1ataplace May 9, 2026
109888d
Merge branch 'master' into Multiple-Fixes
some1ataplace May 9, 2026
e11d56a
Create mouse_filtering.py
some1ataplace May 9, 2026
6b501dc
Create mouse_retrieval.py
some1ataplace May 9, 2026
6d9dba2
Create mouse_main.py
some1ataplace May 9, 2026
2b41225
Create mouse_config.py
some1ataplace May 9, 2026
4843e07
Create mouse_fix.sh
some1ataplace May 9, 2026
4b34237
Delete src/mouse_fix.sh
some1ataplace May 9, 2026
b4b360b
Create mouse_chattering.sh
some1ataplace May 9, 2026
567f5fa
Create mouse_fix.service
some1ataplace May 9, 2026
bb6e45c
Rename chattering_fix.sh to keyboard_chattering.sh
some1ataplace May 9, 2026
558a871
Rename chattering_fix.service to keyboard_chattering.service
some1ataplace May 9, 2026
0811deb
Rename mouse_fix.service to mouse_chattering.service
some1ataplace May 9, 2026
c01a759
Rename config.py to keyboard_config.py
some1ataplace May 9, 2026
c845b70
Rename filtering.py to keyboard_filtering.py
some1ataplace May 9, 2026
65f321a
Update keyboard_filtering.py
some1ataplace May 9, 2026
179ca9c
Update keyboard_retrieval.py
some1ataplace May 9, 2026
f165665
Update and rename __main__.py to keyboard_main.py
some1ataplace May 9, 2026
f38bc08
Update mouse_filtering.py
some1ataplace May 9, 2026
6aeb242
Update mouse_retrieval.py
some1ataplace May 9, 2026
33c7e80
Update mouse_main.py
some1ataplace May 9, 2026
d413d3f
Update keyboard_chattering.sh
some1ataplace May 9, 2026
3d66341
Update mouse_chattering.sh
some1ataplace May 9, 2026
6071ce5
Update mouse_chattering.sh
some1ataplace May 9, 2026
32903fa
Update mouse_chattering.service
some1ataplace May 9, 2026
b107a6b
Update keyboard_chattering.service
some1ataplace May 9, 2026
193105b
Update README.md
some1ataplace May 9, 2026
d72fb35
Update README.md
some1ataplace May 9, 2026
4f11429
Update keyboard_chattering.sh
some1ataplace May 9, 2026
746c1e8
Update mouse_chattering.sh
some1ataplace May 9, 2026
566701d
Update keyboard_filtering.py
some1ataplace May 9, 2026
1bdf5c8
Update keyboard_main.py
some1ataplace May 9, 2026
02f5184
Update mouse_filtering.py
some1ataplace May 9, 2026
a930fb1
Update mouse_main.py
some1ataplace May 9, 2026
bc84357
Update keyboard_retrieval.py
some1ataplace May 9, 2026
9c39f93
Update mouse_retrieval.py
some1ataplace May 9, 2026
8850586
Update keyboard_retrieval.py
some1ataplace May 9, 2026
8c85f60
Update mouse_retrieval.py
some1ataplace May 9, 2026
eb3d9df
Update README.md
some1ataplace May 9, 2026
e6d8313
Update README.md
some1ataplace May 9, 2026
23ab86e
Update README.md
some1ataplace May 9, 2026
05a37b5
Update keyboard_retrieval.py
some1ataplace May 9, 2026
f983e5f
Update mouse_retrieval.py
some1ataplace May 9, 2026
e6a0707
Update mouse_retrieval.py
some1ataplace May 9, 2026
ec37dd6
Update keyboard_retrieval.py
some1ataplace May 9, 2026
90a59b2
Update keyboard_filtering.py
some1ataplace May 9, 2026
ca50a28
Update mouse_filtering.py
some1ataplace May 9, 2026
e2a6dc3
Update keyboard_filtering.py
some1ataplace May 9, 2026
b0907c4
Update mouse_filtering.py
some1ataplace May 9, 2026
7352b51
Update README.md
some1ataplace May 9, 2026
c194a64
Update README.md
some1ataplace May 9, 2026
90a358c
Update README.md
some1ataplace May 9, 2026
ed5d5a2
Update keyboard_chattering.service
some1ataplace May 20, 2026
ad424b2
Update mouse_chattering.service
some1ataplace May 20, 2026
1822851
Update README.md
some1ataplace May 20, 2026
bf94ecf
Update README.md
some1ataplace May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
306 changes: 250 additions & 56 deletions README.md

Large diffs are not rendered by default.

12 changes: 0 additions & 12 deletions chattering_fix.service

This file was deleted.

3 changes: 0 additions & 3 deletions chattering_fix.sh

This file was deleted.

21 changes: 21 additions & 0 deletions keyboard_chattering.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[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=<absolute/path/to/file/keyboard_chattering.sh>

Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
WantedBy=suspend.target
WantedBy=hibernate.target
WantedBy=hybrid-sleep.target
6 changes: 6 additions & 0 deletions keyboard_chattering.sh
Original file line number Diff line number Diff line change
@@ -0,0 +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 Linux/Python, you may need to run: sudo pip3 install -r requirements.txt --break-system-packages)

cd </absolute/path/to/folder> && sudo python3 -m src.keyboard_main -k <KEYBOARD id> -t 30
21 changes: 21 additions & 0 deletions mouse_chattering.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[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=<absolute/path/to/file/mouse_chattering.sh>

Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
WantedBy=suspend.target
WantedBy=hibernate.target
WantedBy=hybrid-sleep.target
6 changes: 6 additions & 0 deletions mouse_chattering.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/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)

cd </absolute/path/to/folder> && sudo python3 -m src.mouse_main -m <MOUSE id> -t 30
50 changes: 0 additions & 50 deletions src/__main__.py

This file was deleted.

60 changes: 0 additions & 60 deletions src/filtering.py

This file was deleted.

43 changes: 43 additions & 0 deletions src/keyboard_config.py
Original file line number Diff line number Diff line change
@@ -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
86 changes: 86 additions & 0 deletions src/keyboard_filtering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import logging
from collections import defaultdict
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)

# 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...")

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


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

# 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

# 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')
_last_key_up[event.code] = event.sec * 1E6 + event.usec
_key_pressed[event.code] = False
return True
else:
logging.info(f'FILTERING {event.code} up: key not pressed beforehand')
return False

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.
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')
return False

# Global state trackers
_last_key_up: Dict[libevdev.EventCode, int] = {}
_key_pressed: DefaultDict[libevdev.EventCode, bool] = defaultdict(bool)
_last_key_code = None
64 changes: 64 additions & 0 deletions src/keyboard_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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

# Safely import the config file if it exists
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)

# 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()

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(',')]

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")

# 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))
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)
Loading