lib-cli-exit-tools 1.0.2__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/CHANGELOG.md +7 -3
  2. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/CONTRIBUTING.md +1 -1
  3. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/Makefile +3 -10
  4. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/PKG-INFO +4 -7
  5. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/README.md +3 -6
  6. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/codecov.yml +1 -1
  7. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/packaging/README.md +2 -2
  8. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/packaging/brew/Formula/lib-cli-exit-tools.rb +1 -1
  9. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/packaging/conda/recipe/meta.yaml +1 -1
  10. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/packaging/nix/flake.nix +2 -2
  11. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/pyproject.toml +1 -2
  12. lib_cli_exit_tools-1.0.3/qlty.toml +2 -0
  13. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/_utils.py +2 -2
  14. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/bump.py +1 -1
  15. {lib_cli_exit_tools-1.0.2/tools → lib_cli_exit_tools-1.0.3/scripts}/bump_version.py +1 -1
  16. lib_cli_exit_tools-1.0.2/scripts/menu_tui.py → lib_cli_exit_tools-1.0.3/scripts/menu.py +41 -58
  17. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/test.py +24 -11
  18. lib_cli_exit_tools-1.0.2/docs/textual/README.md +0 -109
  19. lib_cli_exit_tools-1.0.2/docs/textual/snippets/actions_bindings.py +0 -24
  20. lib_cli_exit_tools-1.0.2/docs/textual/snippets/listview_basic.py +0 -20
  21. lib_cli_exit_tools-1.0.2/docs/textual/snippets/mount_await.py +0 -21
  22. lib_cli_exit_tools-1.0.2/docs/textual/snippets/richlog_stream.py +0 -56
  23. lib_cli_exit_tools-1.0.2/scripts/menu.py +0 -310
  24. lib_cli_exit_tools-1.0.2/scripts/menu_cli.py +0 -136
  25. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/.env.example +0 -0
  26. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/.github/workflows/ci.yml +0 -0
  27. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/.github/workflows/codeql.yml +0 -0
  28. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/.github/workflows/release.yml +0 -0
  29. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/.gitignore +0 -0
  30. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/AGENTS.md +0 -0
  31. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/LICENSE +0 -0
  32. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/notebooks/Quickstart.ipynb +0 -0
  33. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/__init__.py +0 -0
  34. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/build.py +0 -0
  35. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/bump_major.py +0 -0
  36. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/bump_minor.py +0 -0
  37. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/bump_patch.py +0 -0
  38. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/clean.py +0 -0
  39. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/dev.py +0 -0
  40. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/install.py +0 -0
  41. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/push.py +0 -0
  42. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/release.py +0 -0
  43. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/run_cli.py +0 -0
  44. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/scripts/version_current.py +0 -0
  45. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/__init__.py +0 -0
  46. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/__init__conf__.py +0 -0
  47. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/__main__.py +0 -0
  48. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/cli.py +0 -0
  49. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/lib_cli_exit_tools.py +0 -0
  50. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/src/lib_cli_exit_tools/py.typed +0 -0
  51. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/tests/test_cli.py +0 -0
  52. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/tests/test_cli_extra.py +0 -0
  53. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/tests/test_exit_tools.py +0 -0
  54. {lib_cli_exit_tools-1.0.2 → lib_cli_exit_tools-1.0.3}/tests/test_lib_extra.py +0 -0
@@ -1,12 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.3] - 2025-09-16
4
+
5
+ - added make menu
6
+
3
7
  ## [1.0.2] - 2025-09-15
4
8
 
5
- - _Describe changes here._
9
+ - minor internals
6
10
 
7
11
  ## [1.0.1] - 2025-09-15
8
12
 
9
- - _Describe changes here._
13
+ - minor internals
10
14
 
11
15
  ## [1.0.0] - 2025-09-15
12
16
 
@@ -16,7 +20,7 @@
16
20
 
17
21
  ### Added
18
22
  - CI job to execute Jupyter notebook on Python 3.10 (installs ipykernel, normalizes notebook IDs) and a tag‑time packaging‑consistency check.
