Metadata-Version: 2.4
Name: keyboard-event-calc
Version: 0.1.0
Summary: A compact desktop counter for global keyboard presses.
Author-email: GGN_2015 <neko@jlulug.org>
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PySide6-Essentials<7,>=6.11.2
Requires-Dist: pynput<2,>=1.8.2
Dynamic: license-file

# keyboard-event-calc

`keyboard-event-calc` is a compact, always-on-top desktop application that
counts global keyboard presses. It provides both a Qt-based graphical interface
and a thread-safe Python API for embedding the counter in another program.

The application stores aggregate statistics only. It never stores typed text,
the order of key presses, keyboard shortcuts as sequences, window titles, or
the name of the active application.

## Features

- Counts global keyboard press events, including letters, numbers, function
  keys, navigation keys, modifiers, and keys used in shortcuts.
- Counts each key in a shortcut separately. For example, `Ctrl+C` adds two to
  the total.
- Filters operating-system key-repeat events while a key remains held down. A
  new count is recorded after that key is released and pressed again.
- Starts in a paused state so global counting begins only after **Start** is
  clicked.
- Uses a green status light while counting and a red status light while paused.
- Provides **Start**, **Pause**, and **Clear** controls in a small fixed-size
  window that remains above other windows.
- Requires confirmation before clearing data. A successful clear resets every
  total, saves the empty state immediately, and leaves counting paused until
  **Start** is clicked again.
- Includes a collapsed-by-default letter ranking. When expanded, it displays
  up to ten letters ordered by descending press count.
- Treats uppercase and lowercase forms as the same letter. Non-letter keys are
  included in the total but not in the letter ranking.
- Restores the previous total and letter counts each time the application is
  launched.
- Saves changes periodically while counting and whenever the counter is paused
  or closed.
- Writes the statistics file atomically to reduce the chance of partial data
  after an interrupted write.

## Requirements

- Python 3.10 or newer
- [uv](https://docs.astral.sh/uv/)
- Windows or Linux
- An X11 session on Linux for complete global keyboard monitoring

The interface uses `PySide6-Essentials`. Global keyboard events are provided by
`pynput`.

## Installation

Create the project virtual environment and install the locked dependencies:

```powershell
uv venv
uv sync
```

`uv sync` installs the application and its dependencies into `.venv`. The same
commands work in PowerShell, Command Prompt, and most Linux shells.

## Running the application

Launch the installed GUI entry point:

```powershell
uv run keyboard-event-calc
```

The package can also be launched as a Python module:

```powershell
uv run python -m keyboard_event_calc
```

The application loads previously saved statistics but initially remains
paused. Click **Start** to begin counting. **Pause** stops new events from being
added without discarding the current totals.

## Letter ranking

Select **Letter details (top 10)** to expand the ranking. The table shows only
letters that have been recorded and displays no more than ten rows. Letters are
sorted by count from highest to lowest; equal counts are sorted alphabetically.

The ranking stores counts by normalized letter, not by physical keyboard
position. Consequently, the active keyboard layout determines which letter a
key represents.

## Data and privacy

The persisted JSON document contains only a format version, the total press
count, and a mapping of letters to counts. It does not contain enough
information to reconstruct text or shortcuts.

The default data file is stored at:

- Windows: `%LOCALAPPDATA%\keyboard-event-calc\stats.json`
- Linux with `XDG_STATE_HOME`:
  `$XDG_STATE_HOME/keyboard-event-calc/stats.json`
- Linux without `XDG_STATE_HOME`:
  `~/.local/state/keyboard-event-calc/stats.json`

Missing, unreadable, malformed, or incompatible data files are treated as an
empty statistics set. If saving fails during a normal close operation, the
window remains open so that saving can be retried.

## Python API

`KeyboardCounter` exposes the same start, pause, clear, snapshot, persistence,
and shutdown lifecycle used by the GUI:

```python
from keyboard_event_calc import KeyboardCounter

with KeyboardCounter() as counter:
    counter.start()

    # The application can do other work while pynput listens in the background.
    snapshot = counter.snapshot()
    print(snapshot.total)
    print(snapshot.active)
    print(snapshot.letters)
    print(snapshot.top_letters(10))

    counter.pause()
```

A custom statistics file can be supplied when the counter is created:

```python
from pathlib import Path

from keyboard_event_calc import KeyboardCounter

counter = KeyboardCounter(storage_path=Path("my-keyboard-stats.json"))
try:
    counter.start()
    print(counter.storage_path)
finally:
    counter.close()
```

### API behavior

- `start()` creates the global listener on first use and begins or resumes
  counting.
- `pause()` stops counting and immediately flushes pending statistics.
- `clear()` resets all statistics, persists the reset, and pauses the counter.
  The confirmation dialog is a GUI responsibility, so API callers should ask
  for confirmation themselves when appropriate.
- `snapshot()` returns an independent `StatsSnapshot` containing `total`,
  `letters`, and `active`.
- `StatsSnapshot.top_letters(limit=10)` returns `(letter, count)` pairs sorted
  by descending count and then alphabetically.
- `record_key(key)` records one event when active and can be used by custom
  integrations. It returns `True` if the event was counted.
- `flush()` writes pending changes without pausing the counter.
- `close()` pauses, saves, stops the listener, and permanently closes that
  counter instance. It is also called by the context manager.
- `storage_path` exposes the path used by the current instance.

All counter state operations are protected by a reentrant lock. Snapshots can
therefore be read safely from a different thread while the listener records
events.

`KeyboardListenerError` is raised when the operating-system listener cannot be
started. File-system failures raised while saving are left as `OSError` so API
callers can choose an appropriate retry policy.

## Linux notes

`pynput` uses X11 for global input monitoring on Linux. Under a native Wayland
session, it is generally limited to applications running through Xwayland and
may not observe all key events. This limitation affects the listener rather
than the Qt interface.

The desktop environment or distribution may also apply its own security rules
to global input monitoring.

## Development and tests

The test suite uses the Python standard library and does not activate a real
global keyboard hook:

```powershell
uv run python -m unittest discover -v
```

The main package is organized as follows:

- `keyboard_event_calc/counter.py`: public API and synchronized counter state
- `keyboard_event_calc/listener.py`: `pynput` adapter and key-repeat filtering
- `keyboard_event_calc/storage.py`: platform paths and atomic JSON persistence
- `keyboard_event_calc/gui.py`: always-on-top Qt Widgets interface
- `tests/test_counter.py`: core behavior and persistence tests

## License

This project is distributed under the MIT License. See `LICENSE` for details.
