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.
Files changed (35) hide show
  1. keyclean-0.9.0/.claude/settings.local.json +13 -0
  2. keyclean-0.9.0/.editorconfig +27 -0
  3. keyclean-0.9.0/.gitignore +31 -0
  4. keyclean-0.9.0/CHANGELOG.md +139 -0
  5. keyclean-0.9.0/LICENSE +24 -0
  6. keyclean-0.9.0/Makefile +182 -0
  7. keyclean-0.9.0/PKG-INFO +141 -0
  8. keyclean-0.9.0/PLAN.md +236 -0
  9. keyclean-0.9.0/PROMPT.md +112 -0
  10. keyclean-0.9.0/README.md +104 -0
  11. keyclean-0.9.0/Screenshot_20260322_051649.png +0 -0
  12. keyclean-0.9.0/Screenshot_20260322_053149.png +0 -0
  13. keyclean-0.9.0/Screenshot_20260322_054529.png +0 -0
  14. keyclean-0.9.0/Screenshot_20260322_055850.png +0 -0
  15. keyclean-0.9.0/pyproject.toml +103 -0
  16. keyclean-0.9.0/src/keyclean/__init__.py +5 -0
  17. keyclean-0.9.0/src/keyclean/__main__.py +6 -0
  18. keyclean-0.9.0/src/keyclean/app.py +139 -0
  19. keyclean-0.9.0/src/keyclean/config.py +78 -0
  20. keyclean-0.9.0/src/keyclean/input_grabber/__init__.py +84 -0
  21. keyclean-0.9.0/src/keyclean/input_grabber/_base.py +37 -0
  22. keyclean-0.9.0/src/keyclean/input_grabber/_fallback.py +31 -0
  23. keyclean-0.9.0/src/keyclean/input_grabber/_pynput.py +62 -0
  24. keyclean-0.9.0/src/keyclean/input_grabber/_wayland.py +166 -0
  25. keyclean-0.9.0/src/keyclean/input_grabber/_x11.py +76 -0
  26. keyclean-0.9.0/src/keyclean/keyboard_layout.py +236 -0
  27. keyclean-0.9.0/src/keyclean/renderer.py +246 -0
  28. keyclean-0.9.0/src/keyclean/safety_sequence.py +31 -0
  29. keyclean-0.9.0/tests/conftest.py +12 -0
  30. keyclean-0.9.0/tests/test_app.py +85 -0
  31. keyclean-0.9.0/tests/test_input_grabber.py +187 -0
  32. keyclean-0.9.0/tests/test_keyboard_layout.py +81 -0
  33. keyclean-0.9.0/tests/test_renderer.py +80 -0
  34. keyclean-0.9.0/tests/test_safety_sequence.py +69 -0
  35. 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>
@@ -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
@@ -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>.