19
- - Packaging auto‑sync: `make test`, `make push`, and the bump script now align Conda/Brew/Nix files to `pyproject.toml` version, `requires-python`, and runtime deps. Homebrew resources are updated (URL + sha256) using PyPI metadata. A sync‑only mode is available via `python tools/bump_version.py --sync-packaging`.
23
+ - Packaging auto‑sync: `make test`, `make push`, and the bump script now align Conda/Brew/Nix files to `pyproject.toml` version, `requires-python`, and runtime deps. Homebrew resources are updated (URL + sha256) using PyPI metadata. A sync‑only mode is available via `python scripts/bump_version.py --sync-packaging`.
20
24
  - Tests covering SystemExit variants, tolerant output printing, English CLI messages, and Windows mapping via monkeypatched `os.name`.
21
25
 
22
26
  ### Changed
@@ -28,7 +28,7 @@ Thanks for helping improve lib_cli_exit_tools! This guide keeps changes small, s
28
28
  - Homebrew: ensures a `resource "<dep>"` for each dep; if pinned, uses that version; otherwise pins to the latest PyPI release, updating `url` + `sha256`.
29
29
  - Nix: updates the package version, `pkgs.pythonXYZPackages`/`pkgs.pythonXYZ` to match the min Python, and `propagatedBuildInputs` to include all deps as `pypkgs.<name>`.
30
30
  - Network note: Homebrew resource updates fetch PyPI metadata; if offline, those updates may be skipped.
31
- - If packaging drifts, run: `python tools/bump_version.py --sync-packaging`.
31
+ - If packaging drifts, run: `python scripts/bump_version.py --sync-packaging`.
32
32
  - CI enforces consistency on tags via a `packaging-consistency` job.
33
33
 
34
34
  ## 5) Tests & Style
@@ -1,6 +1,5 @@
1
1
  SHELL := /bin/bash
2
2
  .ONESHELL:
3
-
4
3
  # Config
5
4
  PY ?= python3
6
5
  PIP ?= pip
@@ -18,7 +17,7 @@ FAIL_UNDER ?= 80
18
17
  # - off : never run coverage
19
18
  COVERAGE ?= on
20
19
 
21
- .PHONY: help install dev test run clean build push release version-current bump bump-patch bump-minor bump-major menu menu-cli menu-tui _bootstrap-dev
20
+ .PHONY: help install dev test run clean build push release version-current bump bump-patch bump-minor bump-major menu _bootstrap-dev
22
21
 
23
22
  help: ## Show help
24
23
  @grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
@@ -95,11 +94,5 @@ build: ## Build wheel/sdist and attempt conda, brew, and nix builds (auto-instal
95
94
  release: ## Create and push tag vX.Y.Z from pyproject, create GitHub release (if gh present), then sync packaging
96
95
  $(PY) scripts/release.py --remote=$(REMOTE)
97
96
 
98
- menu: ## Interactive menu (CLI prompts by default)
99
- $(PY) -u scripts/menu_cli.py < /dev/tty > /dev/tty 2>&1
100
-
101
- menu-cli: ## Force simple CLI prompt menu (no TUI)
102
- $(PY) -u scripts/menu_cli.py < /dev/tty > /dev/tty 2>&1
103
-
104
- menu-tui: ## Force Textual TUI menu (may not work on limited terminals)
105
- $(PY) -u scripts/menu_tui.py < /dev/tty > /dev/tty 2>&1
97
+ menu: ## Interactive TUI menu
98
+ $(PY) -u scripts/menu.py < /dev/tty > /dev/tty 2>&1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lib_cli_exit_tools
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: CLI exit handling helpers: clean signals, exit codes, and error printing
5
5
  Project-URL: Homepage, https://github.com/bitranox/lib_cli_exit_tools
6
6
  Project-URL: Repository, https://github.com/bitranox/lib_cli_exit_tools.git
@@ -144,8 +144,6 @@ pip install .
144
144
  | `push` | Commit all changes once and push to GitHub (no CI monitoring) |
145
145
  | `build` | Build wheel/sdist and attempt conda, brew, and nix builds (auto-installs tools if missing) |
146
146
  | `menu` | Interactive TUI to run targets and edit parameters (requires dev dep: textual) |
147
- | `menu-cli` | Force simple prompt menu (no TUI) |
148
- | `menu-tui` | Force Textual TUI (may not work in limited terminals) |
149
147
 
150
148
  #### Target Parameters (env vars)
151
149
 
@@ -162,6 +160,7 @@ pip install .
162
160
  - `test`
