console-window 1.4.3__tar.gz → 2.0.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.
- console_window-2.0.0/.github/workflows/ci.yml +52 -0
- console_window-2.0.0/AUDIT.md +152 -0
- console_window-2.0.0/CHANGELOG.md +285 -0
- console_window-2.0.0/PKG-INFO +245 -0
- console_window-2.0.0/README.md +225 -0
- console_window-1.4.3/ConsoleWindowDemo.py → console_window-2.0.0/SampleApp.py +84 -111
- console_window-2.0.0/console_window/__init__.py +31 -0
- console_window-2.0.0/console_window/backend.py +279 -0
- console_window-2.0.0/console_window/context.py +37 -0
- console_window-2.0.0/console_window/geometry.py +82 -0
- console_window-2.0.0/console_window/header.py +126 -0
- console_window-2.0.0/console_window/keys.py +18 -0
- console_window-2.0.0/console_window/opts.py +78 -0
- console_window-2.0.0/console_window/screens.py +234 -0
- console_window-2.0.0/console_window/signals.py +49 -0
- console_window-2.0.0/console_window/spinner.py +333 -0
- console_window-2.0.0/console_window/textwidth.py +170 -0
- console_window-2.0.0/console_window/theme.py +231 -0
- console_window-2.0.0/console_window/widgets.py +337 -0
- console_window-2.0.0/console_window/window.py +1983 -0
- console_window-2.0.0/pyproject.toml +85 -0
- console_window-2.0.0/runner +13 -0
- console_window-2.0.0/tests/test_backend.py +315 -0
- console_window-2.0.0/tests/test_context.py +45 -0
- console_window-2.0.0/tests/test_demo_mode.py +46 -0
- console_window-2.0.0/tests/test_dialogs.py +177 -0
- console_window-2.0.0/tests/test_flash.py +130 -0
- console_window-2.0.0/tests/test_geometry.py +116 -0
- console_window-2.0.0/tests/test_header.py +172 -0
- console_window-2.0.0/tests/test_interactive.py +663 -0
- console_window-2.0.0/tests/test_overflow.py +114 -0
- console_window-2.0.0/tests/test_public_api.py +127 -0
- console_window-2.0.0/tests/test_render_retry.py +88 -0
- console_window-2.0.0/tests/test_sample_app.py +123 -0
- console_window-2.0.0/tests/test_signals.py +71 -0
- console_window-2.0.0/tests/test_spinner.py +268 -0
- console_window-2.0.0/tests/test_textwidth.py +143 -0
- console_window-2.0.0/tests/test_theme.py +72 -0
- console_window-2.0.0/tests/test_widechars.py +191 -0
- console_window-2.0.0/tests/test_widgets.py +184 -0
- console_window-1.4.3/PKG-INFO +0 -133
- console_window-1.4.3/README.md +0 -117
- console_window-1.4.3/console_window/ConsoleWindow.py +0 -3815
- console_window-1.4.3/console_window/__init__.py +0 -14
- console_window-1.4.3/pyproject.toml +0 -47
- {console_window-1.4.3 → console_window-2.0.0}/.gitignore +0 -0
- {console_window-1.4.3 → console_window-2.0.0}/LICENSE +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
name: tests (Python ${{ matrix.python-version }})
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- name: Install test dependencies
|
|
22
|
+
run: python -m pip install --upgrade pip pytest
|
|
23
|
+
- name: Run tests (TTY-free)
|
|
24
|
+
run: python -m pytest -q
|
|
25
|
+
|
|
26
|
+
lint:
|
|
27
|
+
name: ruff
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- name: Set up Python
|
|
32
|
+
uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.x"
|
|
35
|
+
- name: Install ruff
|
|
36
|
+
run: python -m pip install ruff
|
|
37
|
+
- name: Lint
|
|
38
|
+
run: ruff check .
|
|
39
|
+
|
|
40
|
+
types:
|
|
41
|
+
name: mypy
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
- name: Set up Python
|
|
46
|
+
uses: actions/setup-python@v5
|
|
47
|
+
with:
|
|
48
|
+
python-version: "3.x"
|
|
49
|
+
- name: Install mypy
|
|
50
|
+
run: python -m pip install mypy
|
|
51
|
+
- name: Type check
|
|
52
|
+
run: python -m mypy
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# `console-window` — Code & Design Audit
|
|
2
|
+
|
|
3
|
+
**Audited:** 2026-10-15 (`368304d`); **refreshed:** 2026-10-18 (`15125a8`) after removing every finding that has since been resolved. Section references inside older `CHANGELOG.md` entries point at the pre-refresh numbering.
|
|
4
|
+
**Scope:** the `console_window` package, its test suite, `SampleApp.py`, `runner`, packaging and CI. Competitive/ecosystem analysis is deliberately excluded (it is covered in [`README.md`](README.md:1)).
|
|
5
|
+
**Method:** full source read of all 14 package modules plus the example and tests; `ruff`, `pylint`, `mypy`, `pytest` and `coverage` run locally; API reachability established by repo-wide call-site search, not by inference.
|
|
6
|
+
|
|
7
|
+
| Metric | Value |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Package size | 3,988 lines / 2,391 statements across 14 modules; stdlib-only, POSIX curses |
|
|
10
|
+
| Example | [`SampleApp.py`](SampleApp.py:1) 532 lines; [`runner`](runner:1) 13 lines |
|
|
11
|
+
| Tests | 268 tests, ~3,045 lines in 18 files — **all pass** (1.5 s) |
|
|
12
|
+
| Coverage (`--source=console_window`) | **87%** overall; engine [`window.py`](console_window/window.py:1) **88%**, [`theme.py`](console_window/theme.py:1) **69%**, [`screens.py`](console_window/screens.py:1) **67%** |
|
|
13
|
+
| `ruff check .` | clean (`E,F,W,I,B,C4,ANN,RUF,C901`) |
|
|
14
|
+
| `pylint` | 9.92/10 with `C,R` disabled |
|
|
15
|
+
| Type checking | **mypy** configured for `console_window` and run in CI; clean (14 source files) |
|
|
16
|
+
| Complexity (ruff `C901`, threshold 12) | `prompt()` decomposed from 43 to **5**; worst remaining is [`answer()`](console_window/window.py:1167) at **12** |
|
|
17
|
+
|
|
18
|
+
## 1. Executive summary
|
|
19
|
+
|
|
20
|
+
The library is small, dependency-free, and unusually disciplined for a single-author project: a real module split, a headless render backend, 268 fast TTY-free tests, a green linter and type checker, changelog and release metadata, and a worked example that is itself smoke-tested. Resolved findings from the 2026-10-15 audit have been removed from this document; [`CHANGELOG.md`](CHANGELOG.md:1) records them.
|
|
21
|
+
|
|
22
|
+
The remaining risks are narrower than the ones in the original audit. There is no data-loss or security defect, and the interactive core is no longer an untested hole. What is left is one drawing path outside the overflow contract, a body of unreachable or dead-but-broken public surface, residual process-global state, and CI that cannot reproduce its own inputs. Highest-risk findings, in order:
|
|
23
|
+
|
|
24
|
+
1. **[`draw()`](console_window/window.py:749) bypasses the pad-overflow policy.** It inflates `row_cnt` without a capacity check and never records row context or attributes, so phantom rows, silently pickable rows and attribute loss can follow (§3.2).
|
|
25
|
+
2. **A meaningful amount of public API is unreachable and one piece is dead-but-broken.** `Theme.create_picker_screen()`, `ScreenStack.act_in()`, `ScreenStack.is_curr()`, `ConsoleWindow.set_pick_range()`, `BasicHelpScreen` and `OptionSpinner.add()` have no consumer outside the test suite; `add()` additionally discards the `comments` field it exists to populate (§3.3).
|
|
26
|
+
3. **Process-global state is still managed asymmetrically.** The active theme is a class global, and the Ctrl-C policy deliberately latches terminating Ctrl-C for the process lifetime without restoring a host application's prior SIGINT handler (§3.1).
|
|
27
|
+
4. **CI is green but shallow.** Tool versions are unpinned, and there is no coverage or `pylint` gate, so the current 87% coverage and 9.92/10 score are not protected (§3.4).
|
|
28
|
+
|
|
29
|
+
None of these is a data-loss or security defect, and the project's own bar remains high. The work left is concentrated in the list above rather than spread across the package.
|
|
30
|
+
|
|
31
|
+
## 2. Architecture as implemented
|
|
32
|
+
|
|
33
|
+
| Module | Lines | Stmts | Cover | Role |
|
|
34
|
+
| --- | --- | --- | --- | --- |
|
|
35
|
+
| [`window.py`](console_window/window.py:1) | 1983 | 1185 | 88% | The engine: two pads + screen, add/draw/render/prompt/answer/alert/flash, navigation |
|
|
36
|
+
| [`widgets.py`](console_window/widgets.py:1) | 337 | 231 | 86% | `InlineConfirmation`, `IncrementalSearchBar` (pure, no curses drawing) |
|
|
37
|
+
| [`spinner.py`](console_window/spinner.py:1) | 333 | 225 | 87% | `OptionSpinner`: key → option registry, genre inference, scope subtraction, help body |
|
|
38
|
+
| [`screens.py`](console_window/screens.py:1) | 234 | 159 | 67% | `Screen` base, `ScreenStack` router, `HOME_ST`, `BasicHelpScreen` |
|
|
39
|
+
| [`theme.py`](console_window/theme.py:1) | 231 | 71 | 69% | 7 palettes, `set()`/`set_active()`, picker-screen factory |
|
|
40
|
+
| [`textwidth.py`](console_window/textwidth.py:1) | 170 | 129 | 95% | Display-cell arithmetic: width, truncate, justify, wrap, wrap positions |
|
|
41
|
+
| [`header.py`](console_window/header.py:1) | 126 | 88 | 99% | `Span`/`HeaderLine` token API + legacy `[x]`/`KEY:` string parser |
|
|
42
|
+
| [`backend.py`](console_window/backend.py:1) | 279 | 128 | 88% | `Surface`/`RenderBackend` protocols, `CursesBackend`, `HeadlessBackend` |
|
|
43
|
+
| [`geometry.py`](console_window/geometry.py:1) | 82 | 61 | 100% | Pure abut/indicator/pickable-row math |
|
|
44
|
+
| [`opts.py`](console_window/opts.py:1) | 78 | 59 | 100% | `ConsoleWindowOpts` (`__slots__` + kwargs) |
|
|
45
|
+
| [`context.py`](console_window/context.py:1) | 37 | 17 | 100% | Per-row `Context` (`genre`/`pickable`/`abut` + open kwargs) |
|
|
46
|
+
| [`signals.py`](console_window/signals.py:1) | 49 | 24 | 96% | SIGINT policy globals |
|
|
47
|
+
| [`keys.py`](console_window/keys.py:1) | 18 | 3 | 100% | `NAVIGATION_KEYS` |
|
|
48
|
+
| [`__init__.py`](console_window/__init__.py:1) | 31 | 11 | 100% | Re-exports + `__all__` |
|
|
49
|
+
|
|
50
|
+
**Frame cycle.** `main_loop` → `screen.draw_screen()` (`clear()` + `add_header*`/`add_body` into the two pads) → `win.render()` → `fix_positions()` (clamp/scroll/pick, returns `indent`) → gutter/separator/indicator → `pnoutrefresh` both pads → `doupdate()` → `win.prompt()` (key read + built-in navigation) → `handle_key()` → `stack.perform_actions(spin)` (convention dispatch of `<attr>_ACTION`) → `clear()`.
|
|
51
|
+
|
|
52
|
+
**Two data models, one window.** Layout/scroll/pick live in `SimpleNamespace`s (`self.head`, `self.body`) with parallel lists (`texts`, `text_attrs` run-length encoded, `contexts`, `row_cnt`); `Row Context` carries per-row semantics. `Screen` subclasses add routing and `*_ACTION` dispatch. `OptionSpinner` is the key/option registry, optionally scope-aware per screen with automatic scope subtraction ([`spinner.py:134`](console_window/spinner.py:134)).
|
|
53
|
+
|
|
54
|
+
**The seam covers the drawing, input and colour paths.** `RenderBackend` abstracts `start`/`newpad`/`doupdate`/`curs_set`/`update_lines_cols`/`is_term_resized`/`has_colors`/`start_color`/`use_default_colors`/`init_pair`/`color_pair`/`stop`, and the dialogs, flash, resize checks and colour init all go through it.
|
|
55
|
+
|
|
56
|
+
**Two window models over that seam.** `ConsoleWindow(singleton=True)` (the default) restores the historical process-wide screen: every instance shares one screen/backend, `static_scr`/`static_win` are populated, and `ConsoleWindow.stop_curses()` tears the process screen down. `singleton=False` keeps the per-instance model: each window owns its screen and teardown, and `stop_all()` is the multi-window hook. The first construction latches the mode process-wide; mixing values asserts.
|
|
57
|
+
|
|
58
|
+
**Current shape.** Docstrings have been replaced by comments, `_selftest.py` is folded into `SampleApp.py`, the pad-overflow contract is counted + warned + enforced in this project's suite, and the package version is single-sourced through `dynamic = ["version"]`.
|
|
59
|
+
|
|
60
|
+
## 3. Findings
|
|
61
|
+
|
|
62
|
+
### 3.1 🟡 Process-global theme, and a one-way Ctrl-C latch
|
|
63
|
+
|
|
64
|
+
`Theme._current` is a class attribute ([`theme.py:36`](console_window/theme.py:36)) — documented, but it means two windows in one process cannot differ. The Ctrl-C policy is now safe against the interleaving that used to leave a custom handler installed, because terminating Ctrl-C is a one-way process-lifetime latch ([`signals.py:16`](console_window/signals.py:16)): once [`setup_ctrl_c()`](console_window/signals.py:31) sees a terminating request it installs Python's default handler and [`teardown_ctrl_c()`](console_window/signals.py:45) subsequently no-ops.
|
|
65
|
+
|
|
66
|
+
**Limits:** the active theme remains global, and the latch deliberately restores `signal.default_int_handler` rather than a handler a host application may have installed before the first window. There is no public reset. An embedding application that manages SIGINT itself can therefore lose its handler for the rest of the process.
|
|
67
|
+
|
|
68
|
+
**Fix:** make the theme per-window (or document one-theme-per-process explicitly), and either capture/restore the previous SIGINT handler or expose a deliberate reset.
|
|
69
|
+
|
|
70
|
+
### 3.2 🟡 `draw()` bypasses the pad-overflow policy
|
|
71
|
+
|
|
72
|
+
Overflow through `_add()` is counted (`ns.rows_dropped`, exposed as [`ConsoleWindow.rows_dropped`](console_window/window.py:359)), warned about once per fill episode with a caller-facing `stacklevel`, reset by [`clear()`](console_window/window.py:1759), non-fatal by design, and enforced in this project's own suite via `filterwarnings = ["error::UserWarning"]` ([`pyproject.toml:49`](pyproject.toml:49)).
|
|
73
|
+
|
|
74
|
+
**The gap:** [`draw()`](console_window/window.py:749) writes a raw row and inflates `ns.row_cnt = max(ns.row_cnt, y+1)` ([`window.py:755`](console_window/window.py:755)) **without any capacity check**, so it can put `row_cnt` past the pad's real row count — after which `calc()` computes `max_scroll_pos` from phantom rows, `highlight_picked()` walks rows that do not exist, and `rows_dropped` reports nothing. `draw()` also never appends to `contexts`/`text_attrs`, so drawn rows fall through to the default-pickable branch in [`_is_pickable()`](console_window/window.py:367) and lose their attributes on the next full redraw.
|
|
75
|
+
|
|
76
|
+
**Fix:** clamp in `draw()` (or route it through `_add`) and let the same counter warn.
|
|
77
|
+
|
|
78
|
+
### 3.3 🟡 Unreachable public surface (and dead code in the example)
|
|
79
|
+
|
|
80
|
+
No consumer outside the test suite for:
|
|
81
|
+
|
|
82
|
+
| Symbol | Where | Note |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `Theme.create_picker_screen()` | [`theme.py:187`](console_window/theme.py:187) | Untested; duplicates the sample app's theme screen; the factory reaches into `app.opts.theme` |
|
|
85
|
+
| `ScreenStack.act_in()` | [`screens.py:181`](console_window/screens.py:181) | Duplicates what `perform_actions()` already does; `getattr` raises on a missing flag |
|
|
86
|
+
| `ScreenStack.is_curr()` | [`screens.py:168`](console_window/screens.py:168) | Only reachable from `act_in()` |
|
|
87
|
+
| `ConsoleWindow.set_pick_range()` | [`window.py:518`](console_window/window.py:518) | The `pick_range` feature's only public entry point; called only by [`tests/test_interactive.py`](tests/test_interactive.py:1) |
|
|
88
|
+
| `BasicHelpScreen` | [`screens.py:227`](console_window/screens.py:227) | Exported in `__all__`, unused; the sample app writes its own help screen |
|
|
89
|
+
| `OptionSpinner.add()` | [`spinner.py:163`](console_window/spinner.py:163) | Dead **and** broken (see below) |
|
|
90
|
+
| `draw()` | [`window.py:749`](console_window/window.py:749) | Used only by [`tests/test_interactive.py`](tests/test_interactive.py:365) and [`tests/test_widechars.py`](tests/test_widechars.py:1) |
|
|
91
|
+
| `add_fancy_header()` string path | [`window.py:685`](console_window/window.py:685) | The sample uses `add_header_line()`; only tests exercise the string path |
|
|
92
|
+
| `demo_mode` / `set_demo_mode()` | [`window.py:325`](console_window/window.py:325) | Only tests toggle it; internal prompt/render code reads `opts.demo_mode` |
|
|
93
|
+
|
|
94
|
+
`OptionSpinner.add()` truncates `ns.vals` and *then* slices it with the pre-truncation index ([`spinner.py:170`](console_window/spinner.py:170)), so `ns.comments` is always empty — the parameter it exists for is silently dropped. Dead code that cannot be right is worse than no code.
|
|
95
|
+
|
|
96
|
+
The same is true of two example methods: `DemoApp.do_key()` ([`SampleApp.py:219`](SampleApp.py:219)) is never called (`main_loop` calls [`handle_key()`](SampleApp.py:184), which calls `spin.do_key` directly), and `ThemeScreen.theme_screen()` ([`SampleApp.py:512`](SampleApp.py:512)) is never called and, unlike the live [`theme_screen_ACTION()`](SampleApp.py:426) path and `Theme.create_picker_screen`'s `spin_theme_ACTION`, does not update `app.opts.theme`.
|
|
97
|
+
|
|
98
|
+
**Fix:** delete what no consumer needs (these are pre-2.0 leftovers), or promote it deliberately: give each survivor a test and a README mention, and fix `OptionSpinner.add()` if it is retained.
|
|
99
|
+
|
|
100
|
+
### 3.4 🟡 CI is green but shallow, and its inputs are unpinned
|
|
101
|
+
|
|
102
|
+
[`.github/workflows/ci.yml`](.github/workflows/ci.yml:1) runs `pytest -q` on 3.10-3.13, `ruff check .` and `mypy`, installing `pytest`, `ruff` and `mypy` **unpinned** ([`ci.yml:22`](.github/workflows/ci.yml:22), [`ci.yml:36`](.github/workflows/ci.yml:36), [`ci.yml:50`](.github/workflows/ci.yml:50)) — a new tool release can break the build with no code change, and the same build cannot reproduce yesterday's result. There is no coverage gate and no `pylint` (so the 87% coverage and 9.92/10 figures in this audit are not protected by CI).
|
|
103
|
+
|
|
104
|
+
### 3.5 🟢 `answer()` residual: class-wide retry budget and key draining
|
|
105
|
+
|
|
106
|
+
[`answer()`](console_window/window.py:1167) now has one unambiguous meaning per outcome (`str` = submitted, `None` = user abort, `TerminalTooSmallError` = environment), and its recovery loop is bounded. Two narrow issues remain:
|
|
107
|
+
|
|
108
|
+
* the budget is the class-wide `render_retry_seconds` ([`window.py:1193`](console_window/window.py:1193)) rather than a dialog-specific option;
|
|
109
|
+
* [`_recover_from_resize()`](console_window/window.py:1522) still drains and **discards** every queued key ([`window.py:1528`](console_window/window.py:1528)).
|
|
110
|
+
|
|
111
|
+
**Fix:** make the dialog retry budget configurable and preserve (or explicitly document the loss of) keys typed during recovery.
|
|
112
|
+
|
|
113
|
+
### 3.6 🟢 Lint findings lack rationale
|
|
114
|
+
|
|
115
|
+
`pylint` remains in the high 9s only because its warnings are few, not because they are annotated. Current examples: protected-member access ([`window.py:78`](console_window/window.py:78), [`window.py:84`](console_window/window.py:84), [`theme.py:171`](console_window/theme.py:171)), unused arguments ([`window.py:1742`](console_window/window.py:1742), [`backend.py:252`](console_window/backend.py:252), [`backend.py:278`](console_window/backend.py:278)), global statements and unused handler arguments in [`signals.py:19`](console_window/signals.py:19), a not-callable method ([`screens.py:67`](console_window/screens.py:67)), and broad exceptions in the example ([`SampleApp.py:150`](SampleApp.py:150), [`SampleApp.py:523`](SampleApp.py:523)). Each is plausibly deliberate, but none says why.
|
|
116
|
+
|
|
117
|
+
**Fix:** add a short rationale comment or a scoped `# pylint: disable=...` at each site, so a future reader can distinguish intent from accident.
|
|
118
|
+
|
|
119
|
+
## 4. Prioritized recommendations
|
|
120
|
+
|
|
121
|
+
**P1 — robustness and surface**
|
|
122
|
+
|
|
123
|
+
1. Clamp `draw()` to the pad and route its overflow through the existing counter (§3.2).
|
|
124
|
+
2. Delete or promote the unreachable API (§3.3), fix or remove `OptionSpinner.add()`, and delete `DemoApp.do_key()` and `ThemeScreen.theme_screen()`.
|
|
125
|
+
3. Pin CI tool versions and add a coverage floor (even a low one, e.g. `--cov-fail-under=80`) so the suite cannot silently shrink (§3.4).
|
|
126
|
+
|
|
127
|
+
**P2 — honesty and hygiene**
|
|
128
|
+
|
|
129
|
+
4. Make the active theme per-window (or document one-theme-per-process), and capture/restore the host SIGINT handler or expose a reset (§3.1).
|
|
130
|
+
5. Make the dialog retry budget configurable and stop discarding keys during resize recovery (§3.5).
|
|
131
|
+
6. Add rationale comments or scoped disables for the remaining `pylint` findings (§3.6).
|
|
132
|
+
|
|
133
|
+
## 5. What is genuinely good (protect it)
|
|
134
|
+
|
|
135
|
+
* **The test suite is fast and genuinely TTY-free** (268 tests in 1.5 s) and includes headless smoke tests for the example ([`tests/test_sample_app.py`](tests/test_sample_app.py:1)), the interactive core ([`tests/test_interactive.py`](tests/test_interactive.py:1)) and the overflow contract ([`tests/test_overflow.py`](tests/test_overflow.py:1)).
|
|
136
|
+
* **The overflow contract for `_add()` is a model of restraint** — counted, warned once, non-fatal for users, fatal in the project's own suite ([`pyproject.toml:49`](pyproject.toml:49)), and it paid for itself in the same commit by finding the sample app's clipped help screen.
|
|
137
|
+
* **The `prompt()` loop is decomposed and guarded** — the one 160-line, complexity-43 function is now a 12-line intake loop over small helpers (worst `prompt`-family helper: 4), and `C901` at a ratcheted threshold of 12 prevents a new monolith ([`pyproject.toml:62`](pyproject.toml:62)).
|
|
138
|
+
* **`textwidth` and `geometry` are pure and near-fully covered** (95%/100%) — the right place for the fiddly math.
|
|
139
|
+
* **`Context`'s near-miss warning** ([`context.py:30`](console_window/context.py:30)) and **`OptionSpinner`'s conflict detection** ([`spinner.py:127`](console_window/spinner.py:127), [`spinner.py:142`](console_window/spinner.py:142)) turn silent misconfiguration into loud `ValueError`/warning at the exact point of the mistake.
|
|
140
|
+
* **Packaging is correct**: flit module config, static metadata, `__all__` matching the re-exports, and a `runner` that executes the example as `__main__` without an installed package.
|
|
141
|
+
|
|
142
|
+
## 6. Reproduction
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python3 -m ruff check .
|
|
146
|
+
python3 -m pytest -q
|
|
147
|
+
python3 -m coverage run --source=console_window -m pytest -q && python3 -m coverage report -m
|
|
148
|
+
python3 -m pylint console_window SampleApp.py runner --disable=C,R
|
|
149
|
+
python3 -m mypy
|
|
150
|
+
python3 -m ruff check --select C901 console_window SampleApp.py # uses the ratchet in pyproject.toml
|
|
151
|
+
python3 -W error::UserWarning -c "import SampleApp" # exit path for the pad-overflow policy
|
|
152
|
+
```
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `console-window` are documented here.
|
|
4
|
+
This project follows [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- `mypy` type checking for the library: `[tool.mypy]` in `pyproject.toml`
|
|
10
|
+
checks `console_window` (`files`, `ignore_missing_imports`,
|
|
11
|
+
`warn_unused_ignores`, `disallow_untyped_defs = false`) and a new `types` job
|
|
12
|
+
in CI runs it on every push/PR. Ruff's `ANN` rules were already enforcing
|
|
13
|
+
annotation *presence*; this enforces that the annotations are true
|
|
14
|
+
(AUDIT.md §3.4).
|
|
15
|
+
- `[project.urls]` in `pyproject.toml` (Homepage / Repository / Issues /
|
|
16
|
+
Changelog) so published package metadata carries project links.
|
|
17
|
+
- `ConsoleWindow(..., singleton=True)` restores the historical process-wide
|
|
18
|
+
window model for applications that relied on it: `static_scr`/`static_win`
|
|
19
|
+
are populated, every window shares one process screen and backend, and the
|
|
20
|
+
unbound `ConsoleWindow.stop_curses()` tears it down. The first successful
|
|
21
|
+
construction latches the mode; constructing another window with a different
|
|
22
|
+
`singleton` value raises `AssertionError`. Pass `singleton=False` for
|
|
23
|
+
independent per-instance screens and teardown (the test suite does).
|
|
24
|
+
- `ConsoleWindow.rows_dropped`, counting the rows `add_body()`/`add_header()`
|
|
25
|
+
discard once that pad has reached `opts.body_rows` / `opts.head_rows`. The
|
|
26
|
+
first discarded row of each fill episode also emits a `UserWarning` naming the
|
|
27
|
+
pad and the option to raise. Overflow previously left no trace, so an
|
|
28
|
+
under-sized pad showed up only as missing content. The behaviour is
|
|
29
|
+
deliberately non-fatal: the excess is still discarded and the app keeps
|
|
30
|
+
running, `-W error::UserWarning` turns it into a hard failure for anyone who
|
|
31
|
+
wants that, and `clear()` re-arms the warning for the next fill.
|
|
32
|
+
- A headless harness for the interactive core: `HeadlessBackend.queue_keys()`
|
|
33
|
+
feeds ordered key codes to the screen's `getch()`, and the new
|
|
34
|
+
`tests/test_interactive.py` (43 tests) drives `prompt()` and its navigation,
|
|
35
|
+
`render()`/`draw()`, the `answer()` dialog and the resize-recovery path
|
|
36
|
+
without a TTY. Coverage of `window.py` rises from 55% to 84% (missing
|
|
37
|
+
statements 444 → 161), so the half of the engine users actually feel is now
|
|
38
|
+
smoke-tested in CI (AUDIT.md §3.2).
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- The package version now has a single source of truth: `pyproject.toml`
|
|
42
|
+
declares `dynamic = ["version"]` and flit reads `__version__` from
|
|
43
|
+
`console_window/__init__.py` (AUDIT.md §3.14). The stale manual-build recipe
|
|
44
|
+
at the top of `pyproject.toml` is gone.
|
|
45
|
+
- `OptionSpinner.get_value()` is now a pure read: its `coerce` parameter is
|
|
46
|
+
gone and initialisation moved to the explicit `OptionSpinner.normalize(attr)`,
|
|
47
|
+
which `_register()` calls. Apps that read option values through
|
|
48
|
+
`get_value()` are unaffected because `add_key()` already normalizes the value
|
|
49
|
+
during registration; code that passed `coerce=True` directly should call
|
|
50
|
+
`normalize()` instead (AUDIT.md §3.12).
|
|
51
|
+
- Raised the minimum supported Python to **3.10** (was 3.8): `requires-python =
|
|
52
|
+
">=3.10"`, the Ruff `target-version` is now `py310`, and the CI matrix drops
|
|
53
|
+
the 3.8/3.9 legs. The 3.8 floor was never real — [`backend.py`](console_window/backend.py:35)
|
|
54
|
+
calls `curses.set_escdelay()`, which is new in 3.9, so every
|
|
55
|
+
`ConsoleWindow()` raised `AttributeError` on 3.8. 3.10 is a deliberate floor
|
|
56
|
+
rather than the bare 3.9 that `set_escdelay` would have permitted.
|
|
57
|
+
- Renamed `ConsoleWindowDemo.py` to `SampleApp.py`: the old name collided with
|
|
58
|
+
the library's own `set_demo_mode()` feature. Updated [`runner`](runner:1), the
|
|
59
|
+
README, the ruff per-file-ignores and the headless smoke tests
|
|
60
|
+
(`tests/test_sample_app.py`) to match. The classes inside are still named
|
|
61
|
+
`DemoApp`/`DemoScreen`.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
- Hygiene debt from AUDIT.md §3.14: removed blank lines inside the
|
|
65
|
+
`index_at_cell()`/`answer()` signatures and the commented-out assignment in
|
|
66
|
+
`fix_positions()`; replaced `render()`'s `print()`/`except Exception` failure
|
|
67
|
+
path with `logging` and `curses.error`; made `_BACKSPACE_KEYS` a single
|
|
68
|
+
module constant; moved `ScreenStack`'s `SimpleNamespace` import to module
|
|
69
|
+
level; stored navigation help as `NAV_KEY_LINES` so indentation cannot alter
|
|
70
|
+
it; bounded `_NoopSurface.calls` to `MAX_CALLS`; and restored the hardware
|
|
71
|
+
cursor in `CursesBackend.stop()`.
|
|
72
|
+
- The window no longer uses identity or sentinel tricks to decide what it is
|
|
73
|
+
drawing. `_add()` takes an explicit `is_body` keyword from
|
|
74
|
+
`add_header()`/`add_body()` instead of comparing `id(ns)` with `id(self.body)`,
|
|
75
|
+
and `_warn_rows_dropped()` receives the pad name. The undocumented
|
|
76
|
+
`last_pick_pos == -2` "clear every row" sentinel is replaced by the one-shot
|
|
77
|
+
`_pick_clear_pending` flag, which `set_pick_mode()`/`set_pick_range()` set and
|
|
78
|
+
`highlight_picked()` consumes; `last_pick_pos` is now only a row index or `-1`.
|
|
79
|
+
A range change still repaints the picked row when its position is unchanged
|
|
80
|
+
(AUDIT.md §3.13).
|
|
81
|
+
- The help screen no longer relies on `assert` to guard option rendering.
|
|
82
|
+
`OptionSpinner._write_option_body()` now renders a dim `<unset: 'attr'>`
|
|
83
|
+
placeholder when an application has deleted or cleared a registered option,
|
|
84
|
+
so the row shows the same thing under `python -O` instead of falling through
|
|
85
|
+
to `None` (AUDIT.md §3.12).
|
|
86
|
+
- `ConsoleWindowOpts` now rejects unknown option names with a `TypeError`
|
|
87
|
+
instead of silently ignoring them. A typo such as
|
|
88
|
+
`ConsoleWindowOpts(body_row=5000)` used to leave `body_rows` at its default
|
|
89
|
+
and surface only later as discarded pad rows; construction now fails, and a
|
|
90
|
+
`difflib` suggestion points at the intended name (`body_rows`). Multiple
|
|
91
|
+
unknown keys are all reported (AUDIT.md §3.11).
|
|
92
|
+
- `answer()` no longer loops forever on `curses.error`, and it no longer uses
|
|
93
|
+
`None` for both "user aborted" and "terminal too small". The recovery loop is
|
|
94
|
+
bounded by `render_retry_seconds` and then re-raises; both too-small paths
|
|
95
|
+
raise `TerminalTooSmallError` (ESC on the mid-dialog too-small notice still
|
|
96
|
+
returns `None`, since that is a real abort). The dialog timeout is now
|
|
97
|
+
restored in a `finally`, so it is also restored when `answer()` raises.
|
|
98
|
+
- Annotations that `mypy` exposed: `fix_positions()` is declared `-> int`
|
|
99
|
+
(it returns `indent` and the caller uses it), `_draw_line_with_attrs()` and
|
|
100
|
+
`_draw_plain_line()` accept `attrs: list | None`, `pick_range` is typed as
|
|
101
|
+
`list[int] | tuple[int, int] | None`, and `draw()` accepts `str | bytes` to
|
|
102
|
+
match its decode path. `ConsoleWindowOpts` attributes now have explicit types
|
|
103
|
+
instead of being `object` values from `**kwargs`, and the full screen/pad API
|
|
104
|
+
is described by the new `CursesSurface` protocol rather than `object`.
|
|
105
|
+
- Ctrl-C termination is now a process-lifetime latch. The first `ConsoleWindow`
|
|
106
|
+
created with `ctrl_c_terminates=True` sets SIGINT to the default handler and
|
|
107
|
+
no later window can install the key-delivery handler, even after that window
|
|
108
|
+
stops. Previously `teardown_ctrl_c()` decided from whichever policy happened to
|
|
109
|
+
be current, so window A (`False`, handler installed) → window B (`True`) →
|
|
110
|
+
stop A left A's handler installed and Ctrl-C could no longer terminate. The
|
|
111
|
+
latch is deliberately one-way: `ctrl_c_terminates=False` applies only while no
|
|
112
|
+
terminating window has existed in the process (AUDIT.md §3.6).
|
|
113
|
+
- Aborting a `prompt`-genre spinner option (ESC in the `answer()` dialog) no
|
|
114
|
+
longer wipes the stored value. `OptionSpinner.do_key()` wrote whatever
|
|
115
|
+
`answer()` returned straight onto the attribute, and an aborted dialog returns
|
|
116
|
+
`None`, so cancelling blanked the setting and the next read coerced it back to
|
|
117
|
+
`''`. The prompt branch now treats `None` as "no change" and returns without
|
|
118
|
+
touching the attribute (AUDIT.md §3.7).
|
|
119
|
+
- `ConsoleWindow.alert()` now actually scrolls. It used to build a pad sized to
|
|
120
|
+
the whole message but always refresh it at offset 0, so content taller than the
|
|
121
|
+
box was silently unreachable despite the "scrollable if needed" comment. The
|
|
122
|
+
content is now reachable with `j`/`k`, the arrow keys, PageUp/PageDown (and
|
|
123
|
+
Ctrl-B/Ctrl-F/Space) and Home/End, and the box's left border shows `▲`/`▼`
|
|
124
|
+
markers when content extends past the fold. The `_height`/`_width` parameters,
|
|
125
|
+
previously accepted and ignored, now cap the (centred) box; passing `None`
|
|
126
|
+
keeps the previous full-box, 1-cell-margin geometry.
|
|
127
|
+
- `alert()` no longer reaches around the backend seam: it builds its content pad
|
|
128
|
+
through `RenderBackend.newpad()` and drives its own key loop rather than calling
|
|
129
|
+
module-level `curses.newpad()`/`curses.newwin()`/`Textbox`. It therefore runs
|
|
130
|
+
under `HeadlessBackend` and is covered by the new `alert()` tests in
|
|
131
|
+
`tests/test_interactive.py`; the `Textbox` monkeypatch in `tests/test_dialogs.py`
|
|
132
|
+
is gone, along with the now-unused `_textbox_edit_on_enter()` helper (AUDIT.md
|
|
133
|
+
§3.3).
|
|
134
|
+
- `answer()`, `flash()` and the shared render/dialog helpers no longer call the
|
|
135
|
+
process-global `curses.doupdate()`/`curs_set()`/`update_lines_cols()`/
|
|
136
|
+
`is_term_resized()` directly. Four matching methods were added to
|
|
137
|
+
`RenderBackend`, implemented by `CursesBackend` (delegating to curses) and
|
|
138
|
+
`HeadlessBackend` (recording no-ops), so the dialogs are backend-driven end to
|
|
139
|
+
end and the `curs_set`/`doupdate` monkeypatches are gone from `tests/test_dialogs.py`
|
|
140
|
+
(AUDIT.md §3.3).
|
|
141
|
+
- `ConsoleWindow` startup and `Theme.set()` now (re)initialise the same semantic
|
|
142
|
+
colour pairs from the single `Theme.SEMANTIC_PAIRS` constant. Startup previously
|
|
143
|
+
omitted `OLD_SUCCESS`, so it rendered on an uninitialised pair (effectively pair
|
|
144
|
+
0) until the user changed theme. The magic pair id `8` is now the named
|
|
145
|
+
`Theme.DEFAULT_BG`. `tests/test_theme.py` pins the shared list, the historic id
|
|
146
|
+
and that both init paths agree (AUDIT.md §3.9).
|
|
147
|
+
|
|
148
|
+
### Removed
|
|
149
|
+
- `console_window/_selftest.py`, the TTY-only dev self-test. The old
|
|
150
|
+
`python -m console_window._selftest` entry point is gone; the features it
|
|
151
|
+
demonstrated — multi-row picking, a prompt-style option, forced
|
|
152
|
+
`render(redraw=True)`, `Ctrl-C` as an application key, large-body stress and
|
|
153
|
+
`ConsoleWindow.primary_window()` recovery — were folded into `SampleApp.py`,
|
|
154
|
+
which is now covered by headless smoke tests (`tests/test_sample_app.py`).
|
|
155
|
+
- All docstrings from the package, the test suite, the demo and the `runner`
|
|
156
|
+
script (305 in total, across 29 files). Nothing consumed them via `__doc__`,
|
|
157
|
+
`inspect.getdoc()` or `help()` (verified by grep), so runtime behaviour and the
|
|
158
|
+
public API are unchanged; statement order, bodies and the blank-line layout
|
|
159
|
+
around definitions were preserved.
|
|
160
|
+
- The `DWIPE_THEME` environment variable. `Theme.set()` no longer exports it and
|
|
161
|
+
the startup path reads `Theme.get_current()` instead of `os.environ`, making
|
|
162
|
+
`Theme` state the single source of truth. The library no longer namespaces its
|
|
163
|
+
config under a downstream app's name (`dwipe` never read the variable) and no
|
|
164
|
+
longer leaks the theme value into child processes via the inherited
|
|
165
|
+
environment.
|
|
166
|
+
- `console_window/ConsoleWindow.py`, the compatibility facade. Its filename
|
|
167
|
+
shadowed the `ConsoleWindow` class and violated PEP 8 (ruff `N999`). The
|
|
168
|
+
public API is unchanged for package-root imports (`from console_window import
|
|
169
|
+
...`), which is the documented style; only the legacy
|
|
170
|
+
`from console_window.ConsoleWindow import ...` path is gone. The interactive
|
|
171
|
+
self-test moved to `python -m console_window._selftest`.
|
|
172
|
+
|
|
173
|
+
### Fixed
|
|
174
|
+
- Scrolling could not reach non-pickable `DECOR` rows that sit past the last
|
|
175
|
+
pickable row (or before the first one). In pick mode the frame's
|
|
176
|
+
pick-visibility clamp in [`_adjust_scroll_for_abut()`](console_window/window.py:862)
|
|
177
|
+
reset `scroll_pos` back to `pick_pos` every frame, so the SampleApp's `z`
|
|
178
|
+
"stress" mode created 2000 filler rows that were unreachable — the view showed
|
|
179
|
+
the last pickable row plus one screen of filler and the scroll indicator sat
|
|
180
|
+
near the start. Movement is now allowed to scroll past the pick (a new
|
|
181
|
+
`free_scroll` state) while the pick can find no pickable row in the requested
|
|
182
|
+
direction; the clamp is skipped in that state and restored as soon as a real
|
|
183
|
+
pick move resynchronises the view. The blocked-move scroll amount is now the
|
|
184
|
+
*requested* delta rather than a single row, so page keys and `HOME`/`END`
|
|
185
|
+
traverse a `DECOR` block too, and the scroll indicator follows the viewport
|
|
186
|
+
while free-scrolling instead of the parked pick.
|
|
187
|
+
- `SampleApp.py` asked for `head_rows=4`, but the help screen's nav-key block is
|
|
188
|
+
five header rows, so the last line was being clipped in silence. Raised to 8,
|
|
189
|
+
and the pad-sizing comment now records what each pad has to hold. The bug was
|
|
190
|
+
found by the overflow signal above, which this project's own suite now
|
|
191
|
+
enforces (`filterwarnings = ["error::UserWarning"]` in `pyproject.toml`) so
|
|
192
|
+
that neither the sample app nor the library's own helpers can quietly regress
|
|
193
|
+
that way again.
|
|
194
|
+
|
|
195
|
+
## [2.0.1]
|
|
196
|
+
|
|
197
|
+
Internal overhaul — modularization and tests. The public API is unchanged and
|
|
198
|
+
fully backward compatible (`from console_window import ...` and
|
|
199
|
+
`from console_window.ConsoleWindow import ...` both continue to work).
|
|
200
|
+
|
|
201
|
+
### Added
|
|
202
|
+
- Unit-test suite (`tests/`, TTY-free) covering scroll/pick geometry,
|
|
203
|
+
`IncrementalSearchBar` / `InlineConfirmation`, `OptionSpinner` scope
|
|
204
|
+
subtraction, public-API compatibility, the `answer()`/`alert()` dialog fixes,
|
|
205
|
+
`set_demo_mode()` toggle/state handling, `render()`'s retry cap, and the
|
|
206
|
+
non-destructive `flash()` behaviour (`tests/test_flash.py`).
|
|
207
|
+
- `ConsoleWindow.flash_repaint` (repaint the underlying screen when `flash()`
|
|
208
|
+
ends) and `ConsoleWindow.flash_poll_ms` (flash key-poll interval).
|
|
209
|
+
- `console_window.textwidth` — a stdlib-only (`unicodedata`-based) display-cell
|
|
210
|
+
width toolkit (`char_width`, `string_width`, `truncate`, `ljust`/`rjust`,
|
|
211
|
+
`cell_to_index`, `wrap_ranges`/`wrap_position`/`index_at_cell`, `wrap`),
|
|
212
|
+
plus `tests/test_textwidth.py` and `tests/test_widechars.py`.
|
|
213
|
+
- `console_window.geometry` — pure scroll/pick math extracted for testing:
|
|
214
|
+
`cook_abut`, `compute_abut`, `calc_indicator`, `scroll_indicator_row`,
|
|
215
|
+
`scroll_indicator_col`, `count_pickable_rows`, `find_pickable`.
|
|
216
|
+
- `console_window.backend` — a `RenderBackend` abstraction with `CursesBackend`
|
|
217
|
+
(real terminal) and `HeadlessBackend` (in-memory), plus `tests/test_backend.py`.
|
|
218
|
+
- `console_window.__version__`.
|
|
219
|
+
- Explicit `__all__` in the package root and compatibility module.
|
|
220
|
+
- `[tool.pytest.ini_options]` in `pyproject.toml`.
|
|
221
|
+
- GitHub Actions CI (`.github/workflows/ci.yml`) running the TTY-free suite on
|
|
222
|
+
Python 3.8–3.13, plus a Ruff lint job backed by a `[tool.ruff]` config in
|
|
223
|
+
`pyproject.toml`.
|
|
224
|
+
- Type annotations across the public API (every package module now uses
|
|
225
|
+
`from __future__ import annotations`), enforced for the package by Ruff's
|
|
226
|
+
`ANN` rules.
|
|
227
|
+
|
|
228
|
+
### Changed
|
|
229
|
+
- Split the 3,815-line `console_window/ConsoleWindow.py` into focused modules:
|
|
230
|
+
`window.py`, `theme.py`, `context.py`, `opts.py`, `widgets.py`, `spinner.py`,
|
|
231
|
+
`screens.py`, `keys.py`, `signals.py`, `geometry.py`; `ConsoleWindow.py` is now
|
|
232
|
+
a thin compatibility facade.
|
|
233
|
+
- Theme state unified into `Theme._current` (via `Theme.set_active()`); removed
|
|
234
|
+
the split between `_current` and `CURRENT_THEME` that made
|
|
235
|
+
`Theme.get_current()` return `None` after startup.
|
|
236
|
+
- SIGINT handling moved to an explicit `console_window.signals` policy API;
|
|
237
|
+
removed the action-at-a-distance rebinding of module-level functions.
|
|
238
|
+
- `set_demo_mode()` now tests the resulting state rather than the argument.
|
|
239
|
+
- Removed the process-wide `ConsoleWindow.static_scr`/`static_win` singletons:
|
|
240
|
+
live windows are tracked in a weak-reference registry, teardown is per-instance
|
|
241
|
+
(`stop_curses()`), `stop_all()` is the single `atexit` hook, and `Theme.set()`
|
|
242
|
+
broadcasts to every live window via `_apply_default_colors()`. Multiple windows
|
|
243
|
+
may now coexist, and one may be constructed without a terminal by passing
|
|
244
|
+
`backend=HeadlessBackend()` to the constructor.
|
|
245
|
+
- Every package module opts into PEP 563 (`from __future__ import annotations`)
|
|
246
|
+
so the modern `X | None` annotations remain valid on Python 3.8.
|
|
247
|
+
- `add_fancy_header()` regex logic extracted into named helpers.
|
|
248
|
+
|
|
249
|
+
### Fixed
|
|
250
|
+
- `answer()` now records the screen read timeout in effect before the dialog and
|
|
251
|
+
restores that value on every exit path (submit, TAB, ESC, ESC-ESC, too-small
|
|
252
|
+
abort) instead of resetting to the class default `ConsoleWindow.timeout_ms`.
|
|
253
|
+
- `alert()` recomputes `self.rows`/`self.cols` from `getmaxyx()` before drawing,
|
|
254
|
+
so a resize immediately before the call no longer causes a misdraw.
|
|
255
|
+
- `flash()` is now non-destructive and interruptible: it polls for a key
|
|
256
|
+
(`flash_poll_ms`) instead of `time.sleep`ing, so it can be dismissed early —
|
|
257
|
+
and any key pressed is preserved in `pending_keys` for the next `prompt()`
|
|
258
|
+
rather than being swallowed. A resize while the message is up redraws it, and
|
|
259
|
+
on exit the underlying screen is repainted from the pads (`flash_repaint`)
|
|
260
|
+
instead of being left blank; the pre-flash read timeout is restored on every
|
|
261
|
+
exit path.
|
|
262
|
+
- Width arithmetic is now **display-cell aware** (wide CJK/emoji = 2 cells,
|
|
263
|
+
combining marks/ZWJ = 0) throughout the render pipeline: `_add`, `draw`,
|
|
264
|
+
`highlight_picked`/`_draw_line_with_attrs` (highlight columns map to
|
|
265
|
+
code-point indices), `add_fancy_header`, `flash`, `alert`, and the `answer()`
|
|
266
|
+
display/editor (cell-accurate wrapping and cursor placement) — fixing
|
|
267
|
+
misalignment for the wide characters the demo already uses.
|
|
268
|
+
- `render()` now bounds its transient-error retry loop by wall-clock time
|
|
269
|
+
(`ConsoleWindow.render_retry_seconds`, default 2 s) instead of a fixed 128
|
|
270
|
+
iterations, so a reproducible `curses.error` aborts quickly with the
|
|
271
|
+
diagnostic dump rather than freezing the UI for ~20 s.
|
|
272
|
+
- Demo: the settings spinner keys (`v` view mode, `s` sort, `t` theme) now also
|
|
273
|
+
work while the HELP screen is open — they are scoped to include `HELP_ST`.
|
|
274
|
+
(Scoping logic itself was unchanged by the refactor; this was a demo-scope
|
|
275
|
+
issue.)
|
|
276
|
+
- `ScreenStack.__init__(screen_objects=...)` annotated as `dict | None` (fixes the
|
|
277
|
+
implicit-`Optional` finding, Ruff RUF013); removed redundant `int(round(...))`
|
|
278
|
+
casts (RUF046) and trailing whitespace in `widgets.py` (W291); dropped an
|
|
279
|
+
unused local in `tests/test_spinner.py` (F841).
|
|
280
|
+
- Removed retired `put_body()` references (docstring + `__main__` self-test now
|
|
281
|
+
use `add_body()`).
|
|
282
|
+
- `Development Status` classifier aligned with the release; version bumped to
|
|
283
|
+
`2.0.1`.
|
|
284
|
+
|
|
285
|
+
[2.0.1]: https://github.com/joedefen/console-window
|