keyclean 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- keyclean-0.9.0/.claude/settings.local.json +13 -0
- keyclean-0.9.0/.editorconfig +27 -0
- keyclean-0.9.0/.gitignore +31 -0
- keyclean-0.9.0/CHANGELOG.md +139 -0
- keyclean-0.9.0/LICENSE +24 -0
- keyclean-0.9.0/Makefile +182 -0
- keyclean-0.9.0/PKG-INFO +141 -0
- keyclean-0.9.0/PLAN.md +236 -0
- keyclean-0.9.0/PROMPT.md +112 -0
- keyclean-0.9.0/README.md +104 -0
- keyclean-0.9.0/Screenshot_20260322_051649.png +0 -0
- keyclean-0.9.0/Screenshot_20260322_053149.png +0 -0
- keyclean-0.9.0/Screenshot_20260322_054529.png +0 -0
- keyclean-0.9.0/Screenshot_20260322_055850.png +0 -0
- keyclean-0.9.0/pyproject.toml +103 -0
- keyclean-0.9.0/src/keyclean/__init__.py +5 -0
- keyclean-0.9.0/src/keyclean/__main__.py +6 -0
- keyclean-0.9.0/src/keyclean/app.py +139 -0
- keyclean-0.9.0/src/keyclean/config.py +78 -0
- keyclean-0.9.0/src/keyclean/input_grabber/__init__.py +84 -0
- keyclean-0.9.0/src/keyclean/input_grabber/_base.py +37 -0
- keyclean-0.9.0/src/keyclean/input_grabber/_fallback.py +31 -0
- keyclean-0.9.0/src/keyclean/input_grabber/_pynput.py +62 -0
- keyclean-0.9.0/src/keyclean/input_grabber/_wayland.py +166 -0
- keyclean-0.9.0/src/keyclean/input_grabber/_x11.py +76 -0
- keyclean-0.9.0/src/keyclean/keyboard_layout.py +236 -0
- keyclean-0.9.0/src/keyclean/renderer.py +246 -0
- keyclean-0.9.0/src/keyclean/safety_sequence.py +31 -0
- keyclean-0.9.0/tests/conftest.py +12 -0
- keyclean-0.9.0/tests/test_app.py +85 -0
- keyclean-0.9.0/tests/test_input_grabber.py +187 -0
- keyclean-0.9.0/tests/test_keyboard_layout.py +81 -0
- keyclean-0.9.0/tests/test_renderer.py +80 -0
- keyclean-0.9.0/tests/test_safety_sequence.py +69 -0
- keyclean-0.9.0/tox.ini +11 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(python -c \"import pygame\")",
|
|
5
|
+
"Bash(SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy python -m pytest tests/ -v 2>&1)",
|
|
6
|
+
"Bash(PYTHONPATH=src pylint src/keyclean 2>&1)",
|
|
7
|
+
"Bash(SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy python -m pytest tests/test_input_grabber.py -v 2>&1)",
|
|
8
|
+
"Bash(python -m pytest tests/test_input_grabber.py -v)",
|
|
9
|
+
"Bash(pylint src/keyclean)",
|
|
10
|
+
"Bash(make:*)"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# EditorConfig is awesome: https://EditorConfig.org
|
|
2
|
+
|
|
3
|
+
# top-most EditorConfig file
|
|
4
|
+
root = true
|
|
5
|
+
|
|
6
|
+
[*]
|
|
7
|
+
charset = utf-8
|
|
8
|
+
end_of_line = lf
|
|
9
|
+
# The POSIX standard requires the last line to end with a new line character.
|
|
10
|
+
# All UNIX tools expect a new line at the end of files. Most text editors use this convention too.
|
|
11
|
+
insert_final_newline = true
|
|
12
|
+
trim_trailing_whitespace = true
|
|
13
|
+
max_line_length = 120
|
|
14
|
+
|
|
15
|
+
# Matches multiple files with brace expansion notation
|
|
16
|
+
# Set default charset and 4 space indentation
|
|
17
|
+
[*.{py,txt,md,rst,c,cxx,cpp,h,hpp,hxx,sh,cfg,ini}]
|
|
18
|
+
indent_style = space
|
|
19
|
+
indent_size = 4
|
|
20
|
+
|
|
21
|
+
[*.{js,json,html,htm,xml,yaml,yml}]
|
|
22
|
+
indent_style = space
|
|
23
|
+
indent_size = 2
|
|
24
|
+
|
|
25
|
+
# Tab indentation (no size specified)
|
|
26
|
+
[{Makefile,*.go}]
|
|
27
|
+
indent_style = tab
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/dist/
|
|
2
|
+
/build/
|
|
3
|
+
/htmlcov/
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*.egg-info/
|
|
7
|
+
*.egg
|
|
8
|
+
.eggs/
|
|
9
|
+
.tox/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
*.py,cover
|
|
13
|
+
site.py
|
|
14
|
+
__pypackages__/
|
|
15
|
+
|
|
16
|
+
# uv
|
|
17
|
+
/.venv/
|
|
18
|
+
/uv.lock
|
|
19
|
+
|
|
20
|
+
# Local wheels cache
|
|
21
|
+
/whl_local/
|
|
22
|
+
|
|
23
|
+
# Packaging
|
|
24
|
+
/*.spec
|
|
25
|
+
/VERSION
|
|
26
|
+
|
|
27
|
+
# Editor
|
|
28
|
+
/.idea/
|
|
29
|
+
/.vscode/
|
|
30
|
+
*.swp
|
|
31
|
+
*~
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
## 0.9.0 — 2026-03-22
|
|
2
|
+
|
|
3
|
+
### Changes since beginning
|
|
4
|
+
|
|
5
|
+
- f0a2b04 Add uv run keyclean to README usage examples
|
|
6
|
+
- 785119a Add app header above clock; make milliseconds optional
|
|
7
|
+
- 2e9f934 Show /dev/input hint in UI below exit-phrase help text
|
|
8
|
+
- b09c869 Add make release target
|
|
9
|
+
- d42cc72 Fix Enter key overlapping Del — shorten #~ and backslash, shift Enter left
|
|
10
|
+
- d71cb0d Fix Wayland Super key leak — explicit zwp_keyboard_shortcuts_inhibit_manager_v1
|
|
11
|
+
- 0c4fb12 Add Development section to README, fix uv dep groups, add py314 to tox
|
|
12
|
+
- 0703c7b Silence pygame setuptools related depecation warnings.
|
|
13
|
+
- bdd632d Initial implementation of KeyClean 0.1.0
|
|
14
|
+
- 47251bc Minor plan adjustment, .gitignore.
|
|
15
|
+
- 0154a64 Start.
|
|
16
|
+
|
|
17
|
+
# Changelog
|
|
18
|
+
|
|
19
|
+
## 0.1.0 — 2026-03-22
|
|
20
|
+
|
|
21
|
+
Initial implementation.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `pyproject.toml` — build config using `hatchling`, `pygame-ce` as core dependency,
|
|
26
|
+
`pynput` and `python-xlib` as optional extras, full dev toolchain
|
|
27
|
+
(`autopep8`, `pylint`, `pytest`, `pytest-mock`, `pytest-cov`, `tox`, `twine`, `build`).
|
|
28
|
+
- `tox.ini` — cross-version test matrix for Python 3.9–3.13.
|
|
29
|
+
- `README.md` — installation, usage, exit methods, platform notes.
|
|
30
|
+
- `LICENSE` — Unlicense.
|
|
31
|
+
- `.gitignore` — Python, uv, build artifact, and editor ignores.
|
|
32
|
+
|
|
33
|
+
#### `src/keyclean/`
|
|
34
|
+
|
|
35
|
+
- `__init__.py` — version (`0.1.0`), author, license metadata.
|
|
36
|
+
- `__main__.py` — `python -m keyclean` entry point.
|
|
37
|
+
- `config.py` — all constants: colors, key sizes, FPS (60), exit phrase
|
|
38
|
+
(`"keys are clean"`), datetime format, Done button geometry.
|
|
39
|
+
- `keyboard_layout.py` — full ISO 105-key layout: rows 0–5 (Esc/F-keys, number row,
|
|
40
|
+
QWERTY, home row, bottom row with ISO extra key, space bar row), navigation cluster
|
|
41
|
+
(Insert/Delete/Home/End/PgUp/PgDn/arrows), numpad (17 keys including tall Enter and
|
|
42
|
+
Plus). `PYGAME_KEY_MAP` dict for O(1) keycode lookup.
|
|
43
|
+
- `renderer.py` — `Renderer` class: auto-scales layout to screen resolution;
|
|
44
|
+
draws datetime top bar (`YYYY-MM-DD HH:MM:SS.mmm ±HHMM TZ`), ISO keyboard in the
|
|
45
|
+
middle with pressed-key highlighting, keystroke counter (bottom-left), help text
|
|
46
|
+
(bottom-center), Done button (bottom-right, hover highlight), warning banner.
|
|
47
|
+
- `safety_sequence.py` — `SafetySequence`: `collections.deque` ring buffer,
|
|
48
|
+
case-insensitive match against the exit phrase; `feed(char) -> bool`.
|
|
49
|
+
- `app.py` — `App` class: fullscreen pygame init, 60 FPS loop that drains the full
|
|
50
|
+
SDL event queue each frame, tracks `pressed_keys: set[int]` and `strike_count: int`,
|
|
51
|
+
feeds printable characters to `SafetySequence`, handles Done button mouse click.
|
|
52
|
+
|
|
53
|
+
#### `src/keyclean/input_grabber/`
|
|
54
|
+
|
|
55
|
+
- `_base.py` — `AbstractGrabber` ABC with `grab()`, `release()`, context manager.
|
|
56
|
+
- `_fallback.py` — No-op grabber; sets `is_fallback=True` to trigger warning banner.
|
|
57
|
+
- `_wayland.py` — Best-effort Wayland grabber; relies on SDL2
|
|
58
|
+
`zwp_keyboard_shortcuts_inhibit_manager_v1`; warns if SDL2 < 2.28.
|
|
59
|
+
- `_x11.py` — Full keyboard grab via `XGrabKeyboard` (python-xlib); logs window ID.
|
|
60
|
+
- `_pynput.py` — Global suppression via `pynput.keyboard.Listener(suppress=True)`;
|
|
61
|
+
used on macOS (CGEventTap) and Windows (LowLevelHook).
|
|
62
|
+
- `__init__.py` — `get_grabber()` factory: dispatches on `sys.platform` and
|
|
63
|
+
`$XDG_SESSION_TYPE`; gracefully falls back through X11 → pynput → fallback.
|
|
64
|
+
|
|
65
|
+
#### `tests/`
|
|
66
|
+
|
|
67
|
+
- `conftest.py` — shared fixtures.
|
|
68
|
+
- `test_safety_sequence.py` — 9 tests: exact phrase, case-insensitive, partial,
|
|
69
|
+
wrong chars, sliding ring buffer, reset, edge cases.
|
|
70
|
+
- `test_keyboard_layout.py` — 12 tests: key count (~105), unique IDs, unique pygame
|
|
71
|
+
keycodes, map consistency, no zero-size keys, all labels present, ISO extra key,
|
|
72
|
+
numpad completeness, F1–F12, arrows, row/col bounds.
|
|
73
|
+
- `test_renderer.py` — 8 tests: instantiation, Done rect returned and within screen,
|
|
74
|
+
pressed keys, warning banner, counter values, small and large resolutions.
|
|
75
|
+
- `test_input_grabber.py` — 11 tests: FallbackGrabber lifecycle, context manager,
|
|
76
|
+
WaylandGrabber SDL version warning, factory dispatch for unknown/Wayland/X11/macOS/Windows.
|
|
77
|
+
- `test_app.py` — 9 tests: `_key_to_char` filtering, exit phrase detection,
|
|
78
|
+
counter initial state.
|
|
79
|
+
|
|
80
|
+
#### `Makefile`
|
|
81
|
+
|
|
82
|
+
- Updated `test_upload` and `upload` targets from `termcolor_dg-*` to `keyclean-*`.
|
|
83
|
+
|
|
84
|
+
### Result
|
|
85
|
+
|
|
86
|
+
- **52/52 tests passing.**
|
|
87
|
+
- **pylint 10.00/10.**
|
|
88
|
+
|
|
89
|
+
● All done. Here's the final state:
|
|
90
|
+
|
|
91
|
+
52/52 tests passing • pylint 10.00/10
|
|
92
|
+
|
|
93
|
+
Files created:
|
|
94
|
+
|
|
95
|
+
┌─────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────┐
|
|
96
|
+
│ File │ Purpose │
|
|
97
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
98
|
+
│ .gitignore │ Updated with full Python/uv/build ignores │
|
|
99
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
100
|
+
│ LICENSE │ Already existed (Unlicense) │
|
|
101
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
102
|
+
│ README.md │ Installation, usage, platform notes │
|
|
103
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
104
|
+
│ tox.ini │ Python 3.9–3.13 test matrix │
|
|
105
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
106
|
+
│ pyproject.toml │ Build config, deps, pylint/pytest/autopep8 settings │
|
|
107
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
108
|
+
│ src/keyclean/__init__.py │ Version, author, license metadata │
|
|
109
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
110
|
+
│ src/keyclean/__main__.py │ python -m keyclean entry point │
|
|
111
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
112
|
+
│ src/keyclean/config.py │ All constants: colors, sizes, FPS, exit phrase, datetime format │
|
|
113
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
114
|
+
│ src/keyclean/keyboard_layout.py │ Full ISO 105-key definitions (rows 0–5 + nav cluster + numpad) │
|
|
115
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
116
|
+
│ src/keyclean/renderer.py │ Datetime top bar, keyboard, counter, help text, Done button, warning banner │
|
|
117
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
118
|
+
│ src/keyclean/safety_sequence.py │ Ring buffer matching "keys are clean" case-insensitively │
|
|
119
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
120
|
+
│ src/keyclean/app.py │ Main loop, event drain, key state, exit handling │
|
|
121
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
122
|
+
│ src/keyclean/input_grabber/__init__.py │ Platform-dispatch factory │
|
|
123
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
124
|
+
│ src/keyclean/input_grabber/_base.py │ AbstractGrabber ABC │
|
|
125
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
126
|
+
│ src/keyclean/input_grabber/_fallback.py │ Pygame-only no-op with warning │
|
|
127
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
128
|
+
│ src/keyclean/input_grabber/_wayland.py │ Best-effort (SDL2 shortcuts_inhibit) │
|
|
129
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
130
|
+
│ src/keyclean/input_grabber/_x11.py │ XGrabKeyboard via python-xlib │
|
|
131
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
132
|
+
│ src/keyclean/input_grabber/_pynput.py │ pynput suppress=True (macOS/Windows) │
|
|
133
|
+
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
|
|
134
|
+
│ tests/ │ 52 tests across all modules │
|
|
135
|
+
└─────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────┘
|
|
136
|
+
|
|
137
|
+
The warnings in the test output are all from the system's pkg_resources (pygame's dependency) — not from our code.
|
|
138
|
+
|
|
139
|
+
✻ Sautéed for 18m 23s
|
keyclean-0.9.0/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
keyclean-0.9.0/Makefile
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
SHELL:=/usr/bin/env bash # Use bash syntax, mitigates dash's printf on Debian
|
|
2
|
+
export TOP:=$(shell dirname "$(abspath $(lastword $(MAKEFILE_LIST)))")
|
|
3
|
+
name:=$(shell basename "$(TOP)")
|
|
4
|
+
export PIP_FIND_LINKS:=$(abspath $(TOP)/whl_local/)
|
|
5
|
+
export PYTHONPATH:=$(TOP)/src
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
.PHONY: help
|
|
9
|
+
help:
|
|
10
|
+
@echo
|
|
11
|
+
@echo "▍Help"
|
|
12
|
+
@echo "▀▀▀▀▀▀"
|
|
13
|
+
@echo
|
|
14
|
+
@echo "Available targets:"
|
|
15
|
+
@echo " check: run checks"
|
|
16
|
+
@echo " test: run all tests"
|
|
17
|
+
@echo " coverage: run all tests and collect code coverage"
|
|
18
|
+
@echo " lint: run linters"
|
|
19
|
+
@echo
|
|
20
|
+
@echo " pep8format: auto-format code to PEP8 standards"
|
|
21
|
+
@echo
|
|
22
|
+
@echo " build: build the source and whl package, look for */dist/*.whl"
|
|
23
|
+
@echo
|
|
24
|
+
@echo " release: tag a new release (required: V=X.Y.Z)"
|
|
25
|
+
@echo " updates version in pyproject.toml + __init__.py,"
|
|
26
|
+
@echo " prepends git log to CHANGELOG.md, commits, tags."
|
|
27
|
+
@echo " Example: make V=0.2.0 release"
|
|
28
|
+
@echo
|
|
29
|
+
@echo " clean: clean the build tree"
|
|
30
|
+
@echo
|
|
31
|
+
@echo "name='$(name)'"
|
|
32
|
+
@echo "PYTHONPATH = '$(PYTHONPATH)'"
|
|
33
|
+
@echo "PIP_FIND_LINKS = '$(PIP_FIND_LINKS)'"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
.PHONY: check
|
|
37
|
+
check: lint
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
.PHONY: test
|
|
41
|
+
test:
|
|
42
|
+
pytest -v
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
.PHONY: coverage
|
|
46
|
+
coverage:
|
|
47
|
+
pytest --cov=.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
.PHONY: lint
|
|
51
|
+
lint:
|
|
52
|
+
pylint "src/$(name)"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
.PHONY: pep8format
|
|
56
|
+
pep8format:
|
|
57
|
+
autopep8 --in-place --recursive "src/$(name)"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
.PHONY: build
|
|
61
|
+
build:
|
|
62
|
+
python3 -m build
|
|
63
|
+
mkdir -p "$(PIP_FIND_LINKS)/"
|
|
64
|
+
cp dist/*.whl "$(PIP_FIND_LINKS)/"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
.PHONY: userinstall
|
|
68
|
+
userinstall: build
|
|
69
|
+
python3 -m pip install $(PIP_USER) ./dist/*.whl
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
.PHONY: uninstall
|
|
73
|
+
uninstall:
|
|
74
|
+
python3 -m pip uninstall -y "$(name)"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
.PHONY: useruninstall
|
|
78
|
+
useruninstall: uninstall
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
.PHONY: rpmprep
|
|
82
|
+
rpmprep:
|
|
83
|
+
cp "rpm/$(name).spec.in" "$(name).spec"
|
|
84
|
+
sed -i 's|^Release:.*|Release: $(RPM_REV)%{?dist}|g' "$(name).spec"
|
|
85
|
+
sed -i 's|^Version:.*|Version: $(RPM_VER)|g' "$(name).spec"
|
|
86
|
+
rm -rf ~/rpmbuild/RPMS/noarch/"$(name)"*.rpm
|
|
87
|
+
rm -rf ~/rpmbuild/SRPMS/"$(name)"*.src.rpm
|
|
88
|
+
python3 setup.py sdist
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
.PHONY: rpm
|
|
92
|
+
rpm: rpmprep
|
|
93
|
+
rpmbuild -ba "$(name).spec" --define "_sourcedir $$PWD/dist"
|
|
94
|
+
rm "$(name).spec"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
.PHONY: srpm
|
|
98
|
+
srpm: rpmprep
|
|
99
|
+
rpmbuild -bs "$(name).spec" --define "_sourcedir $$PWD/dist"
|
|
100
|
+
rm "$(name).spec"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Python script that prepends a new section to CHANGELOG.md.
|
|
104
|
+
# Exported as an env var so the recipe can pipe it to python3 via stdin
|
|
105
|
+
# without heredoc/quoting nightmares.
|
|
106
|
+
define _release_py
|
|
107
|
+
import subprocess, datetime, pathlib, sys
|
|
108
|
+
v, top = sys.argv[1], sys.argv[2]
|
|
109
|
+
cl = pathlib.Path(top, "CHANGELOG.md")
|
|
110
|
+
tags = subprocess.check_output(
|
|
111
|
+
["git", "-C", top, "tag", "--sort=-version:refname"],
|
|
112
|
+
text=True
|
|
113
|
+
).strip().splitlines()
|
|
114
|
+
tags = [t for t in tags if t]
|
|
115
|
+
last_tag = tags[0] if tags else None
|
|
116
|
+
log_range = [last_tag + "..HEAD"] if last_tag else []
|
|
117
|
+
commits = subprocess.check_output(
|
|
118
|
+
["git", "-C", top, "log"] + log_range +
|
|
119
|
+
["--oneline", "--no-decorate", "--no-merges"],
|
|
120
|
+
text=True
|
|
121
|
+
).strip()
|
|
122
|
+
date = datetime.date.today().isoformat()
|
|
123
|
+
since = last_tag if last_tag else "beginning"
|
|
124
|
+
section = "## " + v + " \u2014 " + date + "\n\n"
|
|
125
|
+
section += "### Changes since " + since + "\n\n"
|
|
126
|
+
if commits:
|
|
127
|
+
section += "\n".join("- " + c for c in commits.splitlines()) + "\n"
|
|
128
|
+
section += "\n"
|
|
129
|
+
existing = cl.read_text()
|
|
130
|
+
cl.write_text(section + existing)
|
|
131
|
+
n = len(commits.splitlines()) if commits else 0
|
|
132
|
+
print("Updated CHANGELOG.md (" + str(n) + " commit" + ("s" if n != 1 else "") + ")")
|
|
133
|
+
endef
|
|
134
|
+
export _release_py
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
.PHONY: release
|
|
138
|
+
release:
|
|
139
|
+
@[ -n "$(V)" ] || { echo "Error: V is not set. Usage: make V=X.Y.Z release"; exit 1; }
|
|
140
|
+
@printf '%s' "$(V)" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$$' || \
|
|
141
|
+
{ echo "Error: '$(V)' is not a valid version (expected X.Y.Z)"; exit 1; }
|
|
142
|
+
@git -C "$(TOP)" diff --quiet HEAD 2>/dev/null || \
|
|
143
|
+
{ echo "Error: uncommitted changes — commit or stash first"; exit 1; }
|
|
144
|
+
@git -C "$(TOP)" tag | grep -qxF "v$(V)" && \
|
|
145
|
+
{ echo "Error: tag v$(V) already exists"; exit 1; } || true
|
|
146
|
+
@echo "▶ Bumping version to $(V) ..."
|
|
147
|
+
sed -i 's/^version = ".*"/version = "$(V)"/' "$(TOP)/pyproject.toml"
|
|
148
|
+
sed -i 's/^__version__ = ".*"/__version__ = "$(V)"/' \
|
|
149
|
+
"$(TOP)/src/$(name)/__init__.py"
|
|
150
|
+
@echo "▶ Updating CHANGELOG.md ..."
|
|
151
|
+
printf '%s\n' "$$_release_py" | python3 - "$(V)" "$(TOP)"
|
|
152
|
+
@echo "▶ Committing and tagging v$(V) ..."
|
|
153
|
+
git -C "$(TOP)" add pyproject.toml "src/$(name)/__init__.py" CHANGELOG.md
|
|
154
|
+
git -C "$(TOP)" commit -m "Release $(V)"
|
|
155
|
+
git -C "$(TOP)" tag -a "v$(V)" -m "Version $(V)"
|
|
156
|
+
@echo
|
|
157
|
+
@echo "✓ Released v$(V). Push with:"
|
|
158
|
+
@echo " git push && git push --tags"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
.PHONY: clean
|
|
162
|
+
clean:
|
|
163
|
+
-python3 -m coverage erase
|
|
164
|
+
-python3 -m pip uninstall -y "$(name)"
|
|
165
|
+
find . -depth \( -name '*.pyc' -o -name '__pycache__' -o -name '__pypackages__' \
|
|
166
|
+
-o -name '*.pyc' -o -name '*.pyd' -o -name '*.pyo' -o -name '*.egg-info' \
|
|
167
|
+
-o -name '*.py,cover' \) -not -path "./.?*/*" \
|
|
168
|
+
-exec rm -rf \{\} \;
|
|
169
|
+
rm -rf site.py build/ dist/ "$(name).spec" VERSION bin/ .tox/ .pytest_cache/
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# https://packaging.python.org/en/latest/guides/using-testpypi/
|
|
173
|
+
# Upload to https://test.pypi.org/
|
|
174
|
+
.PHONY: test_upload
|
|
175
|
+
test_upload: build
|
|
176
|
+
twine upload --repository testpypi dist/keyclean-*.whl dist/keyclean-*.tar.gz
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Upload to https://pypi.org/
|
|
180
|
+
.PHONY: upload
|
|
181
|
+
upload: build
|
|
182
|
+
twine upload dist/keyclean-*.whl dist/keyclean-*.tar.gz
|
keyclean-0.9.0/PKG-INFO
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keyclean
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Cross-platform keyboard cleaning utility — lock your keyboard, wipe it clean.
|
|
5
|
+
Project-URL: Homepage, https://github.com/gunchev/keyclean
|
|
6
|
+
Project-URL: Repository, https://github.com/gunchev/keyclean
|
|
7
|
+
Author: Doncho Nikolaev Gunchev
|
|
8
|
+
License-Expression: Unlicense
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cleaning,keyboard,pygame,utility
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: MacOS X
|
|
13
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
14
|
+
Classifier: Environment :: X11 Applications
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: pygame-ce>=2.4.0
|
|
28
|
+
Provides-Extra: grab
|
|
29
|
+
Requires-Dist: pynput>=1.7.6; extra == 'grab'
|
|
30
|
+
Provides-Extra: linux
|
|
31
|
+
Requires-Dist: python-xlib>=0.33; extra == 'linux'
|
|
32
|
+
Requires-Dist: pywayland>=0.4.0; extra == 'linux'
|
|
33
|
+
Provides-Extra: macos
|
|
34
|
+
Requires-Dist: pyobjc-framework-quartz>=9.0; extra == 'macos'
|
|
35
|
+
Provides-Extra: windows
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# KeyClean
|
|
39
|
+
|
|
40
|
+
Cross-platform keyboard cleaning utility. Locks the keyboard, visualizes keypresses on a
|
|
41
|
+
full ISO 105-key layout, and suppresses input to the OS — so you can wipe your physical
|
|
42
|
+
keyboard without triggering commands.
|
|
43
|
+
|
|
44
|
+
> Vibe-coded using [Claude Sonnet 4.6](https://www.anthropic.com/claude).
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install keyclean
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Platform extras for stronger input suppression:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install "keyclean[grab]" # pynput (macOS / Windows)
|
|
56
|
+
pip install "keyclean[grab,linux]" # pynput + python-xlib (Linux X11)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
keyclean
|
|
63
|
+
# or
|
|
64
|
+
python -m keyclean
|
|
65
|
+
# or from the dev tree
|
|
66
|
+
uv run keyclean
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The application launches fullscreen. All keypresses are shown on the virtual keyboard and
|
|
70
|
+
counted but have no effect on the OS.
|
|
71
|
+
|
|
72
|
+
### Exit
|
|
73
|
+
|
|
74
|
+
- Type **`keys are clean`** on the physical keyboard, or
|
|
75
|
+
- Click the **Done** button with the mouse.
|
|
76
|
+
|
|
77
|
+
## Platform Notes
|
|
78
|
+
|
|
79
|
+
| Platform | Suppression method | Notes |
|
|
80
|
+
|-----------------|--------------------------|--------------------------------------------|
|
|
81
|
+
| Linux (X11) | `XGrabKeyboard` | Full suppression while window is focused |
|
|
82
|
+
| Linux (Wayland) | SDL2 `shortcuts_inhibit` | Best-effort; Ctrl+Alt+F* cannot be blocked |
|
|
83
|
+
| macOS | `pynput` / CGEventTap | Requires Accessibility permission |
|
|
84
|
+
| Windows | `pynput` / LowLevelHook | Ctrl+Alt+Del cannot be blocked (by design) |
|
|
85
|
+
|
|
86
|
+
Without the optional extras the app falls back to pygame-only mode (fullscreen), which prevents
|
|
87
|
+
most accidental input but a warning banner is displayed.
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
Clone and set up the dev environment with [uv](https://docs.astral.sh/uv/):
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git clone https://github.com/gunchev/keyclean
|
|
95
|
+
cd keyclean
|
|
96
|
+
uv sync --group dev # core dev tools + pynput
|
|
97
|
+
uv sync --group dev --extra linux # also install python-xlib (Linux X11)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Run the test suite:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uv run pytest
|
|
104
|
+
# or via make
|
|
105
|
+
make test
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Lint and format:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
make lint # pylint
|
|
113
|
+
make pep8format # autopep8
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Build a wheel:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
make build
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Publish to PyPI / TestPyPI:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
make upload # PyPI
|
|
126
|
+
make test_upload # TestPyPI
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Dependency groups:
|
|
130
|
+
|
|
131
|
+
| Command | Installs |
|
|
132
|
+
|-------------------------|--------------------------------------------------------------|
|
|
133
|
+
| `uv sync --group dev` | dev toolchain (pytest, pylint, autopep8, tox, twine, pynput) |
|
|
134
|
+
| `uv sync --extra grab` | `pynput` (runtime, macOS/Windows suppression) |
|
|
135
|
+
| `uv sync --extra linux` | `python-xlib` (runtime, Linux X11 suppression) |
|
|
136
|
+
| `uv sync --extra macos` | `pyobjc-framework-Quartz` (runtime, macOS) |
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
This is free and unencumbered software released into the public domain.
|
|
141
|
+
See [LICENSE](LICENSE) or <https://unlicense.org>.
|