163
161
  - `COVERAGE=on|auto|off` (default: `on`) — controls pytest coverage run and Codecov upload
164
162
  - `SKIP_BOOTSTRAP=1` — skip auto-install of dev tools if missing
163
+ - `TEST_VERBOSE=1` — echo each command executed by the test harness
165
164
  - Also respects `CODECOV_TOKEN` when needed for uploads
166
165
 
167
166
  - `run`
@@ -195,7 +194,7 @@ pip install .
195
194
 
196
195
  ### Interactive Menu (Textual)
197
196
 
198
- `make menu` launches a colorful terminal UI (powered by `textual`) to browse targets, edit parameters, and run them with live output. If the terminal doesn’t support a full TUI or `textual` can’t load, it falls back to a simple prompt menu.
197
+ `make menu` launches a colorful terminal UI (powered by `textual`) to browse targets, edit parameters, and run them with live output.
199
198
 
200
199
  Install dev extras if you haven’t:
201
200
 
@@ -207,8 +206,6 @@ Run the menu:
207
206
 
208
207
  ```bash
209
208
  make menu
210
- make menu-cli # always use simple prompt menu
211
- make menu-tui # force TUI (if your terminal supports it)
212
209
  ```
213
210
 
214
211
  #### Target Details
@@ -288,7 +285,7 @@ COVERAGE=on make test # force coverage and generate coverage.xml/codecov.
288
285
  - Homebrew: updates the source URL tag to `vX.Y.Z` and sets `depends_on "python@X.Y"` to match `requires-python`.
289
286
  - Nix: updates the package `version`, example `rev = "vX.Y.Z"`, and switches `pkgs.pythonXYZPackages` / `pkgs.pythonXYZ` to match the minimum Python version from `requires-python`.
290
287
 
291
- - To run just the sync without bumping versions: `python tools/bump_version.py --sync-packaging`.
288
+ - To run just the sync without bumping versions: `python scripts/bump_version.py --sync-packaging`.
292
289
 
293
290
  - On release tags (`v*.*.*`), CI validates that packaging files are consistent with `pyproject.toml` and will fail if they drift.
294
291
 
@@ -114,8 +114,6 @@ pip install .
114
114
  | `push` | Commit all changes once and push to GitHub (no CI monitoring) |
115
115
  | `build` | Build wheel/sdist and attempt conda, brew, and nix builds (auto-installs tools if missing) |
116
116
  | `menu` | Interactive TUI to run targets and edit parameters (requires dev dep: textual) |
117
- | `menu-cli` | Force simple prompt menu (no TUI) |
118
- | `menu-tui` | Force Textual TUI (may not work in limited terminals) |
119
117
 
120
118
  #### Target Parameters (env vars)
121
119
 
@@ -132,6 +130,7 @@ pip install .
132
130
  - `test`
133
131
  - `COVERAGE=on|auto|off` (default: `on`) — controls pytest coverage run and Codecov upload
134
132
  - `SKIP_BOOTSTRAP=1` — skip auto-install of dev tools if missing
133
+ - `TEST_VERBOSE=1` — echo each command executed by the test harness
135
134
  - Also respects `CODECOV_TOKEN` when needed for uploads
136
135
 
137
136
  - `run`
@@ -165,7 +164,7 @@ pip install .
165
164
 
166
165
  ### Interactive Menu (Textual)
167
166
 
168
- `make menu` launches a colorful terminal UI (powered by `textual`) to browse targets, edit parameters, and run them with live output. If the terminal doesn’t support a full TUI or `textual` can’t load, it falls back to a simple prompt menu.
167
+ `make menu` launches a colorful terminal UI (powered by `textual`) to browse targets, edit parameters, and run them with live output.
169
168
 
170
169
  Install dev extras if you haven’t:
171
170
 
@@ -177,8 +176,6 @@ Run the menu:
177
176
 
178
177
  ```bash
179
178
  make menu
180
- make menu-cli # always use simple prompt menu
181
- make menu-tui # force TUI (if your terminal supports it)
182
179
  ```
183
180
 
184
181
  #### Target Details
@@ -258,7 +255,7 @@ COVERAGE=on make test # force coverage and generate coverage.xml/codecov.
258
255
  - Homebrew: updates the source URL tag to `vX.Y.Z` and sets `depends_on "python@X.Y"` to match `requires-python`.
