vir-tui 2.2.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.
- vir_tui-2.2.0/.github/workflows/ci.yml +22 -0
- vir_tui-2.2.0/.github/workflows/publish.yml +45 -0
- vir_tui-2.2.0/.gitignore +2 -0
- vir_tui-2.2.0/AGENTS.md +1 -0
- vir_tui-2.2.0/CLAUDE.md +15 -0
- vir_tui-2.2.0/LICENSE +21 -0
- vir_tui-2.2.0/PKG-INFO +41 -0
- vir_tui-2.2.0/README.md +32 -0
- vir_tui-2.2.0/VERSION +1 -0
- vir_tui-2.2.0/logo.svg +68 -0
- vir_tui-2.2.0/patchnotes.md +27 -0
- vir_tui-2.2.0/pyproject.toml +17 -0
- vir_tui-2.2.0/refactor.py +59 -0
- vir_tui-2.2.0/roadmap.md +36 -0
- vir_tui-2.2.0/spec.md +18 -0
- vir_tui-2.2.0/src/vir_tui/__init__.py +45 -0
- vir_tui-2.2.0/src/vir_tui/core.py +106 -0
- vir_tui-2.2.0/src/vir_tui/menu.py +1020 -0
- vir_tui-2.2.0/tests/test_tui.py +169 -0
- vir_tui-2.2.0/uv.lock +8 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.14"
|
|
19
|
+
- run: pip install . ruff pytest
|
|
20
|
+
- run: ruff check src/ tests/
|
|
21
|
+
- run: ruff format --check src/ tests/
|
|
22
|
+
- run: pytest
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.14"
|
|
16
|
+
|
|
17
|
+
- name: Run test suite
|
|
18
|
+
run: |
|
|
19
|
+
pip install pytest
|
|
20
|
+
PYTHONPATH=src pytest
|
|
21
|
+
|
|
22
|
+
build-and-publish:
|
|
23
|
+
name: Build distribution and publish to PyPI
|
|
24
|
+
needs: test
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
environment:
|
|
27
|
+
name: pypi
|
|
28
|
+
url: https://pypi.org/p/vir-tui
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write # Required for OIDC authentication with PyPI
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
- uses: actions/setup-python@v5
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.14"
|
|
37
|
+
|
|
38
|
+
- name: Install build
|
|
39
|
+
run: python -m pip install --upgrade build
|
|
40
|
+
|
|
41
|
+
- name: Build sdist and wheel
|
|
42
|
+
run: python -m build
|
|
43
|
+
|
|
44
|
+
- name: Publish to PyPI
|
|
45
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
vir_tui-2.2.0/.gitignore
ADDED
vir_tui-2.2.0/AGENTS.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CLAUDE.md
|
vir_tui-2.2.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# vir-tui
|
|
2
|
+
|
|
3
|
+
- **Domain**: Terminal UI rendering for VirInvictus CLI apps.
|
|
4
|
+
- **Dependencies**: `stdlib` only. No dependencies are allowed in `pyproject.toml`.
|
|
5
|
+
- **Formatting**: ANSI codes.
|
|
6
|
+
|
|
7
|
+
Read `spec.md` before making changes.
|
|
8
|
+
|
|
9
|
+
### Consumers
|
|
10
|
+
Any breaking changes to `vir-tui` MUST be cascaded to the following applications that depend on it:
|
|
11
|
+
1. `CalibreQuarry` (tracks `@main`)
|
|
12
|
+
2. `Lattice` (tracks `@main`)
|
|
13
|
+
3. `Bindery` (pins an exact commit by policy — bump the pin deliberately)
|
|
14
|
+
|
|
15
|
+
- This is a generalized library. Do NOT hardcode domain menus. Pass them via `tui_select`.
|
vir_tui-2.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brandon LaRocque
|
|
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.
|
vir_tui-2.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vir-tui
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: A lightweight, terminal UI primitive library for the VirInvictus CLI toolchain.
|
|
5
|
+
Author: Brandon LaRocque
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# vir-tui
|
|
11
|
+
|
|
12
|
+
A lightweight, terminal UI primitive library for the VirInvictus CLI toolchain.
|
|
13
|
+
|
|
14
|
+
Provides a raw TTY event loop, a grid-based menu renderer, robust cross-platform ANSI colors, input prompt lifecycles, and a fallback progress bar wrapper for CLI applications that run headless but offer an interactive terminal interface.
|
|
15
|
+
|
|
16
|
+
Powers [CalibreQuarry](https://github.com/VirInvictus/CalibreQuarry) and [Lattice](https://github.com/VirInvictus/Lattice).
|
|
17
|
+
|
|
18
|
+
`Python · stdlib`
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Menus**: full-screen arrow-key `tui_select` (sections, aliases, letter keys) with an automatic numbered text fallback when curses is unavailable; a scrollable, pannable results pager (`tui_page`) with `/` search and `n`/`N` match jumping.
|
|
23
|
+
- **Progress**: `progress_box()` — a session-screen-aware curses progress box with a tqdm-like API and a pipe-safe text fallback.
|
|
24
|
+
- **Sessions**: `interactive_session()` context manager owning the curses screen lifecycle (open, degrade, close, KeyboardInterrupt cleanup).
|
|
25
|
+
- **Formatters**: consistent `success`, `info`, `warn`, `error` styling across apps.
|
|
26
|
+
- **Prompts**: `ask`, `ask_yn`, `confirm`, `prompt_int`, `prompt_float`, `prompt_out`, `prompt_path`, plus `out_note` for "where did my report go" footers.
|
|
27
|
+
- **Capture**: `run_with_capture` wrapper for redirecting stdout/stderr into a temporary scrolling buffer while a background task runs, rendering a header/footer on top.
|
|
28
|
+
|
|
29
|
+
## Support
|
|
30
|
+
|
|
31
|
+
If vir-tui's useful to you and you'd like to chip in:
|
|
32
|
+
|
|
33
|
+
- liberapay · [liberapay.com/bdkl](https://liberapay.com/bdkl/)
|
|
34
|
+
- bitcoin
|
|
35
|
+
```
|
|
36
|
+
bc1qkge6zr45tzqfwfmvma2ylumt6mg7wlwmhr05yv
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
GPL-3.0-or-later.
|
vir_tui-2.2.0/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# vir-tui
|
|
2
|
+
|
|
3
|
+
A lightweight, terminal UI primitive library for the VirInvictus CLI toolchain.
|
|
4
|
+
|
|
5
|
+
Provides a raw TTY event loop, a grid-based menu renderer, robust cross-platform ANSI colors, input prompt lifecycles, and a fallback progress bar wrapper for CLI applications that run headless but offer an interactive terminal interface.
|
|
6
|
+
|
|
7
|
+
Powers [CalibreQuarry](https://github.com/VirInvictus/CalibreQuarry) and [Lattice](https://github.com/VirInvictus/Lattice).
|
|
8
|
+
|
|
9
|
+
`Python · stdlib`
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Menus**: full-screen arrow-key `tui_select` (sections, aliases, letter keys) with an automatic numbered text fallback when curses is unavailable; a scrollable, pannable results pager (`tui_page`) with `/` search and `n`/`N` match jumping.
|
|
14
|
+
- **Progress**: `progress_box()` — a session-screen-aware curses progress box with a tqdm-like API and a pipe-safe text fallback.
|
|
15
|
+
- **Sessions**: `interactive_session()` context manager owning the curses screen lifecycle (open, degrade, close, KeyboardInterrupt cleanup).
|
|
16
|
+
- **Formatters**: consistent `success`, `info`, `warn`, `error` styling across apps.
|
|
17
|
+
- **Prompts**: `ask`, `ask_yn`, `confirm`, `prompt_int`, `prompt_float`, `prompt_out`, `prompt_path`, plus `out_note` for "where did my report go" footers.
|
|
18
|
+
- **Capture**: `run_with_capture` wrapper for redirecting stdout/stderr into a temporary scrolling buffer while a background task runs, rendering a header/footer on top.
|
|
19
|
+
|
|
20
|
+
## Support
|
|
21
|
+
|
|
22
|
+
If vir-tui's useful to you and you'd like to chip in:
|
|
23
|
+
|
|
24
|
+
- liberapay · [liberapay.com/bdkl](https://liberapay.com/bdkl/)
|
|
25
|
+
- bitcoin
|
|
26
|
+
```
|
|
27
|
+
bc1qkge6zr45tzqfwfmvma2ylumt6mg7wlwmhr05yv
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
GPL-3.0-or-later.
|
vir_tui-2.2.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.2.0
|
vir_tui-2.2.0/logo.svg
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<svg width="100%" viewBox="0 0 680 320" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<style>
|
|
3
|
+
:root {
|
|
4
|
+
--bg-opacity: 0.08;
|
|
5
|
+
--stroke: #8490a0;
|
|
6
|
+
--text-primary: #5a6270;
|
|
7
|
+
--text-secondary: #8490a0;
|
|
8
|
+
--rect-fill: #5a6270;
|
|
9
|
+
}
|
|
10
|
+
@media (prefers-color-scheme: dark) {
|
|
11
|
+
:root {
|
|
12
|
+
--bg-opacity: 0.15;
|
|
13
|
+
--stroke: #a0acbc;
|
|
14
|
+
--text-primary: #d0d7e0;
|
|
15
|
+
--text-secondary: #a0acbc;
|
|
16
|
+
--rect-fill: #d0d7e0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
.stroke-color { stroke: var(--stroke); }
|
|
20
|
+
.fill-color { fill: var(--stroke); fill-opacity: var(--bg-opacity); }
|
|
21
|
+
.text-p { fill: var(--text-primary); }
|
|
22
|
+
.text-s { fill: var(--text-secondary); }
|
|
23
|
+
.rect-f { fill: var(--rect-fill); }
|
|
24
|
+
</style>
|
|
25
|
+
|
|
26
|
+
<rect x="40" y="40" width="600" height="240" fill="none" class="stroke-color" stroke-width="0.5"/>
|
|
27
|
+
|
|
28
|
+
<rect x="40" y="40" width="600" height="32" class="fill-color"/>
|
|
29
|
+
<line x1="40" y1="72" x2="640" y2="72" class="stroke-color" stroke-width="0.5"/>
|
|
30
|
+
|
|
31
|
+
<circle cx="60" cy="56" r="5" fill="none" class="stroke-color" stroke-width="0.5"/>
|
|
32
|
+
<circle cx="78" cy="56" r="5" fill="none" class="stroke-color" stroke-width="0.5"/>
|
|
33
|
+
<circle cx="96" cy="56" r="5" fill="none" class="stroke-color" stroke-width="0.5"/>
|
|
34
|
+
|
|
35
|
+
<text x="340" y="59" text-anchor="middle" font-family="'SF Mono','Cascadia Code','JetBrains Mono','Fira Code',Consolas,'Liberation Mono',Menlo,monospace" font-size="11" class="text-s" font-weight="400">~/Calibre $ cquarry</text>
|
|
36
|
+
|
|
37
|
+
<g opacity="0.06">
|
|
38
|
+
<rect x="80" y="100" width="18" height="24" class="rect-f" rx="1"/>
|
|
39
|
+
<rect x="102" y="96" width="18" height="28" class="rect-f" rx="1"/>
|
|
40
|
+
<rect x="124" y="102" width="18" height="22" class="rect-f" rx="1"/>
|
|
41
|
+
<rect x="146" y="94" width="18" height="30" class="rect-f" rx="1"/>
|
|
42
|
+
<rect x="168" y="98" width="18" height="26" class="rect-f" rx="1"/>
|
|
43
|
+
<rect x="190" y="104" width="18" height="20" class="rect-f" rx="1"/>
|
|
44
|
+
<rect x="212" y="92" width="18" height="32" class="rect-f" rx="1"/>
|
|
45
|
+
<rect x="234" y="100" width="18" height="24" class="rect-f" rx="1"/>
|
|
46
|
+
|
|
47
|
+
<rect x="444" y="98" width="18" height="26" class="rect-f" rx="1"/>
|
|
48
|
+
<rect x="466" y="92" width="18" height="32" class="rect-f" rx="1"/>
|
|
49
|
+
<rect x="488" y="102" width="18" height="22" class="rect-f" rx="1"/>
|
|
50
|
+
<rect x="510" y="96" width="18" height="28" class="rect-f" rx="1"/>
|
|
51
|
+
<rect x="532" y="104" width="18" height="20" class="rect-f" rx="1"/>
|
|
52
|
+
<rect x="554" y="94" width="18" height="30" class="rect-f" rx="1"/>
|
|
53
|
+
<rect x="576" y="100" width="18" height="24" class="rect-f" rx="1"/>
|
|
54
|
+
<rect x="598" y="96" width="18" height="28" class="rect-f" rx="1"/>
|
|
55
|
+
</g>
|
|
56
|
+
|
|
57
|
+
<text x="340" y="155" text-anchor="middle" font-family="'SF Mono','Cascadia Code','JetBrains Mono','Fira Code',Consolas,'Liberation Mono',Menlo,monospace" font-size="48" class="text-p" font-weight="500" letter-spacing="-2">CalibreQuarry</text>
|
|
58
|
+
|
|
59
|
+
<line x1="140" y1="190" x2="540" y2="190" class="stroke-color" stroke-width="0.5"/>
|
|
60
|
+
|
|
61
|
+
<text x="340" y="212" text-anchor="middle" font-family="'SF Mono','Cascadia Code','JetBrains Mono','Fira Code',Consolas,'Liberation Mono',Menlo,monospace" font-size="11" class="text-s" font-weight="400" letter-spacing="3">CATALOG · AUDIT · SERIES · EXPORT</text>
|
|
62
|
+
|
|
63
|
+
<rect x="40" y="252" width="600" height="28" class="fill-color"/>
|
|
64
|
+
<line x1="40" y1="252" x2="640" y2="252" class="stroke-color" stroke-width="0.5"/>
|
|
65
|
+
|
|
66
|
+
<text x="56" y="270" font-family="'SF Mono','Cascadia Code','JetBrains Mono','Fira Code',Consolas,'Liberation Mono',Menlo,monospace" font-size="10" class="text-s" font-weight="400">--catalog --all-wings --stats --audit --recent --series --export --wings</text>
|
|
67
|
+
|
|
68
|
+
</svg>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 2.2.0 (2026-08-27)
|
|
2
|
+
Phase 3 — consumer-driven primitives, from the cross-app TUI survey (CalibreQuarry, Lattice, Bindery).
|
|
3
|
+
- **Feature**: `text_mode()` — public curses-status accessor (True when running without curses), so hosts stop poking the private `_USE_CURSES` global.
|
|
4
|
+
- **Feature**: `progress_box()` / `ProgressBox` — first-class curses progress widget: session-screen-aware, throttled redraws, tqdm-like `update`/`set_description`/`close`, context-manager support, and a plain-text fallback (carriage-returned line on a tty; silent when piped) instead of starting a screen of its own. Replaces Lattice's hand-mirrored `_TUIPbar`. The `CP_*` color-pair constants are now public for hosts drawing their own widgets.
|
|
5
|
+
- **Feature**: `interactive_session()` context manager owning the open-screen / degrade-to-text / close-screen boilerplate; KeyboardInterrupt is re-raised after cleanup so hosts keep mapping it to exit code 130. Adopts the duplicated `interactive_menu()` scaffolding from CalibreQuarry and Lattice.
|
|
6
|
+
- **Feature**: Prompt primitives — `prompt_float(label, default, lo, hi)` with bounded re-asking, `prompt_path(label, default, must_exist)` with existence loops built in, `confirm(label, default, danger=True)` for destructive gates, and public `out_note(path)` (both consumers had copied the private `_out_note` verbatim).
|
|
7
|
+
- **Feature**: Results-pager search — `/` opens a query prompt, `n`/`N` jump to the next/previous case-insensitive match with wrap-around; hints line updated. Matching logic is the pure `_match_lines()` helper (unit-tested).
|
|
8
|
+
- **Tests**: Suite grew from 3 to 13 tests covering the new primitives (degrade path, KI re-raise, prompt validation loops, match wrap semantics, ProgressBox fallback safety).
|
|
9
|
+
- **Consumers**: `Lattice` 4.17.0 (drops its mirrored progress box), `CalibreQuarry` 3.22.0 (adopts the session CM + prompt primitives), `Bindery` 0.19.1 (vir-tui pin bumped deliberately per its policy).
|
|
10
|
+
|
|
11
|
+
# 2.1.0 (2026-08-25)
|
|
12
|
+
- **Feature**: Added `session_screen()` accessor exposing the persistent curses screen, so host apps can route their own progress widgets into the session screen instead of starting their own.
|
|
13
|
+
|
|
14
|
+
# 2.0.0 (2026-08-24)
|
|
15
|
+
- **Breaking**: Gutted hardcoded Lattice/CalibreQuarry domains (`_MAIN_SECTIONS`, `_LIB_SECTIONS`). Consumers must now provide their own tuples to `tui_select`.
|
|
16
|
+
- **Breaking**: Exported public API clean without underscores (e.g. `tui_select`, `ask`, `notify`, `reset_terminal`).
|
|
17
|
+
- **Feature**: `tui_select` now automatically builds text-mode fallback menus dynamically using `aliases` and `letter_keys` kwargs.
|
|
18
|
+
- **Fix**: Reverted `getch()` to `get_wch()` to fix a multibyte character search crash.
|
|
19
|
+
- **Fix**: Enforced `visible_w = max(1, content_w - 4)` in `_tui_page` to prevent slicing crashes on narrow terminals.
|
|
20
|
+
- **Maintenance**: Added `tests/` directory with `pytest` suite for core formatters.
|
|
21
|
+
|
|
22
|
+
# Patch Notes
|
|
23
|
+
|
|
24
|
+
## v1.0.0 (2026-08-23)
|
|
25
|
+
|
|
26
|
+
- **Feature:** Initial extraction from `CalibreQuarry` and `Lattice`.
|
|
27
|
+
- **Feature:** Standalone `GridMenu`, prompt wrappers, and ANSI formatters.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vir-tui"
|
|
7
|
+
version = "2.2.0"
|
|
8
|
+
description = "A lightweight, terminal UI primitive library for the VirInvictus CLI toolchain."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Brandon LaRocque" }
|
|
13
|
+
]
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[tool.ruff]
|
|
17
|
+
lint.extend-ignore = ['F401', 'BLE001', 'S110', 'RUF059', 'I001']
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
menu_path = '/home/bdkl/.gitrepos/vir-tui/src/vir_tui/menu.py'
|
|
5
|
+
with open(menu_path, 'r') as f:
|
|
6
|
+
content = f.read()
|
|
7
|
+
|
|
8
|
+
# 1. Unicode Fix
|
|
9
|
+
content = content.replace("key = stdscr.getch()", "key = stdscr.get_wch()")
|
|
10
|
+
# get_wch returns int for special keys and str for chars.
|
|
11
|
+
# we need to handle that.
|
|
12
|
+
content = content.replace("elif 32 <= key <= 126:", "elif isinstance(key, str) and key.isprintable():")
|
|
13
|
+
|
|
14
|
+
# 2. Narrow Pager Crash Fix
|
|
15
|
+
content = content.replace("visible_w = content_w - 4", "visible_w = max(1, content_w - 4)")
|
|
16
|
+
|
|
17
|
+
# 3. Remove hardcoded sections
|
|
18
|
+
start_idx = content.find("_MAIN_SECTIONS = [")
|
|
19
|
+
end_idx = content.find("def _tui_page", start_idx)
|
|
20
|
+
|
|
21
|
+
if start_idx != -1 and end_idx != -1:
|
|
22
|
+
# Insert new build_fallback and tui_select wrapper
|
|
23
|
+
new_code = """def build_fallback(sections, aliases=None, letter_keys=None):
|
|
24
|
+
aliases = aliases or {}
|
|
25
|
+
letter_keys = letter_keys or {}
|
|
26
|
+
mapping = dict(aliases)
|
|
27
|
+
display = []
|
|
28
|
+
n = 0
|
|
29
|
+
for si, (hdr, items) in enumerate(sections):
|
|
30
|
+
rows = []
|
|
31
|
+
for ii, label in enumerate(items):
|
|
32
|
+
clean = " ".join(label.split())
|
|
33
|
+
letter = letter_keys.get(clean)
|
|
34
|
+
if letter is not None:
|
|
35
|
+
key, target = letter
|
|
36
|
+
rows.append(f"{key}) {clean}")
|
|
37
|
+
mapping[key] = (si, ii) if target == "self" else target
|
|
38
|
+
else:
|
|
39
|
+
n += 1
|
|
40
|
+
rows.append(f"{n}) {clean}")
|
|
41
|
+
mapping[str(n)] = (si, ii)
|
|
42
|
+
display.append((hdr, rows))
|
|
43
|
+
return display, mapping, n
|
|
44
|
+
|
|
45
|
+
def tui_select(title, sections, hints="\u2191\u2193 Navigate \u23ce Select q Quit", aliases=None, letter_keys=None):
|
|
46
|
+
if _USE_CURSES:
|
|
47
|
+
res = _tui_select(title, sections, hints=hints)
|
|
48
|
+
if res != "fallback":
|
|
49
|
+
return res
|
|
50
|
+
# Fallback
|
|
51
|
+
display, mapping, max_n = build_fallback(sections, aliases, letter_keys)
|
|
52
|
+
_box_menu(title, display)
|
|
53
|
+
return _fallback_input(f" Select [1-{max_n}/q]: ", mapping)
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
content = content[:start_idx] + new_code + content[end_idx:]
|
|
57
|
+
|
|
58
|
+
with open(menu_path, 'w') as f:
|
|
59
|
+
f.write(content)
|
vir_tui-2.2.0/roadmap.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
- [x] Extract `GridMenu`, pagers, and prompts from existing CLI apps.
|
|
4
|
+
- [x] Investigate Windows terminal support (`msvcrt`) for raw TTY.
|
|
5
|
+
|
|
6
|
+
### Consumer Updates
|
|
7
|
+
When a roadmap item is completed, ensure the following dependent applications are bumped or verified:
|
|
8
|
+
- [x] `CalibreQuarry`
|
|
9
|
+
- [x] `Lattice`
|
|
10
|
+
- [x] `Bindery` (pins vir-tui to an exact commit by policy — bump the pin deliberately)
|
|
11
|
+
|
|
12
|
+
## Phase 2: Core Generalization & Integration Fixes
|
|
13
|
+
*Based on the post-extraction research report.*
|
|
14
|
+
|
|
15
|
+
- [x] **Decouple Menus**: Remove hardcoded Lattice sections (`_MAIN_SECTIONS`, `_LIB_SECTIONS`) and aliases from `vir-tui`.
|
|
16
|
+
- [x] **Dynamic Fallback**: Generalize `tui_select` to accept sections, aliases, and letter keys, and automatically build the text fallback menu if curses is unavailable, rather than relying on static Lattice maps.
|
|
17
|
+
- [x] **Unicode Prompt Fix**: Revert `getch()` to `get_wch()` in `_tui_prompt_str` so multibyte characters work again (critical for CalibreQuarry search).
|
|
18
|
+
- [x] **Narrow Pager Crash**: Enforce `visible_w = max(1, content_w - 4)` in `_tui_page` to prevent slicing errors on small windows.
|
|
19
|
+
- [x] **API Standardization**: Clean up `__init__.py` to export public (non-underscored) methods, remove the shadowed `prompts.py` file or merge it cleanly, and ensure consumers aren't relying on private methods like `_Cancelled`.
|
|
20
|
+
- [x] **Test Coverage**: Write `pytest` coverage for formatters, text fallback mapping logic, and pager geometry calculations in the currently empty `tests/` directory.
|
|
21
|
+
|
|
22
|
+
### Consumer Integration (Post-Phase 2)
|
|
23
|
+
- [x] **Lattice**: Remove the duplicate ~1,267-line `tui.py` in Lattice's codebase and properly delegate to `vir-tui`'s generalized `tui_select`.
|
|
24
|
+
- [x] **CalibreQuarry**: Update imports to use the public `vir-tui` API and pass its aliases/keys into the new generalized `tui_select` to restore its text fallback mode.
|
|
25
|
+
|
|
26
|
+
## Phase 3: Consumer-Driven Primitives
|
|
27
|
+
*From the 2026-08 cross-app TUI survey (CalibreQuarry, Lattice, Bindery): what hosts still hand-roll, duplicate, or mirror.*
|
|
28
|
+
|
|
29
|
+
- [x] **ProgressBox**: First-class curses progress widget (`progress_box()` / `ProgressBox`) — session-screen-aware, throttled redraws, tqdm-like `update`/`set_description`/`close`, context-manager support, plain-text fallback without a session. Lattice was mirroring `_TUI_BOX_W` and the `_CP_FRAME`/`_CP_HEADER` pair ids by hand and reimplementing the whole box in `lattice/utils.py`, which breaks silently whenever vir-tui restyles. The `CP_*` color-pair constants are now public for advanced hosts.
|
|
30
|
+
- [x] **interactive_session()**: Context manager owning the open-screen / degrade-to-text / KeyboardInterrupt / close-screen boilerplate that CalibreQuarry and Lattice each duplicated in `interactive_menu()`.
|
|
31
|
+
- [x] **Prompt primitives**: `prompt_float(label, default, lo, hi)` (CalibreQuarry hand-rolled its rating loop; Lattice loops tool/threshold choices), `prompt_path(label, default, must_exist)` (both apps hand-rolled path-existence loops), `confirm(label, default, danger)` (CalibreQuarry's double `ask_yn` destructive gates), and public `out_note(path)` (both consumers duplicated vir-tui's private `_out_note` verbatim).
|
|
32
|
+
- [x] **Pager search**: `/` opens a query prompt and `n`/`N` jump to the next/previous match (case-insensitive, wrapping) in `tui_page` — long audit/catalog reports are the primary artifact of every host.
|
|
33
|
+
- [ ] **Mouse support**: click-to-select in `tui_select`, scroll-wheel paging in `tui_page` (needs `getmask`/`BUTTON*` plumbing and fallback no-ops).
|
|
34
|
+
- [ ] **Type-to-filter**: incremental narrowing in `tui_select` for menus with 15+ items (CalibreQuarry's main menu crossed that line in 3.21.0).
|
|
35
|
+
- [ ] **Theme overrides**: let hosts remap color pairs or box glyphs per app instead of mirroring ids (only needed once a host actually wants a distinct look).
|
|
36
|
+
|
vir_tui-2.2.0/spec.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# vir-tui Specification
|
|
2
|
+
|
|
3
|
+
1. **Domain**: Terminal UI rendering and input capturing for VirInvictus Python CLI applications.
|
|
4
|
+
2. **Dependencies**: `stdlib` only. If `tqdm` is available in the consumer's environment, `vir_tui` re-exports it; otherwise, it exports a minimal stub.
|
|
5
|
+
3. **Architecture**:
|
|
6
|
+
- `core.py`: ANSI state logic, color formats, `tqdm` handling (the real tqdm is re-exported when installed, a styled fallback otherwise).
|
|
7
|
+
- `menu.py`: everything interactive —
|
|
8
|
+
- the curses arrow-key menu (`tui_select`, sections/aliases/letter keys, auto-generated typed-input fallback via `build_fallback`),
|
|
9
|
+
- the scrollable, pannable results pager (`tui_page`) with `/` search and `n`/`N` match jumping (pure `_match_lines` helper),
|
|
10
|
+
- boxed prompts (`ask`, `ask_yn`, `confirm`, `prompt_int`, `prompt_float`, `prompt_out`, `prompt_path`) and the `out_note` report footer,
|
|
11
|
+
- the progress widget (`progress_box()` / `ProgressBox`) drawing into the session screen with a pipe-safe text fallback,
|
|
12
|
+
- the session lifecycle (`open_screen`/`close_screen`, `interactive_session`, `session_screen`, `text_mode`),
|
|
13
|
+
- `run_with_capture` for paging a mode's captured stdout/stderr.
|
|
14
|
+
- Public style constants (`CP_FRAME` … `CP_HINT`) let hosts render their own widgets into the session screen without mirroring private ids.
|
|
15
|
+
|
|
16
|
+
4. **Guarantees**:
|
|
17
|
+
- Must fail gracefully and degrade if `sys.stdout` is not a TTY: every widget has a plain-text fallback (menus become numbered typed lists, the pager prints, progress stays silent when piped).
|
|
18
|
+
- Generic by contract: no host-domain menus or strings; hosts pass their own sections/aliases/letter keys to `tui_select`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from .core import (
|
|
2
|
+
info,
|
|
3
|
+
success,
|
|
4
|
+
warn,
|
|
5
|
+
error,
|
|
6
|
+
dry_run,
|
|
7
|
+
print_header,
|
|
8
|
+
print_summary,
|
|
9
|
+
color,
|
|
10
|
+
tqdm,
|
|
11
|
+
)
|
|
12
|
+
from .menu import (
|
|
13
|
+
CP_FRAME,
|
|
14
|
+
CP_HEADER,
|
|
15
|
+
CP_HINT,
|
|
16
|
+
CP_ITEM,
|
|
17
|
+
CP_SELECTED,
|
|
18
|
+
CP_TITLE,
|
|
19
|
+
ProgressBox,
|
|
20
|
+
confirm,
|
|
21
|
+
interactive_session,
|
|
22
|
+
out_note,
|
|
23
|
+
progress_box,
|
|
24
|
+
prompt_float,
|
|
25
|
+
prompt_path,
|
|
26
|
+
tui_select,
|
|
27
|
+
build_fallback,
|
|
28
|
+
reset_terminal,
|
|
29
|
+
open_screen,
|
|
30
|
+
close_screen,
|
|
31
|
+
session_screen,
|
|
32
|
+
text_mode,
|
|
33
|
+
ask,
|
|
34
|
+
ask_yn,
|
|
35
|
+
prompt_int,
|
|
36
|
+
prompt_out,
|
|
37
|
+
run_with_capture,
|
|
38
|
+
CancelledError,
|
|
39
|
+
notify,
|
|
40
|
+
tui_page,
|
|
41
|
+
box_menu,
|
|
42
|
+
fallback_input,
|
|
43
|
+
_Cancelled, # for backwards compatibility
|
|
44
|
+
capture_output,
|
|
45
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
except ImportError:
|
|
7
|
+
# Minimal fallback if tqdm is missing
|
|
8
|
+
class tqdm:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
iterable=None,
|
|
12
|
+
desc=None,
|
|
13
|
+
disable=False,
|
|
14
|
+
total=None,
|
|
15
|
+
unit="it",
|
|
16
|
+
leave=True,
|
|
17
|
+
**kwargs,
|
|
18
|
+
):
|
|
19
|
+
self.iterable = iterable
|
|
20
|
+
self.desc = desc
|
|
21
|
+
self.disable = disable
|
|
22
|
+
self.total = total
|
|
23
|
+
self.unit = unit
|
|
24
|
+
self.leave = leave
|
|
25
|
+
self.n = 0
|
|
26
|
+
if not self.disable and self.desc:
|
|
27
|
+
print(f"{self.desc}...", file=sys.stderr)
|
|
28
|
+
|
|
29
|
+
def __iter__(self):
|
|
30
|
+
if self.iterable is None:
|
|
31
|
+
return self
|
|
32
|
+
for item in self.iterable:
|
|
33
|
+
yield item
|
|
34
|
+
self.update(1)
|
|
35
|
+
|
|
36
|
+
def update(self, n=1):
|
|
37
|
+
self.n += n
|
|
38
|
+
|
|
39
|
+
def close(self):
|
|
40
|
+
if not self.disable and self.leave and self.total is not None:
|
|
41
|
+
print(f"Finished {self.n}/{self.total} {self.unit}", file=sys.stderr)
|
|
42
|
+
|
|
43
|
+
def set_description(self, desc):
|
|
44
|
+
self.desc = desc
|
|
45
|
+
|
|
46
|
+
def set_postfix(self, **kwargs):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def write(s, file=None, end="\n"):
|
|
51
|
+
print(s, file=file or sys.stdout, end=end)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ANSI Colors
|
|
55
|
+
RED = "\033[31m"
|
|
56
|
+
GREEN = "\033[32m"
|
|
57
|
+
YELLOW = "\033[33m"
|
|
58
|
+
CYAN = "\033[36m"
|
|
59
|
+
MAGENTA = "\033[35m"
|
|
60
|
+
BOLD = "\033[1m"
|
|
61
|
+
DIM = "\033[2m"
|
|
62
|
+
RESET = "\033[0m"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _use_color() -> bool:
|
|
66
|
+
return "NO_COLOR" not in os.environ and sys.stdout.isatty()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def color(text: str, code: str) -> str:
|
|
70
|
+
return f"{code}{text}{RESET}" if _use_color() else text
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def info(msg: str) -> str:
|
|
74
|
+
return color(f"ℹ {msg}", CYAN)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def success(msg: str) -> str:
|
|
78
|
+
return color(f"✓ {msg}", GREEN)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def warn(msg: str) -> str:
|
|
82
|
+
return color(f"⚠ {msg}", YELLOW)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def error(msg: str) -> str:
|
|
86
|
+
return color(f"✗ {msg}", RED)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def dry_run(msg: str) -> str:
|
|
90
|
+
return f"{color('[DRY]', YELLOW)} {msg}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def print_header(title: str) -> None:
|
|
94
|
+
tqdm.write(color(f"\n{'=' * 60}", BOLD))
|
|
95
|
+
tqdm.write(color(f"{title}", BOLD + CYAN))
|
|
96
|
+
tqdm.write(color(f"{'=' * 60}", BOLD))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def print_summary(stats: dict) -> None:
|
|
100
|
+
tqdm.write(color("\n--- SUMMARY ---", BOLD))
|
|
101
|
+
for k, v in stats.items():
|
|
102
|
+
if isinstance(v, int) and v > 0:
|
|
103
|
+
tqdm.write(f" {k}: {color(str(v), GREEN)}")
|
|
104
|
+
else:
|
|
105
|
+
tqdm.write(f" {k}: {v}")
|
|
106
|
+
tqdm.write(color("===============\n", BOLD))
|