Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 47 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ By filtering such anomalies, we can hopefully remove chatter without impeding ac

## 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 as a zip and extract the file. The dependencies are listed in the requirements.txt. And you can install it with the command below.

```shell
sudo pip3 install -r requirements.txt
Expand All @@ -57,6 +57,49 @@ sudo pip3 install -r requirements.txt
sudo python3 -m src
```

### Key Filtering Configuration

The filter has two operating modes:

#### Mode 1: Filter ALL keys (original default behavior)
By default, the filter works as it always has - applying the anti-chattering filter to all keyboard keys. This is the behavior when `FILTERED_KEYS` is set to `None` or empty in `src/constants.py`.

#### Mode 2: Filter only specific keys
You can configure the filter to apply only to specific keys that exhibit chattering. To do this:

1. Open `src/constants.py`
2. Uncomment and modify the `FILTERED_KEYS` variable with the desired keys
3. Use only the key name (e.g., `"KEY_A"`, `"KEY_SPACE"`) - no need for the `libevdev.EV_KEY.` prefix

**Configuration examples:**

```python
# Filter only specific keys
FILTERED_KEYS = {
"KEY_A", # A key with chattering
"KEY_SPACE", # Spacebar with chattering
"KEY_ENTER", # Enter key with chattering
}

# Filter all letters and numbers
FILTERED_KEYS = {
"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", "KEY_0", "KEY_1", "KEY_2", "KEY_3", "KEY_4", "KEY_5",
"KEY_6", "KEY_7", "KEY_8", "KEY_9", "KEY_SPACE", "KEY_ENTER",
}

# Return to original behavior (filter all keys)
FILTERED_KEYS = None
```

**Advantages of specific mode:**
- ✅ Better performance (processes only problematic keys)
- ✅ Preserves modifier keys (Ctrl, Alt, Shift)
- ✅ Doesn't interfere with function or special keys
- ✅ Flexible per-key configuration

### Customization Options

- -k KEYBOARD, --keyboard KEYBOARD
Expand Down Expand Up @@ -90,12 +133,12 @@ Then, copy the `chattering_fix.service` to `/etc/systemd/system/` and enable it
```shell
systemctl enable --now chattering_fix
```
You can check if the systemd unit file is properly working using
You can check if the systemd unit file is properly working using
```shell
systemctl status chattering_fix.service
```
You can also use
You can also use
```shell
journalctl -xeu chattering_fix.service
```
just to make sure that there are no errors.
just to make sure that there are no errors.
42 changes: 42 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Constants for keyboard chattering filter configuration.

DEFAULT BEHAVIOR:
- If FILTERED_KEYS is empty (None or empty list), ALL keys will be filtered (original behavior)
- If FILTERED_KEYS contains keys, only those specific keys will be filtered

To customize which keys should be filtered, define the FILTERED_KEYS list with key names.
Use only the key name (e.g., "KEY_A", "KEY_SPACE") instead of the full code.

Examples of key names:
- Letters: "KEY_A", "KEY_B", "KEY_C", etc.
- Numbers: "KEY_0", "KEY_1", "KEY_2", etc.
- Special: "KEY_SPACE", "KEY_ENTER", "KEY_BACKSPACE", "KEY_TAB"
- Function: "KEY_F1", "KEY_F2", etc.
- Modifiers: "KEY_LEFTSHIFT", "KEY_RIGHTSHIFT", "KEY_LEFTCTRL", etc.
- Arrows: "KEY_UP", "KEY_DOWN", "KEY_LEFT", "KEY_RIGHT"
"""

from typing import Optional, Set

# KEY FILTERING CONFIGURATION

FILTERED_KEYS = {
# Example: filter only some letters that have chattering
"KEY_P",
}

# Option 1: Filter ALL keys (original default behavior)
# Leave as None or empty list to maintain original behavior
# FILTERED_KEYS: Optional[Set[str]] = None

# Option 2: Filter only specific keys
# Uncomment and modify the list below to filter only specific keys
# FILTERED_KEYS = {
# # Example: filter only some letters that have chattering
# "KEY_A",
# "KEY_S",
# "KEY_D",
# "KEY_SPACE",
# "KEY_ENTER",
# }
43 changes: 41 additions & 2 deletions src/filtering.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,51 @@
import logging
from collections import defaultdict
from typing import DefaultDict, Dict, NoReturn
from typing import DefaultDict, Dict, NoReturn, Set

import libevdev

from src.constants import FILTERED_KEYS


def _get_filtered_keys_set() -> Set[libevdev.EventCode]:
"""
Converts the list of key names to libevdev codes.
If FILTERED_KEYS is None or empty, returns None (filter all keys).
"""
if not FILTERED_KEYS:
return None

filtered_codes = set()
for key_name in FILTERED_KEYS:
try:
# Converts "KEY_A" to libevdev.EV_KEY.KEY_A
key_code = getattr(libevdev.EV_KEY, key_name)
filtered_codes.add(key_code)
except AttributeError:
logging.warning(f"Key name '{key_name}' not found in libevdev.EV_KEY. Skipping...")

return filtered_codes


# Converts the key list once at initialization
_FILTERED_KEYS_CODES = _get_filtered_keys_set()


def filter_chattering(evdev: libevdev.Device, threshold: int) -> NoReturn:
# 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
ui_dev = evdev.create_uinput_device()

# Log filtering mode
if _FILTERED_KEYS_CODES is None:
logging.info("Filtering mode: ALL keys (original behavior)")
else:
logging.info(f"Filtering mode: SPECIFIC keys only ({len(_FILTERED_KEYS_CODES)} keys configured)")
if logging.getLogger().isEnabledFor(logging.DEBUG):
key_names = [name for name in FILTERED_KEYS]
logging.debug(f"Filtered keys: {', '.join(sorted(key_names))}")

logging.info("Listening to input events...")

while True:
Expand All @@ -30,6 +65,10 @@ def _from_keystroke(event: libevdev.InputEvent, threshold: int) -> bool:
logging.debug(f'FORWARDING {event.code}')
return True

if _FILTERED_KEYS_CODES is not None and event.code not in _FILTERED_KEYS_CODES:
logging.debug(f'FORWARDING {event.code} (not in filtered keys list)')
return True

# the values are 0 for up, 1 for down and 2 for hold
if event.value == 0:
if _key_pressed[event.code]:
Expand All @@ -55,4 +94,4 @@ def _from_keystroke(event: libevdev.InputEvent, threshold: int) -> bool:


_last_key_up: Dict[libevdev.EventCode, int] = {}
_key_pressed: DefaultDict[libevdev.EventCode, bool] = defaultdict(bool)
_key_pressed: DefaultDict[libevdev.EventCode, bool] = defaultdict(bool)