259
256
  - Nix: updates the package `version`, example `rev = "vX.Y.Z"`, and switches `pkgs.pythonXYZPackages` / `pkgs.pythonXYZ` to match the minimum Python version from `requires-python`.
260
257
 
261
- - To run just the sync without bumping versions: `python tools/bump_version.py --sync-packaging`.
258
+ - To run just the sync without bumping versions: `python scripts/bump_version.py --sync-packaging`.
262
259
 
263
260
  - On release tags (`v*.*.*`), CI validates that packaging files are consistent with `pyproject.toml` and will fail if they drift.
264
261
 
@@ -20,9 +20,9 @@ ignore:
20
20
  - "tests/**"
21
21
  - "packaging/**"
22
22
  - "**/__init__conf__.py"
23
+ - "scripts/**"
23
24
 
24
25
  comment:
25
26
  layout: "reach, diff, flags, files"
26
27
  behavior: default
27
28
  require_changes: false
28
-
@@ -9,13 +9,13 @@ This directory contains packaging files and an automated sync flow so they stay
9
9
  - Or semantic part: `make bump PART=patch|minor|major`
10
10
  - To only synchronize packaging to the current `pyproject.toml` without changing versions:
11
11
  ```bash
12
- python3 tools/bump_version.py --sync-packaging
12
+ python3 scripts/bump_version.py --sync-packaging
13
13
  ```
14
14
  - `make test` runs the same sync step up-front to keep packaging up to date.
15
15
 
16
16
  ## What auto-sync updates
17
17
 
18
- The script `tools/bump_version.py` updates the following when network access is available:
18
+ The script `scripts/bump_version.py` updates the following when network access is available:
19
19
 
20
20
  1) Conda recipe (`packaging/conda/recipe/meta.yaml`)
21
21
  - Sets Jinja version to the current `pyproject.toml` version.
@@ -3,7 +3,7 @@ class LibCliExitTools < Formula
3
3
 
4
4
  desc "CLI exit handling helpers: clean signals, exit codes, and error printing"
5
5
  homepage "https://github.com/bitranox/lib_cli_exit_tools"
6
- url "https://github.com/bitranox/lib_cli_exit_tools/archive/refs/tags/v1.0.2.tar.gz"
6
+ url "https://github.com/bitranox/lib_cli_exit_tools/archive/refs/tags/v1.0.3.tar.gz"
7
7
  k3cc316e7d0b09dd79e617cb3e91b14ee414b7e6695b108a326d9d3ccd08dd9"
8
8
  license "MIT"
9
9
 
@@ -1,5 +1,5 @@
1
1
  {% set name = "lib_cli_exit_tools" %}
2
- {% set version = "1.0.2" %}
2
+ {% set version = "1.0.3" %}
3
3
 
4
4
  package:
5
5
  name: {{ name|lower }}
