rsvpreader 1.1.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.
- rsvpreader-1.1.0/.gitignore +19 -0
- rsvpreader-1.1.0/LICENSE +21 -0
- rsvpreader-1.1.0/PKG-INFO +149 -0
- rsvpreader-1.1.0/README.md +122 -0
- rsvpreader-1.1.0/pyproject.toml +70 -0
- rsvpreader-1.1.0/src/rsvpreader/__init__.py +8 -0
- rsvpreader-1.1.0/src/rsvpreader/__main__.py +6 -0
- rsvpreader-1.1.0/src/rsvpreader/app.py +234 -0
- rsvpreader-1.1.0/src/rsvpreader/autoplay.py +52 -0
- rsvpreader-1.1.0/src/rsvpreader/browser_view.py +154 -0
- rsvpreader-1.1.0/src/rsvpreader/cli.py +104 -0
- rsvpreader-1.1.0/src/rsvpreader/colors.py +24 -0
- rsvpreader-1.1.0/src/rsvpreader/layout.py +74 -0
- rsvpreader-1.1.0/src/rsvpreader/library.py +43 -0
- rsvpreader-1.1.0/src/rsvpreader/paths.py +57 -0
- rsvpreader-1.1.0/src/rsvpreader/pivot.py +65 -0
- rsvpreader-1.1.0/src/rsvpreader/progress.py +54 -0
- rsvpreader-1.1.0/src/rsvpreader/reader_view.py +323 -0
- rsvpreader-1.1.0/src/rsvpreader/session.py +85 -0
- rsvpreader-1.1.0/src/rsvpreader/settings.py +96 -0
- rsvpreader-1.1.0/src/rsvpreader/settings_panel.py +150 -0
- rsvpreader-1.1.0/src/rsvpreader/storage.py +38 -0
- rsvpreader-1.1.0/src/rsvpreader/text.py +194 -0
- rsvpreader-1.1.0/src/rsvpreader/theme.py +60 -0
- rsvpreader-1.1.0/tests/test_autoplay.py +40 -0
- rsvpreader-1.1.0/tests/test_gui_smoke.py +237 -0
- rsvpreader-1.1.0/tests/test_layout.py +42 -0
- rsvpreader-1.1.0/tests/test_library.py +32 -0
- rsvpreader-1.1.0/tests/test_paths.py +40 -0
- rsvpreader-1.1.0/tests/test_pivot.py +60 -0
- rsvpreader-1.1.0/tests/test_progress.py +53 -0
- rsvpreader-1.1.0/tests/test_session.py +61 -0
- rsvpreader-1.1.0/tests/test_settings.py +80 -0
- rsvpreader-1.1.0/tests/test_text.py +190 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
|
|
11
|
+
# Personal content: books and the legacy single-script settings file
|
|
12
|
+
book/
|
|
13
|
+
rsvp_settings.json
|
|
14
|
+
|
|
15
|
+
# Local task tracking (tasks-axi)
|
|
16
|
+
backlog.md
|
|
17
|
+
|
|
18
|
+
# macOS
|
|
19
|
+
.DS_Store
|
rsvpreader-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shreejit Verma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: rsvpreader
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Spritz-style rapid serial visual presentation reader for plain-text books, with a fixed pivot letter and per-book resume.
|
|
5
|
+
Project-URL: Homepage, https://github.com/shreejitverma/rsvpreader
|
|
6
|
+
Project-URL: Repository, https://github.com/shreejitverma/rsvpreader
|
|
7
|
+
Project-URL: Issues, https://github.com/shreejitverma/rsvpreader/issues
|
|
8
|
+
Project-URL: Releases, https://github.com/shreejitverma/rsvpreader/releases
|
|
9
|
+
Author: Shreejit Verma
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: reader,rsvp,speed-reading,spritz,tkinter
|
|
13
|
+
Classifier: Environment :: MacOS X
|
|
14
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
15
|
+
Classifier: Environment :: X11 Applications
|
|
16
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Text Processing
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# rsvpreader
|
|
29
|
+
|
|
30
|
+
A Spritz-style rapid serial visual presentation reader for plain-text books.
|
|
31
|
+
It shows one chunk of one to three words at a time with a fixed pivot letter pinned to the center of the screen, or one full sentence at a time, and remembers where you stopped in every book.
|
|
32
|
+
|
|
33
|
+
[](https://github.com/shreejitverma/rsvpreader/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pypi.org/project/rsvpreader/)
|
|
35
|
+
|
|
36
|
+
## Install and run
|
|
37
|
+
|
|
38
|
+
Requires Python 3.10 or newer with Tk.
|
|
39
|
+
On macOS, Homebrew's Python ships without Tk; `uv` installs a Python that has it.
|
|
40
|
+
|
|
41
|
+
From PyPI:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
uv tool install rsvpreader # or: pipx install rsvpreader, pip install rsvpreader
|
|
45
|
+
rsvpreader # fullscreen, reads ./book
|
|
46
|
+
rsvpreader --windowed # normal window
|
|
47
|
+
rsvpreader ~/Books # a different book folder
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
From a checkout:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
uv sync
|
|
54
|
+
uv run rsvpreader --windowed
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`python rsvpreader.py` works as a launcher from a checkout without installing.
|
|
58
|
+
|
|
59
|
+
Put `.txt` files, in any folder structure, under `book/` next to where you run the command, or point `RSVPREADER_BOOK_ROOT` or the positional argument at another folder.
|
|
60
|
+
|
|
61
|
+
## Controls
|
|
62
|
+
|
|
63
|
+
| Where | Input | Action |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| Browser | Click, Enter, Space, Right | Open folder or book |
|
|
66
|
+
| Browser | Up, Down | Move selection |
|
|
67
|
+
| Browser | Backspace, Left | Back one folder |
|
|
68
|
+
| Reader | Click, Right, Space | Next chunk or sentence |
|
|
69
|
+
| Reader | Secondary click, Left | Previous chunk or sentence |
|
|
70
|
+
| Reader | Hold click 2 s | Autoplay while held, ramping up to the max WPM over 5 s |
|
|
71
|
+
| Reader | p | Toggle autoplay (same ramp) |
|
|
72
|
+
| Reader | Up, Down | Max WPM plus or minus 10 |
|
|
73
|
+
| Reader | Home, End | First or last unit |
|
|
74
|
+
| Reader | Hold secondary click 3 s | Open settings |
|
|
75
|
+
| Anywhere | s or the gear icon | Toggle settings |
|
|
76
|
+
| Anywhere | Esc | Close settings, else back one level, else leave fullscreen |
|
|
77
|
+
| Anywhere | q | Quit |
|
|
78
|
+
|
|
79
|
+
Autoplay speed is true words per minute: a three-word chunk is shown three times as long as a single word, and a sentence for as long as its word count implies.
|
|
80
|
+
|
|
81
|
+
## Settings
|
|
82
|
+
|
|
83
|
+
Chunk size (1, 2 or 3 words), max autoplay WPM, font family and size, brightness, text color (native color picker) and sentence mode.
|
|
84
|
+
Changes apply live, rebuild the current book without losing your place, and persist to `settings.json` in the user config directory:
|
|
85
|
+
|
|
86
|
+
| Platform | Location |
|
|
87
|
+
|---|---|
|
|
88
|
+
| macOS | `~/Library/Application Support/rsvpreader/` |
|
|
89
|
+
| Linux | `$XDG_CONFIG_HOME/rsvpreader/` or `~/.config/rsvpreader/` |
|
|
90
|
+
| Windows | `%APPDATA%\rsvpreader\` |
|
|
91
|
+
|
|
92
|
+
Override with `--config-dir` or `RSVPREADER_CONFIG_DIR`.
|
|
93
|
+
On first run, settings and reading positions are imported from the directory the 1.0.x releases used (`RSVP_READER_CONFIG_DIR` if set, otherwise `rsvp-reader` in the same locations), then from a legacy `rsvp_settings.json` in the working directory.
|
|
94
|
+
Reading positions live in `progress.json` in the same directory, keyed by the book's absolute path.
|
|
95
|
+
|
|
96
|
+
## How text is chunked
|
|
97
|
+
|
|
98
|
+
1. Regular punctuation (comma, semicolon, colon, apostrophe, dashes) attaches to its word and never forces a break.
|
|
99
|
+
2. A sentence terminator (`.` `!` `?`) closes the chunk on the word carrying it.
|
|
100
|
+
3. An opening double quote (straight or curly) closes the previous chunk, so the quoted word starts a new one.
|
|
101
|
+
4. A closing double quote closes the chunk on the word carrying it.
|
|
102
|
+
|
|
103
|
+
Text pasted without spaces is repaired first: `forth,The` becomes `forth, The` and `it."Believe` becomes `it. "Believe`, while `0.99365`, `1,000`, `e.g.` and `7.25p.m.` stay intact.
|
|
104
|
+
Straight double quotes alternate open and close by parity.
|
|
105
|
+
A token with no letters or digits, such as a spaced en dash, glues to the previous word so it never occupies a chunk of its own.
|
|
106
|
+
|
|
107
|
+
The pivot word is the middle word of a three-word chunk, the word with more letters in a two-word chunk (tie goes to the first), or the only word.
|
|
108
|
+
The pivot letter is the middle letter counting letters only, rounding left for an even count (`Bull` highlights `u`); a word with no letters falls back to its digits.
|
|
109
|
+
|
|
110
|
+
To inspect the units for any file without opening the GUI:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
uv run rsvpreader --dump-chunks "book/selfhelp/some book.txt" --chunk-size 3
|
|
114
|
+
uv run rsvpreader --dump-chunks "book/selfhelp/some book.txt" --sentences
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
uv sync
|
|
121
|
+
uv run pytest # unit tests plus Tk smoke tests (skipped without a display)
|
|
122
|
+
uv run ruff check src tests
|
|
123
|
+
uv run ruff format src tests
|
|
124
|
+
uv build # sdist and wheel under dist/
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
CI runs lint and tests on Linux, macOS and Windows for every push and pull request, and builds the wheel on Linux.
|
|
128
|
+
|
|
129
|
+
### Releasing
|
|
130
|
+
|
|
131
|
+
The package version lives only in `__version__` in `src/rsvpreader/__init__.py`.
|
|
132
|
+
To release: bump it, merge, then publish a GitHub release whose tag is `v<version>` (for example `v1.0.1`).
|
|
133
|
+
The publish workflow builds the distributions, checks that the tag matches the version, and uploads to PyPI through trusted publishing; no API token is stored anywhere.
|
|
134
|
+
|
|
135
|
+
Layout under `src/rsvpreader/`:
|
|
136
|
+
|
|
137
|
+
| Module | Role |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `text.py` | Normalization, tokenization, chunk and sentence building, file decoding |
|
|
140
|
+
| `pivot.py` | Pivot word and letter selection |
|
|
141
|
+
| `session.py` | Reading cursor over units; position survives mode and chunk-size changes |
|
|
142
|
+
| `autoplay.py` | WPM ramp and due-time logic driven by an external clock |
|
|
143
|
+
| `layout.py` | Pivot box and flow-text geometry |
|
|
144
|
+
| `settings.py`, `progress.py`, `storage.py`, `paths.py`, `colors.py` | Validated settings, per-book resume, atomic JSON, config locations, hex color helpers |
|
|
145
|
+
| `library.py` | Folder listing |
|
|
146
|
+
| `app.py`, `browser_view.py`, `reader_view.py`, `settings_panel.py`, `theme.py` | Tk shell and views |
|
|
147
|
+
| `cli.py` | Argument parsing and entry point |
|
|
148
|
+
|
|
149
|
+
Everything outside the Tk modules is pure and unit tested; the Tk smoke tests drive a real window and assert the rendered pivot geometry from Tk's own bounding boxes.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# rsvpreader
|
|
2
|
+
|
|
3
|
+
A Spritz-style rapid serial visual presentation reader for plain-text books.
|
|
4
|
+
It shows one chunk of one to three words at a time with a fixed pivot letter pinned to the center of the screen, or one full sentence at a time, and remembers where you stopped in every book.
|
|
5
|
+
|
|
6
|
+
[](https://github.com/shreejitverma/rsvpreader/actions/workflows/ci.yml)
|
|
7
|
+
[](https://pypi.org/project/rsvpreader/)
|
|
8
|
+
|
|
9
|
+
## Install and run
|
|
10
|
+
|
|
11
|
+
Requires Python 3.10 or newer with Tk.
|
|
12
|
+
On macOS, Homebrew's Python ships without Tk; `uv` installs a Python that has it.
|
|
13
|
+
|
|
14
|
+
From PyPI:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
uv tool install rsvpreader # or: pipx install rsvpreader, pip install rsvpreader
|
|
18
|
+
rsvpreader # fullscreen, reads ./book
|
|
19
|
+
rsvpreader --windowed # normal window
|
|
20
|
+
rsvpreader ~/Books # a different book folder
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
From a checkout:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
uv sync
|
|
27
|
+
uv run rsvpreader --windowed
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`python rsvpreader.py` works as a launcher from a checkout without installing.
|
|
31
|
+
|
|
32
|
+
Put `.txt` files, in any folder structure, under `book/` next to where you run the command, or point `RSVPREADER_BOOK_ROOT` or the positional argument at another folder.
|
|
33
|
+
|
|
34
|
+
## Controls
|
|
35
|
+
|
|
36
|
+
| Where | Input | Action |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| Browser | Click, Enter, Space, Right | Open folder or book |
|
|
39
|
+
| Browser | Up, Down | Move selection |
|
|
40
|
+
| Browser | Backspace, Left | Back one folder |
|
|
41
|
+
| Reader | Click, Right, Space | Next chunk or sentence |
|
|
42
|
+
| Reader | Secondary click, Left | Previous chunk or sentence |
|
|
43
|
+
| Reader | Hold click 2 s | Autoplay while held, ramping up to the max WPM over 5 s |
|
|
44
|
+
| Reader | p | Toggle autoplay (same ramp) |
|
|
45
|
+
| Reader | Up, Down | Max WPM plus or minus 10 |
|
|
46
|
+
| Reader | Home, End | First or last unit |
|
|
47
|
+
| Reader | Hold secondary click 3 s | Open settings |
|
|
48
|
+
| Anywhere | s or the gear icon | Toggle settings |
|
|
49
|
+
| Anywhere | Esc | Close settings, else back one level, else leave fullscreen |
|
|
50
|
+
| Anywhere | q | Quit |
|
|
51
|
+
|
|
52
|
+
Autoplay speed is true words per minute: a three-word chunk is shown three times as long as a single word, and a sentence for as long as its word count implies.
|
|
53
|
+
|
|
54
|
+
## Settings
|
|
55
|
+
|
|
56
|
+
Chunk size (1, 2 or 3 words), max autoplay WPM, font family and size, brightness, text color (native color picker) and sentence mode.
|
|
57
|
+
Changes apply live, rebuild the current book without losing your place, and persist to `settings.json` in the user config directory:
|
|
58
|
+
|
|
59
|
+
| Platform | Location |
|
|
60
|
+
|---|---|
|
|
61
|
+
| macOS | `~/Library/Application Support/rsvpreader/` |
|
|
62
|
+
| Linux | `$XDG_CONFIG_HOME/rsvpreader/` or `~/.config/rsvpreader/` |
|
|
63
|
+
| Windows | `%APPDATA%\rsvpreader\` |
|
|
64
|
+
|
|
65
|
+
Override with `--config-dir` or `RSVPREADER_CONFIG_DIR`.
|
|
66
|
+
On first run, settings and reading positions are imported from the directory the 1.0.x releases used (`RSVP_READER_CONFIG_DIR` if set, otherwise `rsvp-reader` in the same locations), then from a legacy `rsvp_settings.json` in the working directory.
|
|
67
|
+
Reading positions live in `progress.json` in the same directory, keyed by the book's absolute path.
|
|
68
|
+
|
|
69
|
+
## How text is chunked
|
|
70
|
+
|
|
71
|
+
1. Regular punctuation (comma, semicolon, colon, apostrophe, dashes) attaches to its word and never forces a break.
|
|
72
|
+
2. A sentence terminator (`.` `!` `?`) closes the chunk on the word carrying it.
|
|
73
|
+
3. An opening double quote (straight or curly) closes the previous chunk, so the quoted word starts a new one.
|
|
74
|
+
4. A closing double quote closes the chunk on the word carrying it.
|
|
75
|
+
|
|
76
|
+
Text pasted without spaces is repaired first: `forth,The` becomes `forth, The` and `it."Believe` becomes `it. "Believe`, while `0.99365`, `1,000`, `e.g.` and `7.25p.m.` stay intact.
|
|
77
|
+
Straight double quotes alternate open and close by parity.
|
|
78
|
+
A token with no letters or digits, such as a spaced en dash, glues to the previous word so it never occupies a chunk of its own.
|
|
79
|
+
|
|
80
|
+
The pivot word is the middle word of a three-word chunk, the word with more letters in a two-word chunk (tie goes to the first), or the only word.
|
|
81
|
+
The pivot letter is the middle letter counting letters only, rounding left for an even count (`Bull` highlights `u`); a word with no letters falls back to its digits.
|
|
82
|
+
|
|
83
|
+
To inspect the units for any file without opening the GUI:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
uv run rsvpreader --dump-chunks "book/selfhelp/some book.txt" --chunk-size 3
|
|
87
|
+
uv run rsvpreader --dump-chunks "book/selfhelp/some book.txt" --sentences
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
uv sync
|
|
94
|
+
uv run pytest # unit tests plus Tk smoke tests (skipped without a display)
|
|
95
|
+
uv run ruff check src tests
|
|
96
|
+
uv run ruff format src tests
|
|
97
|
+
uv build # sdist and wheel under dist/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
CI runs lint and tests on Linux, macOS and Windows for every push and pull request, and builds the wheel on Linux.
|
|
101
|
+
|
|
102
|
+
### Releasing
|
|
103
|
+
|
|
104
|
+
The package version lives only in `__version__` in `src/rsvpreader/__init__.py`.
|
|
105
|
+
To release: bump it, merge, then publish a GitHub release whose tag is `v<version>` (for example `v1.0.1`).
|
|
106
|
+
The publish workflow builds the distributions, checks that the tag matches the version, and uploads to PyPI through trusted publishing; no API token is stored anywhere.
|
|
107
|
+
|
|
108
|
+
Layout under `src/rsvpreader/`:
|
|
109
|
+
|
|
110
|
+
| Module | Role |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `text.py` | Normalization, tokenization, chunk and sentence building, file decoding |
|
|
113
|
+
| `pivot.py` | Pivot word and letter selection |
|
|
114
|
+
| `session.py` | Reading cursor over units; position survives mode and chunk-size changes |
|
|
115
|
+
| `autoplay.py` | WPM ramp and due-time logic driven by an external clock |
|
|
116
|
+
| `layout.py` | Pivot box and flow-text geometry |
|
|
117
|
+
| `settings.py`, `progress.py`, `storage.py`, `paths.py`, `colors.py` | Validated settings, per-book resume, atomic JSON, config locations, hex color helpers |
|
|
118
|
+
| `library.py` | Folder listing |
|
|
119
|
+
| `app.py`, `browser_view.py`, `reader_view.py`, `settings_panel.py`, `theme.py` | Tk shell and views |
|
|
120
|
+
| `cli.py` | Argument parsing and entry point |
|
|
121
|
+
|
|
122
|
+
Everything outside the Tk modules is pure and unit tested; the Tk smoke tests drive a real window and assert the rendered pivot geometry from Tk's own bounding boxes.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rsvpreader"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Spritz-style rapid serial visual presentation reader for plain-text books, with a fixed pivot letter and per-book resume."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Shreejit Verma" }]
|
|
10
|
+
keywords = ["rsvp", "speed-reading", "spritz", "tkinter", "reader"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Environment :: X11 Applications",
|
|
13
|
+
"Environment :: MacOS X",
|
|
14
|
+
"Environment :: Win32 (MS Windows)",
|
|
15
|
+
"Intended Audience :: End Users/Desktop",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Topic :: Text Processing",
|
|
24
|
+
]
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
rsvpreader = "rsvpreader.cli:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/shreejitverma/rsvpreader"
|
|
32
|
+
Repository = "https://github.com/shreejitverma/rsvpreader"
|
|
33
|
+
Issues = "https://github.com/shreejitverma/rsvpreader/issues"
|
|
34
|
+
Releases = "https://github.com/shreejitverma/rsvpreader/releases"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling>=1.27"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.version]
|
|
41
|
+
path = "src/rsvpreader/__init__.py"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/rsvpreader"]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.sdist]
|
|
47
|
+
only-include = ["src/rsvpreader", "tests"]
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = ["pytest>=8", "ruff>=0.6"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
addopts = "-q"
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 100
|
|
58
|
+
target-version = "py310"
|
|
59
|
+
src = ["src", "tests"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "I", "B", "UP", "SIM", "RUF", "N", "PL"]
|
|
63
|
+
ignore = [
|
|
64
|
+
"PLR0913", # many keyword args are fine for layout/geometry helpers
|
|
65
|
+
"PLR2004", # magic numbers in tests and geometry are self-explanatory
|
|
66
|
+
"RUF001", "RUF002", "RUF003", # typographic quotes and dashes are this project's domain
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint.per-file-ignores]
|
|
70
|
+
"tests/*" = ["PLR0915"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""rsvpreader: a Spritz-style rapid serial visual presentation reader for plain-text books.
|
|
2
|
+
|
|
3
|
+
The package is split into pure, Tk-free modules (text, pivot, session, autoplay, layout,
|
|
4
|
+
settings, progress, library) and thin Tk views (app, browser_view, reader_view,
|
|
5
|
+
settings_panel). Everything under the pure modules is unit tested without a display.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "1.1.0"
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Application shell: window, navigation stack, settings ownership, key dispatch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import tkinter as tk
|
|
7
|
+
import tkinter.font as tkfont
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from rsvpreader.browser_view import FolderView
|
|
11
|
+
from rsvpreader.colors import apply_brightness
|
|
12
|
+
from rsvpreader.progress import ProgressStore
|
|
13
|
+
from rsvpreader.reader_view import ReaderView
|
|
14
|
+
from rsvpreader.settings import Settings, SettingsStore, platform_default_font_family
|
|
15
|
+
from rsvpreader.settings_panel import SettingsPanel
|
|
16
|
+
from rsvpreader.theme import (
|
|
17
|
+
BG,
|
|
18
|
+
STATUS_BRIGHTNESS_FACTOR,
|
|
19
|
+
UI_FONT_CANDIDATES,
|
|
20
|
+
Theme,
|
|
21
|
+
resolve_font_family,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
WINDOWED_GEOMETRY = "1100x700"
|
|
25
|
+
SETTINGS_SAVE_DEBOUNCE_MS = 300
|
|
26
|
+
GEAR_GLYPH = "⚙"
|
|
27
|
+
REBUILD_KEYS = frozenset({"chunk_size", "sentence_mode"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class App:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
root: tk.Tk,
|
|
34
|
+
*,
|
|
35
|
+
book_root: Path,
|
|
36
|
+
settings_store: SettingsStore,
|
|
37
|
+
progress_store: ProgressStore,
|
|
38
|
+
fullscreen: bool = True,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.root = root
|
|
41
|
+
self.book_root = Path(book_root)
|
|
42
|
+
self.settings_store = settings_store
|
|
43
|
+
self.progress = progress_store
|
|
44
|
+
self.installed_fonts = frozenset(tkfont.families(root))
|
|
45
|
+
self.theme = Theme(resolve_font_family("", self.installed_fonts, UI_FONT_CANDIDATES))
|
|
46
|
+
self._fonts: dict[tuple[str, int, str], tkfont.Font] = {}
|
|
47
|
+
self._settings_save_job: str | None = None
|
|
48
|
+
|
|
49
|
+
loaded = settings_store.load()
|
|
50
|
+
self.settings: Settings = loaded.with_changes(
|
|
51
|
+
font_family=resolve_font_family(
|
|
52
|
+
loaded.font_family,
|
|
53
|
+
self.installed_fonts,
|
|
54
|
+
(platform_default_font_family(), *UI_FONT_CANDIDATES),
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
root.title("rsvpreader")
|
|
59
|
+
root.configure(bg=BG)
|
|
60
|
+
self.is_fullscreen = fullscreen
|
|
61
|
+
if fullscreen:
|
|
62
|
+
root.attributes("-fullscreen", True)
|
|
63
|
+
else:
|
|
64
|
+
root.geometry(WINDOWED_GEOMETRY)
|
|
65
|
+
|
|
66
|
+
self.container = tk.Frame(root, bg=BG)
|
|
67
|
+
self.container.pack(fill="both", expand=True)
|
|
68
|
+
|
|
69
|
+
self.nav_stack: list[tuple[str, Path]] = []
|
|
70
|
+
self.view: FolderView | ReaderView | None = None
|
|
71
|
+
self.settings_panel: SettingsPanel | None = None
|
|
72
|
+
self._gear: tk.Label | None = None
|
|
73
|
+
|
|
74
|
+
root.bind("<Escape>", lambda e: self.on_escape())
|
|
75
|
+
root.bind("<Key>", self._on_key)
|
|
76
|
+
root.protocol("WM_DELETE_WINDOW", self.quit)
|
|
77
|
+
|
|
78
|
+
self.push_folder(self.book_root)
|
|
79
|
+
|
|
80
|
+
# -- appearance -----------------------------------------------------------------
|
|
81
|
+
def accent(self) -> str:
|
|
82
|
+
return apply_brightness(self.settings.font_color, self.settings.brightness)
|
|
83
|
+
|
|
84
|
+
def dim_accent(self) -> str:
|
|
85
|
+
return apply_brightness(
|
|
86
|
+
self.settings.font_color, self.settings.brightness * STATUS_BRIGHTNESS_FACTOR
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def font(self, family: str, size: int, weight: str = "normal") -> tkfont.Font:
|
|
90
|
+
"""Cached named fonts; creating one per redraw would leak Tk font objects."""
|
|
91
|
+
key = (family, size, weight)
|
|
92
|
+
if key not in self._fonts:
|
|
93
|
+
self._fonts[key] = tkfont.Font(root=self.root, family=family, size=size, weight=weight)
|
|
94
|
+
return self._fonts[key]
|
|
95
|
+
|
|
96
|
+
def reader_font(self) -> tkfont.Font:
|
|
97
|
+
return self.font(self.settings.font_family, self.settings.font_size, "bold")
|
|
98
|
+
|
|
99
|
+
# -- navigation --------------------------------------------------------------------
|
|
100
|
+
def push_folder(self, folder: Path) -> None:
|
|
101
|
+
self.nav_stack.append(("folder", Path(folder)))
|
|
102
|
+
self._show_top()
|
|
103
|
+
|
|
104
|
+
def open_book(self, book_path: Path) -> None:
|
|
105
|
+
self.nav_stack.append(("reader", Path(book_path)))
|
|
106
|
+
self._show_top()
|
|
107
|
+
|
|
108
|
+
def _show_top(self, focus: Path | None = None) -> None:
|
|
109
|
+
"""Render the top of the stack; `focus` preselects that entry in a folder view."""
|
|
110
|
+
self.close_settings_panel()
|
|
111
|
+
if self.view is not None:
|
|
112
|
+
self.view.destroy()
|
|
113
|
+
self.view = None
|
|
114
|
+
kind, path = self.nav_stack[-1]
|
|
115
|
+
if kind == "folder":
|
|
116
|
+
self.view = FolderView(self, self.container, path, focus=focus)
|
|
117
|
+
else:
|
|
118
|
+
self.view = ReaderView(self, self.container, path)
|
|
119
|
+
self._add_gear()
|
|
120
|
+
|
|
121
|
+
def on_escape(self) -> None:
|
|
122
|
+
if self.settings_panel is not None:
|
|
123
|
+
self.close_settings_panel()
|
|
124
|
+
return
|
|
125
|
+
if len(self.nav_stack) > 1:
|
|
126
|
+
_, left = self.nav_stack.pop()
|
|
127
|
+
self._show_top(focus=left)
|
|
128
|
+
return
|
|
129
|
+
if self.is_fullscreen:
|
|
130
|
+
self.root.attributes("-fullscreen", False)
|
|
131
|
+
self.root.geometry(WINDOWED_GEOMETRY)
|
|
132
|
+
self.is_fullscreen = False
|
|
133
|
+
|
|
134
|
+
def quit(self) -> None:
|
|
135
|
+
if self.view is not None:
|
|
136
|
+
self.view.destroy() # the reader view saves its position on destroy
|
|
137
|
+
self.view = None
|
|
138
|
+
self._flush_settings_save()
|
|
139
|
+
self.progress.save()
|
|
140
|
+
self.root.destroy()
|
|
141
|
+
|
|
142
|
+
def _on_key(self, event: tk.Event) -> None:
|
|
143
|
+
keysym = event.keysym
|
|
144
|
+
if keysym == "Escape":
|
|
145
|
+
return # handled by the dedicated <Escape> binding
|
|
146
|
+
if event.char == "q":
|
|
147
|
+
self.quit()
|
|
148
|
+
return
|
|
149
|
+
if event.char == "s":
|
|
150
|
+
self.toggle_settings_panel()
|
|
151
|
+
return
|
|
152
|
+
if self.settings_panel is not None:
|
|
153
|
+
return # keys belong to the panel's widgets while it is open
|
|
154
|
+
if self.view is not None:
|
|
155
|
+
self.view.on_key(event)
|
|
156
|
+
|
|
157
|
+
# -- gear icon + settings panel -------------------------------------------------
|
|
158
|
+
def _add_gear(self) -> None:
|
|
159
|
+
self._gear = tk.Label(
|
|
160
|
+
self.container,
|
|
161
|
+
text=GEAR_GLYPH,
|
|
162
|
+
font=self.theme.gear,
|
|
163
|
+
fg=self.accent(),
|
|
164
|
+
bg=BG,
|
|
165
|
+
cursor="hand2",
|
|
166
|
+
)
|
|
167
|
+
self._gear.place(relx=1.0, rely=0.0, x=-16, y=10, anchor="ne")
|
|
168
|
+
self._gear.bind("<Button-1>", lambda e: self.toggle_settings_panel())
|
|
169
|
+
|
|
170
|
+
def toggle_settings_panel(self) -> None:
|
|
171
|
+
if self.settings_panel is None:
|
|
172
|
+
self.open_settings_panel()
|
|
173
|
+
else:
|
|
174
|
+
self.close_settings_panel()
|
|
175
|
+
|
|
176
|
+
def open_settings_panel(self) -> None:
|
|
177
|
+
if self.settings_panel is None:
|
|
178
|
+
self.settings_panel = SettingsPanel(self, self.container)
|
|
179
|
+
|
|
180
|
+
def close_settings_panel(self) -> None:
|
|
181
|
+
if self.settings_panel is not None:
|
|
182
|
+
self.settings_panel.destroy()
|
|
183
|
+
self.settings_panel = None
|
|
184
|
+
|
|
185
|
+
# -- settings ---------------------------------------------------------------------
|
|
186
|
+
def update_settings(self, **changes: object) -> None:
|
|
187
|
+
new = self.settings.with_changes(**changes)
|
|
188
|
+
if new == self.settings:
|
|
189
|
+
return
|
|
190
|
+
rebuild = any(getattr(new, k) != getattr(self.settings, k) for k in REBUILD_KEYS)
|
|
191
|
+
self.settings = new
|
|
192
|
+
self._schedule_settings_save()
|
|
193
|
+
if self.view is not None:
|
|
194
|
+
self.view.on_settings_changed(rebuild=rebuild)
|
|
195
|
+
if self._gear is not None:
|
|
196
|
+
self._gear.configure(fg=self.accent())
|
|
197
|
+
if self.settings_panel is not None:
|
|
198
|
+
self.settings_panel.refresh()
|
|
199
|
+
|
|
200
|
+
def _schedule_settings_save(self) -> None:
|
|
201
|
+
if self._settings_save_job is not None:
|
|
202
|
+
self.root.after_cancel(self._settings_save_job)
|
|
203
|
+
self._settings_save_job = self.root.after(
|
|
204
|
+
SETTINGS_SAVE_DEBOUNCE_MS, self._flush_settings_save
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _flush_settings_save(self) -> None:
|
|
208
|
+
if self._settings_save_job is not None:
|
|
209
|
+
self.root.after_cancel(self._settings_save_job)
|
|
210
|
+
self._settings_save_job = None
|
|
211
|
+
self.settings_store.save(self.settings)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def run_app(
|
|
215
|
+
*,
|
|
216
|
+
book_root: Path,
|
|
217
|
+
settings_store: SettingsStore,
|
|
218
|
+
progress_store: ProgressStore,
|
|
219
|
+
fullscreen: bool,
|
|
220
|
+
) -> int:
|
|
221
|
+
try:
|
|
222
|
+
root = tk.Tk()
|
|
223
|
+
except tk.TclError as exc:
|
|
224
|
+
print(f"rsvpreader: cannot open a display: {exc}", file=sys.stderr)
|
|
225
|
+
return 1
|
|
226
|
+
App(
|
|
227
|
+
root,
|
|
228
|
+
book_root=book_root,
|
|
229
|
+
settings_store=settings_store,
|
|
230
|
+
progress_store=progress_store,
|
|
231
|
+
fullscreen=fullscreen,
|
|
232
|
+
)
|
|
233
|
+
root.mainloop()
|
|
234
|
+
return 0
|