@@ -35,7 +35,7 @@
35
35
  {
36
36
  packages.default = pypkgs.buildPythonPackage {
37
37
  pname = "lib_cli_exit_tools";
38
- version = "1.0.2";
38
+ version = "1.0.3";
39
39
  pyproject = true;
40
40
  # Build from the repository root (two levels up from packaging/nix)
41
41
  src = ../..;
@@ -43,7 +43,7 @@
43
43
  # src = pkgs.fetchFromGitHub {
44
44
  # owner = "bitranox";
45
45
  # repo = "lib_cli_exit_tools";
46
- # rev = "v1.0.2";
46
+ # rev = "v1.0.3";
47
47
  # sha256 = "<fill-me>";
48
48
  # };
49
49
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lib_cli_exit_tools"
3
- version = "1.0.2"
3
+ version = "1.0.3"
4
4
  description = "CLI exit handling helpers: clean signals, exit codes, and error printing"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -62,7 +62,6 @@ target-version = "py313"
62
62
  typeCheckingMode = "strict"
63
63
  exclude = [
64
64
  "scripts/menu.py",
65
- "docs",
66
65
  ]
67
66
 
68
67
  [tool.pytest.ini_options]
@@ -0,0 +1,2 @@
1
+ [analysis]
2
+ ignore = ["scripts/"]
@@ -49,7 +49,7 @@ def cmd_exists(name: str) -> bool:
49
49
 
50
50
  def read_version_from_pyproject(pyproject: Path = Path("pyproject.toml")) -> str:
51
51
  try:
52
- import tomllib # pyright: ignore[reportMissingImports]
52
+ import tomllib # pyright: ignore[reportMissingImports, reportMissingTypeStubs]
53
53
 
54
54
  data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
55
55
  version = str(data.get("project", {}).get("version", ""))
@@ -108,7 +108,7 @@ def gh_release_edit(tag: str, title: str, body: str) -> None:
108
108
 
109
109
 
110
110
  def sync_packaging() -> None:
111
- run([sys.executable, "tools/bump_version.py", "--sync-packaging"], check=False)
111
+ run([sys.executable, "scripts/bump_version.py", "--sync-packaging"], check=False)
112
112
 
113
113
 
114
114
  def bootstrap_dev() -> None:
@@ -14,7 +14,7 @@ from scripts._utils import run # noqa: E402
14
14
  @click.option("--pyproject", type=click.Path(path_type=Path), default=Path("pyproject.toml"))
15
15
  @click.option("--changelog", type=click.Path(path_type=Path), default=Path("CHANGELOG.md"))
16
16
  def main(version_: str | None, part: str | None, pyproject: Path, changelog: Path) -> None:
17
- args = [sys.executable, "tools/bump_version.py"]
17
+ args = [sys.executable, "scripts/bump_version.py"]
18
18
  if version_:
19
19
  args += ["--version", version_]
20
20
  else:
@@ -11,7 +11,7 @@ import hashlib
11
11
 
12
12
  # Optional TOML parser for Python < 3.11 support during type checking
13
13
  try: # pragma: no cover - import resolution
14
- import tomllib as _tomllib # type: ignore[attr-defined]
14
+ import tomllib as _tomllib # type: ignore
15
15
  except Exception: # pragma: no cover - best-effort fallback
16
16
  try:
17
17
  import tomli as _tomllib # type: ignore
@@ -4,13 +4,13 @@ import asyncio
4
4
  import os
5
5
  import signal
6
6
  from dataclasses import dataclass
7
- from typing import Callable, Dict, Optional
7
+ from typing import TYPE_CHECKING, Callable, Dict, Optional
8
8
 
9
9
  import contextlib
10
10
 
11
11
  from rich.text import Text
12
12
 
13
- try:
13
+ if TYPE_CHECKING:
14
14
  from textual import events
15
15
  from textual.app import App, ComposeResult
16
16
  from textual.containers import Container, Horizontal, Vertical
@@ -27,13 +27,31 @@ try:
27
27
  Select,
28
28
  Static,
29
29
  )
30
- except Exception as exc: # pragma: no cover - textual import errors surface clearly
31
- import sys
32
- import traceback
30
+ else: # pragma: no cover
31
+ try:
32
+ from textual import events
33
+ from textual.app import App, ComposeResult
34
+ from textual.containers import Container, Horizontal, Vertical
35
+ from textual.reactive import reactive
36
+ from textual.screen import Screen
37
+ from textual.widgets import (
38
+ Button,
39
+ Footer,
40
+ Input,
41
+ Label,
42
+ ListItem,
43
+ ListView,
44
+ RichLog,
45
+ Select,
46
+ Static,
47
+ )
48
+ except Exception as exc: # pragma: no cover - textual import errors surface clearly
49
+ import sys
50
+ import traceback
33
51
 
34
- print("[menu-tui] Failed to import Textual components:", file=sys.stderr)
35
- traceback.print_exc()
36
- raise SystemExit("textual is not installed or incompatible in this environment. Install dev extras: pip install -e .[dev]") from exc
52
+ print("[menu-tui] Failed to import Textual components:", file=sys.stderr)
53
+ traceback.print_exc()
54
+ raise SystemExit("textual is not installed or incompatible in this environment. Install dev extras: pip install -e .[dev]") from exc
37
55
 
38
56
 
39
57
  @dataclass(slots=True)
@@ -84,6 +102,12 @@ def _build_targets() -> list[Target]:
84
102
  default=_env_default("SKIP_BOOTSTRAP", "0"),
85
103
  choices=["0", "1"],
86
104
  ),
105
+ Param(
106
+ "TEST_VERBOSE",
107
+ "Verbose test runner output (1=yes, 0=no)",
108
+ default=_env_default("TEST_VERBOSE", "0"),
109
+ choices=["0", "1"],
110
+ ),
87
111
  ],
88
112
  ),
89
113
  Target("run", "Run CLI (shows --help)", []),
@@ -327,7 +351,7 @@ class ParamScreen(Screen[Dict[str, str] | None]):
327
351
  self._preset = preset
328
352
  self._inputs: Dict[str, Input | Select[str]] = {}
329
353
  self._error: Static | None = None
330
- self._buttons: list[Button] = []
354
+ self._ok_button: Button | None = None
331
355
 
332
356
  def compose(self) -> ComposeResult: # type: ignore[override]
333
357
  with Container(id="backdrop"):
@@ -354,38 +378,16 @@ class ParamScreen(Screen[Dict[str, str] | None]):
354
378
  yield widget
355
379
  with Horizontal(id="buttons"):
356
380
  ok_btn = Button("OK", id="ok-btn")
357
- cancel_btn = Button("Cancel", id="cancel-btn")
358
- self._buttons = [ok_btn, cancel_btn]
381
+ self._ok_button = ok_btn
359
382
  yield ok_btn
360
- yield cancel_btn
361
383
  yield Footer()
362
384
 
363
385
  def on_mount(self) -> None: # type: ignore[override]
364
- self._focus_first_input()
386
+ self._focus_ok_button()
365
387
 
366
388
  def on_button_pressed(self, event: Button.Pressed) -> None: # type: ignore[override]
367
389
  if event.button.id == "ok-btn":
368
390
  self._submit()
369
- elif event.button.id == "cancel-btn":
370
- self.action_cancel()
371
-
372
- def on_key(self, event: events.Key) -> None: # type: ignore[override]
373
- if event.key not in {"left", "right"}:
374
- return
375
- focused = self.focused
376
- if not isinstance(focused, Button):
377
- return
378
- try:
379
- current = self._buttons.index(focused)
380
- except ValueError:
381
- return
382
- if event.key == "left":
383
- next_index = max(current - 1, 0)
384
- else:
385
- next_index = min(current + 1, len(self._buttons) - 1)
386
- if next_index != current:
387
- self._buttons[next_index].focus()
388
- event.stop()
389
391
 
390
392
  def on_input_submitted(self, event: Input.Submitted) -> None: # type: ignore[override]
391
393
  if event.input.id and event.input.id.startswith("input-"):
@@ -399,6 +401,10 @@ class ParamScreen(Screen[Dict[str, str] | None]):
399
401
  widget.focus()
400
402
  return
401
403
 
404
+ def _focus_ok_button(self) -> None:
405
+ if self._ok_button is not None:
406
+ self._ok_button.focus()
407
+
402
408
  def _submit(self) -> None:
403
409
  values, error = self._gather_values()
404
410
  if error:
@@ -450,8 +456,6 @@ class RunScreen(Screen[Optional[int]]):
450
456
  self._log = RichLog(highlight=True, markup=True, id="log")
451
457
  self._status = Static("", id="status")
452
458
  self._ok_button = Button("OK", id="ok-btn", disabled=True)
453
- self._cancel_button = Button("Cancel", id="cancel-btn")
454
- self._buttons: list[Button] = [self._ok_button, self._cancel_button]
455
459
  self._proc: asyncio.subprocess.Process | None = None
456
460
  self._runner: asyncio.Task[None] | None = None
457
461
  self._exit_code: Optional[int] = None
@@ -467,12 +471,11 @@ class RunScreen(Screen[Optional[int]]):
467
471
  yield self._status
468
472
  with Horizontal(id="buttons"):
469
473
  yield self._ok_button
470
- yield self._cancel_button
471
474
  yield Footer()
472
475
 
473
476
  async def on_mount(self) -> None: # type: ignore[override]
474
- self._status.update("Running… Press Cancel to stop.")
475
- self._cancel_button.focus()
477
+ self._status.update("Running… Press Esc to cancel.")
478
+ self._ok_button.focus()
476
479
  self._runner = asyncio.create_task(self._run_process())
477
480
 
478
481
  async def on_unmount(self) -> None: # type: ignore[override]
@@ -484,26 +487,6 @@ class RunScreen(Screen[Optional[int]]):
484
487
  def on_button_pressed(self, event: Button.Pressed) -> None: # type: ignore[override]
485
488
  if event.button.id == "ok-btn":
486
489
  self.dismiss(self._exit_code)
487
- elif event.button.id == "cancel-btn":
488
- self.action_cancel()
489
-
490
- def on_key(self, event: events.Key) -> None: # type: ignore[override]
491
- if event.key not in {"left", "right"}:
492
- return
493
- focused = self.focused
494
- if not isinstance(focused, Button):
495
- return
496
- try:
497
- current = self._buttons.index(focused)
498
- except ValueError:
499
- return
500
- if event.key == "left":
501
- next_index = max(current - 1, 0)
502
- else:
503
- next_index = min(current + 1, len(self._buttons) - 1)
504
- if next_index != current:
505
- self._buttons[next_index].focus()
506
- event.stop()
507
490
 
508
491
  def action_cancel(self) -> None:
509
492
  if self._proc and self._proc.returncode is None:
@@ -13,23 +13,38 @@ from scripts._utils import bootstrap_dev, cmd_exists, run, sync_packaging # noq
13
13
 
14
14
  @click.command(help="Run lints, type-check, tests with coverage, and Codecov upload if configured")
15
15
  @click.option("--coverage", type=click.Choice(["on", "auto", "off"]), default="on")
16
- def main(coverage: str) -> None:
16
+ @click.option("--verbose", "-v", is_flag=True, help="Print executed commands before running them")
17
+ def main(coverage: str, verbose: bool) -> None:
18
+ env_verbose = os.getenv("TEST_VERBOSE", "").lower()
19
+ if not verbose and env_verbose in {"1", "true", "yes", "on"}:
20
+ verbose = True
21
+
22
+ def _run(cmd: list[str] | str, *, env: dict[str, str] | None = None, check: bool = True) -> None:
23
+ display = cmd if isinstance(cmd, str) else " ".join(cmd)
24
+ if verbose:
25
+ click.echo(f" $ {display}")
26
+ if env:
27
+ overrides = {k: v for k, v in env.items() if os.environ.get(k) != v}
28
+ if overrides:
29
+ env_view = " ".join(f"{k}={v}" for k, v in overrides.items())
30
+ click.echo(f" env {env_view}")
31
+ run(cmd, env=env, check=check) # type: ignore[arg-type]
32
+
17
33
  bootstrap_dev()
18
34
 
19
35
  click.echo("[0/4] Sync packaging (conda/brew/nix) with pyproject")
20
36
  sync_packaging()
21
37
 
22
38
  click.echo("[1/4] Ruff lint")
23
- run(["ruff", "check", "."]) # type: ignore[list-item]
39
+ _run(["ruff", "check", "."], check=False) # type: ignore[list-item]
24
40
 
25
41
  click.echo("[2/4] Ruff format (apply)")
26
- run(["ruff", "format", "."]) # type: ignore[list-item]
42
+ _run(["ruff", "format", "."], check=False) # type: ignore[list-item]
27
43
 
28
44
  click.echo("[3/4] Pyright type-check")
29
- run(["pyright"]) # type: ignore[list-item]
45
+ _run(["pyright"], check=False) # type: ignore[list-item]
30
46
 
31
47
  click.echo("[4/4] Pytest with coverage")
32
- # remove old coverage
33
48
  for f in (".coverage", "coverage.xml"):
34
49
  try:
35
50
  Path(f).unlink()
@@ -42,7 +57,7 @@ def main(coverage: str) -> None:
42
57
  cov_file = Path(tmp) / ".coverage"
43
58
  click.echo(f"[coverage] file={cov_file}")
44
59
  env = os.environ | {"COVERAGE_FILE": str(cov_file)}
45
- run(
60
+ _run(
46
61
  [
47
62
  "python",
48
63
  "-m",
@@ -57,17 +72,15 @@ def main(coverage: str) -> None:
57
72
  )
58
73
  else:
59
74
  click.echo("[coverage] disabled (set --coverage=on to force)")
60
- run(["python", "-m", "pytest", "-q"]) # type: ignore[list-item]
75
+ _run(["python", "-m", "pytest", "-q"]) # type: ignore[list-item]
61
76
 
62
- # Upload coverage to Codecov if present
63
77
  if Path("coverage.xml").exists():
64
78
  click.echo("Uploading coverage to Codecov")
65
79
  if cmd_exists("codecov"):
66
80
  version = run(["python", "-c", "import platform; print(platform.python_version())"]).out.strip()
67
- run(["codecov", "-f", "coverage.xml", "-F", "local", "-n", f"local-$(uname)-{version}"], check=False)
81
+ _run(["codecov", "-f", "coverage.xml", "-F", "local", "-n", f"local-$(uname)-{version}"], check=False)
68
82
  else:
69
- # fallback bash uploader
70
- run(
83
+ _run(
71
84
  [
72
85
  "bash",
73
86
  "-lc",
@@ -1,109 +0,0 @@
1
- (for this repo)
2
-
3
- makThis is a pragmatic quick‑reference for building and debugging Textual UIs that match the versions we use here (Textual ≥ 6.x). It focuses on patterns we implemented in `scripts/menu_tui.py` (ListView + RichLog + async subprocess + Cancel).
4
-
5
- ## Install
6
-
7
- - Dev extras (recommended):
8
- - `pip install -e .[dev]`
9
- - Or just Textual: `pip install textual`
10
- - Verify: `python -c "import textual,sys; print(sys.version, textual.__version__)"`
11
-
12
- ## App + compose
13
-
14
- ```python
15
- from textual.app import App, ComposeResult
16
- from textual.widgets import Header, Footer, Static
17
-
18
- class Hello(App):
19
- def compose(self) -> ComposeResult:
20
- yield Header()
21
- yield Static("Hello Textual")
22
- yield Footer()
23
-
24
- if __name__ == "__main__":
25
- Hello().run()
26
- ```
27
-
28
- ## Mounting: schedule vs await
29
-
30
- - `self.mount(widget)` schedules. If you need to update/query the widget immediately, `await self.mount(widget)` inside an async handler (e.g. `async def on_mount(...)`).
31
-
32
- ```python
33
- # See snippets/mount_await.py
34
- ```
35
-
36
- ## ListView (compose safely)
37
-
38
- Create items inside `compose` or after mount. For initial items:
39
-
40
- ```python
41
- from textual.widgets import ListView, ListItem, Label
42
-
43
- yield ListView(
44
- ListItem(Label("Item 1")),
45
- ListItem(Label("Item 2")),
46
- id="targets",
47
- )
48
- ```
49
-
50
- Adding later (after mount):
51
-
52
- ```python
53
- lv = self.query_one("#targets", ListView)
54
- await lv.append(ListItem(Label("New Item"))) # or lv.append(...) if in compose tick
55
- ```
56
-
57
- ## RichLog (Textual 6.x)
58
-
59
- - Use `RichLog` (not `TextLog`).
60
- - Append with `write()`.
61
-
62
- ```python
63
- from textual.widgets import RichLog
64
- log = RichLog(highlight=True)
65
- log.write("Starting…")
66
- ```
67
-
68
- ## Async subprocess + live streaming
69
-
70
- ```python
71
- # See snippets/richlog_stream.py
72
- # gist: create_subprocess_exec + read stdout/stderr per line → log.write()
73
- ```
74
-
75
- POSIX Cancel: `proc.send_signal(signal.SIGINT)`; Windows: `proc.terminate()`.
76
-
77
- ## Actions & key bindings
78
-
79
- Bind keys to actions:
80
-
81
- ```python
82
- # See snippets/actions_bindings.py
83
- ```
84
-
85
- ## CSS basics (inline)
86
-
87
- ```python
88
- CSS = """
89
- Screen { layout: horizontal; }
90
- #right { width: 1fr; }
91
- RichLog { border: tall $accent; }
92
- """
93
- ```
94
-
95
- ## Run the examples
96
-
97
- ```bash
98
- python docs/textual/snippets/listview_basic.py
99
- python docs/textual/snippets/richlog_stream.py
100
- python docs/textual/snippets/mount_await.py
101
- python docs/textual/snippets/actions_bindings.py
102
- ```
103
-
104
- ## Troubleshooting
105
-
106
- - Nothing shows under `make`: ensure stdin/stdout is a TTY. We wire `/dev/tty` in Make.
107
- - Textual import errors: check version (`textual.__version__`), install `pip install -e .[dev]`.
108
- - MountError on ListView: don’t append before the ListView is mounted; yield items in `compose` or `await mount()` first.
109
-