console-window 2.1.3__tar.gz → 2.2.2__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.1.3/README.md → console_window-2.2.2/PKG-INFO +80 -14
- console_window-2.1.3/PKG-INFO → console_window-2.2.2/README.md +48 -34
- {console_window-2.1.3 → console_window-2.2.2}/console_window/__init__.py +13 -1
- console_window-2.2.2/console_window/appearance.py +137 -0
- {console_window-2.1.3 → console_window-2.2.2}/console_window/backend.py +9 -9
- {console_window-2.1.3 → console_window-2.2.2}/console_window/cli.py +43 -10
- {console_window-2.1.3 → console_window-2.2.2}/console_window/glyphs.py +9 -3
- {console_window-2.1.3 → console_window-2.2.2}/console_window/help.py +1 -1
- {console_window-2.1.3 → console_window-2.2.2}/console_window/opts.py +1 -1
- {console_window-2.1.3 → console_window-2.2.2}/console_window/screens.py +1 -1
- {console_window-2.1.3 → console_window-2.2.2}/console_window/spinner.py +2 -2
- {console_window-2.1.3 → console_window-2.2.2}/console_window/theme.py +66 -25
- {console_window-2.1.3 → console_window-2.2.2}/console_window/window.py +18 -18
- {console_window-2.1.3 → console_window-2.2.2}/pyproject.toml +51 -0
- console_window-2.1.3/.github/workflows/ci.yml +0 -66
- console_window-2.1.3/.gitignore +0 -207
- console_window-2.1.3/SampleApp.py +0 -593
- console_window-2.1.3/docs/GLYPH-MIGRATION-PLAN.md +0 -380
- console_window-2.1.3/runner +0 -13
- console_window-2.1.3/tests/AUDIT.md +0 -120
- console_window-2.1.3/tests/CHANGELOG.md +0 -624
- console_window-2.1.3/tests/conftest.py +0 -23
- console_window-2.1.3/tests/test_backend.py +0 -463
- console_window-2.1.3/tests/test_cli_display_args.py +0 -131
- console_window-2.1.3/tests/test_context.py +0 -45
- console_window-2.1.3/tests/test_demo_mode.py +0 -97
- console_window-2.1.3/tests/test_dialogs.py +0 -177
- console_window-2.1.3/tests/test_flash.py +0 -130
- console_window-2.1.3/tests/test_geometry.py +0 -116
- console_window-2.1.3/tests/test_glyph_policy.py +0 -141
- console_window-2.1.3/tests/test_glyph_rendering.py +0 -124
- console_window-2.1.3/tests/test_glyph_transliteration.py +0 -107
- console_window-2.1.3/tests/test_glyphs.py +0 -121
- console_window-2.1.3/tests/test_header.py +0 -309
- console_window-2.1.3/tests/test_interactive.py +0 -725
- console_window-2.1.3/tests/test_low_colour.py +0 -221
- console_window-2.1.3/tests/test_overflow.py +0 -175
- console_window-2.1.3/tests/test_prompt_line.py +0 -133
- console_window-2.1.3/tests/test_prompt_window.py +0 -288
- console_window-2.1.3/tests/test_public_api.py +0 -133
- console_window-2.1.3/tests/test_render_retry.py +0 -88
- console_window-2.1.3/tests/test_sample_app.py +0 -225
- console_window-2.1.3/tests/test_screens.py +0 -172
- console_window-2.1.3/tests/test_signals.py +0 -166
- console_window-2.1.3/tests/test_spinner.py +0 -276
- console_window-2.1.3/tests/test_status.py +0 -240
- console_window-2.1.3/tests/test_taken_nav_keys.py +0 -251
- console_window-2.1.3/tests/test_textwidth.py +0 -197
- console_window-2.1.3/tests/test_theme.py +0 -218
- console_window-2.1.3/tests/test_theme_picker.py +0 -165
- console_window-2.1.3/tests/test_viewport_pad.py +0 -152
- console_window-2.1.3/tests/test_widechars.py +0 -194
- console_window-2.1.3/tests/test_widgets.py +0 -297
- {console_window-2.1.3 → console_window-2.2.2}/LICENSE +0 -0
- {console_window-2.1.3 → console_window-2.2.2}/console_window/context.py +0 -0
- {console_window-2.1.3 → console_window-2.2.2}/console_window/geometry.py +0 -0
- {console_window-2.1.3 → console_window-2.2.2}/console_window/header.py +0 -0
- {console_window-2.1.3 → console_window-2.2.2}/console_window/keys.py +0 -0
- {console_window-2.1.3 → console_window-2.2.2}/console_window/signals.py +0 -0
- {console_window-2.1.3 → console_window-2.2.2}/console_window/textwidth.py +0 -0
- {console_window-2.1.3 → console_window-2.2.2}/console_window/widgets.py +0 -0
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: console-window
|
|
3
|
+
Version: 2.2.2
|
|
4
|
+
Summary: A robust curses wrapper for creating Text User Interfaces (TUIs).
|
|
5
|
+
Author-email: Joe Defen <joedef@duck.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Topic :: Terminals
|
|
13
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: pytest >= 7.0 ; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-cov >= 4.0 ; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff >= 0.9 ; extra == "dev"
|
|
18
|
+
Requires-Dist: build >= 1.2 ; extra == "dev"
|
|
19
|
+
Requires-Dist: twine >= 7 ; extra == "dev"
|
|
20
|
+
Requires-Dist: packaging >= 26 ; extra == "dev"
|
|
21
|
+
Requires-Dist: tomli >= 2.0 ; extra == "dev" and ( python_version < "3.11")
|
|
22
|
+
Requires-Dist: pytest >= 7.0 ; extra == "test"
|
|
23
|
+
Requires-Dist: pytest-cov >= 4.0 ; extra == "test"
|
|
24
|
+
Requires-Dist: tomli >= 2.0 ; extra == "test" and ( python_version < "3.11")
|
|
25
|
+
Project-URL: Changelog, https://github.com/joedefen/console-window/blob/main/CHANGELOG.md
|
|
26
|
+
Project-URL: Homepage, https://github.com/joedefen/console-window
|
|
27
|
+
Project-URL: Issues, https://github.com/joedefen/console-window/issues
|
|
28
|
+
Project-URL: Repository, https://github.com/joedefen/console-window
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
|
|
1
32
|
|
|
2
33
|
# console-window
|
|
3
34
|
|
|
@@ -45,6 +76,17 @@ no install or build step required:
|
|
|
45
76
|
(`runner` executes `SampleApp` as `__main__`, so live edits are picked
|
|
46
77
|
up immediately. The equivalent one-off is `python3 SampleApp.py`.)
|
|
47
78
|
|
|
79
|
+
The sample takes the same display switches as any app built on the library:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
./runner --no-color # or NO_COLOR=1 ./runner
|
|
83
|
+
./runner --theme nord
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`--no-color` starts in the monochrome mode that the `THEMES` picker also offers
|
|
87
|
+
as its `mono` choice, so `t` on the `THEMES` screen can turn color back on (or
|
|
88
|
+
off again) without restarting.
|
|
89
|
+
|
|
48
90
|
## Core Features (The "Secret Sauce")
|
|
49
91
|
|
|
50
92
|
The wrapper handles several advanced terminal interaction patterns automatically:
|
|
@@ -52,7 +94,7 @@ The wrapper handles several advanced terminal interaction patterns automatically
|
|
|
52
94
|
* **Viewport-Sized Pads:** Separate `head` and `body` pads give the static header band and the independently scrolling body, but the pads are blit buffers sized to the terminal rather than content stores. `add_body()`/`add_header()`/`draw()` write the model, and `render()` paints the visible page into pad row 0, so pad memory and per-frame `clear()` cost stay constant no matter how many rows an app adds. Row counts need no configuration at all: `opts.body_rows` / `opts.head_rows` are advisory hints only, and `opts.max_rows` (default `32767`, `None` for unlimited) is the single content ceiling.
|
|
53
95
|
* **Loud Content Limits:** A row added past `opts.max_rows` is discarded, but no longer quietly: the first discarded row of each fill episode emits a `UserWarning` naming the namespace and the option to change, and every discarded row is counted in `ConsoleWindow.rows_dropped`. It stays non-fatal — run `python -W error::UserWarning` if you would rather it killed the app.
|
|
54
96
|
* **Seamless Resizing:** Automatically detects terminal size changes (`KEY_RESIZE`) and recalculates layout dimensions, with robust error handling to prevent common `curses.error` crashes during resizing.
|
|
55
|
-
* **Graceful
|
|
97
|
+
* **Graceful Color Degradation:** Themes are written against the 256-color xterm palette, but installation asks the backend how many colors the terminal actually exposes. A 256-color terminal gets the theme's literal specs; 16-, 88- and 8-color terminals get the nearest available palette entry, with a contrast guard so a pair never collapses to `fg == bg`; below 8 colors (or with no color support) the theme falls back to attributes. `-1` is only emitted when `use_default_colors()` succeeds, and `COLOR_PAIRS` is respected. Stock tmux (`TERM=screen`, 8 colors) therefore works instead of raising `ValueError: Color number is greater than COLORS-1`.
|
|
56
98
|
* **Scroll & Pick Synchronization:** The `Window` class manages **scroll position** and a separate **pick position** (highlighted row) simultaneously.
|
|
57
99
|
* When using pick mode, scrolling automatically tracks the highlighted element to keep it visible in `.pick_pos`.
|
|
58
100
|
* To use that value, you must separately be able to associate `.pick_pos` to its value by, say, keeping an array of values for every added line to the `body`.
|
|
@@ -90,15 +132,20 @@ demonstrates:
|
|
|
90
132
|
`handle_prompt_key()`, and `N` edits a note in the header band with the
|
|
91
133
|
buffered text mode. Both use `PromptLine` underneath; completion callbacks
|
|
92
134
|
run after passthrough is restored, so they can report with `status()`.
|
|
93
|
-
* **Themes** — a `THEMES` screen of
|
|
135
|
+
* **Themes** — a `THEMES` screen of color samples backed by
|
|
94
136
|
`Theme.list_all()` / `get_current()` / `set()`. The active theme is now
|
|
95
137
|
per-window state: `ConsoleWindowOpts(theme=...)` seeds `window.theme`,
|
|
96
|
-
`window.set_theme()` switches it (and re-
|
|
138
|
+
`window.set_theme()` switches it (and re-initializes the terminal's color
|
|
97
139
|
pairs, which curses keeps per terminal rather than per window), and
|
|
98
140
|
`Theme.set()` remains the process-wide switch. Theme names are semantic:
|
|
99
|
-
asking for `nord` on an 8-
|
|
141
|
+
asking for `nord` on an 8-color terminal installs the closest supported
|
|
100
142
|
representation rather than raising, and startup, `set_theme()` and
|
|
101
|
-
`Theme.set()` all share `Theme.install()`.
|
|
143
|
+
`Theme.set()` all share `Theme.install()`. The demo's picker also offers a
|
|
144
|
+
`mono` choice that is not a palette: selecting it calls
|
|
145
|
+
`appearance.set_colors_enabled(False)` and the swatches switch from color
|
|
146
|
+
pairs to the bold/reverse attribute hierarchy — the same thing `--no-color`
|
|
147
|
+
does at startup. While color is off the header names `mono`, not the palette
|
|
148
|
+
still installed underneath, so one choice has one name.
|
|
102
149
|
* **Dialogs** — `a` calls `answer()` for text input, `l` calls `alert()`, and `n`
|
|
103
150
|
drives `answer()` from an `OptionSpinner` option (`add_key(..., prompt=...)`).
|
|
104
151
|
`answer()` returns `None` only when the user aborts; if the terminal cannot
|
|
@@ -269,7 +316,7 @@ loop. The window never learns which item the prompt belongs to.
|
|
|
269
316
|
`BasicTextScreen` is the small `Screen` for preformatted, scrollable text. It
|
|
270
317
|
resolves `app.win` at draw time, disables pick mode, adds each header before the
|
|
271
318
|
body, and adds each line with `add_body()` and no `Context` (so normal
|
|
272
|
-
navigation scrolls it). Content is
|
|
319
|
+
navigation scrolls it). Content is normalized by `set_text()`, which is safe
|
|
273
320
|
before or after the first draw:
|
|
274
321
|
|
|
275
322
|
```python
|
|
@@ -314,7 +361,7 @@ vertical key is allowed (and logged as "reduced navigation"), since the
|
|
|
314
361
|
application is then responsible for its own scrolling.
|
|
315
362
|
|
|
316
363
|
`ScreenStack.perform_actions()` is the normal router: it walks the spinner
|
|
317
|
-
options,
|
|
364
|
+
options, honors their scopes, and calls the matching `<attr>_ACTION` method.
|
|
318
365
|
For apps that dispatch an action themselves, `ScreenStack.act_in(action,
|
|
319
366
|
screens=None)` is the low-level single-flag variant: it clears that flag from
|
|
320
367
|
`stack.obj` and returns whether it was set and the current screen matched.
|
|
@@ -392,7 +439,7 @@ degrades by itself:
|
|
|
392
439
|
or pass it with the window: `ConsoleWindowOpts(glyphs={'tick': ('✓', 'OK')},
|
|
393
440
|
glyphs_probe=('tick',))`. Once registered, the glyph both degrades with
|
|
394
441
|
`Glyphs.get('tick')` and appears in the font check via `Glyphs.probe_text()`.
|
|
395
|
-
* **
|
|
442
|
+
* **The switches, with no argparse boilerplate.** The bundled helper adds them:
|
|
396
443
|
|
|
397
444
|
```python
|
|
398
445
|
parser = cli.add_display_arguments(argparse.ArgumentParser())
|
|
@@ -402,10 +449,11 @@ degrades by itself:
|
|
|
402
449
|
|
|
403
450
|
`--ascii` forces the fallback (`--ascii=no` forces the full set) and
|
|
404
451
|
`--glyphs {auto,ascii,unicode}` is its symmetric spelling; `--glyphs` wins if
|
|
405
|
-
both are given. `--theme` seeds the first window's
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
452
|
+
both are given. `--theme` seeds the first window's color theme and
|
|
453
|
+
`--no-color` draws the semantic roles with bold/reverse attributes instead of
|
|
454
|
+
color. Apps without argparse can use
|
|
455
|
+
`CONSOLE_WINDOW_GLYPHS=auto|ascii|unicode`, which outranks detection.
|
|
456
|
+
* **The mode is process-wide**, like the color-pair table, because there is one
|
|
409
457
|
terminal. `ConsoleWindowOpts(ascii=...)` seeds it and `win.ascii_mode` reads
|
|
410
458
|
it; a window constructed later never reverts an explicit choice.
|
|
411
459
|
* **Help screens can explain it for you.** Return
|
|
@@ -418,11 +466,28 @@ Themes work the same way: `Theme.create_picker_screen(base, cycle_key=None,
|
|
|
418
466
|
store=None, note=None)` discovers the key you bound to `spin_theme` for its hint
|
|
419
467
|
line, remembers the choice through any object with `load()`/`save()`, and
|
|
420
468
|
`Theme.initial(cli_theme, store)` returns the theme a new process should start
|
|
421
|
-
on. The library never opens a file.
|
|
469
|
+
on. The library never opens a file. Its swatches follow the shared color
|
|
470
|
+
switch, so a picker opened while color is off previews the monochrome
|
|
471
|
+
hierarchy rather than a previous theme's colors; override `swatch_attr()` on a
|
|
472
|
+
subclass to style swatches differently, and `theme_label()` to name an app
|
|
473
|
+
pseudo-theme such as `mono` in the header.
|
|
474
|
+
|
|
475
|
+
The appearance layer is shared too. `console_window.appearance.semantic_attr(role)`
|
|
476
|
+
returns the installed color pair for a semantic role, or the matching attribute
|
|
477
|
+
from `MONO_ROLES` when color is off; it degrades to plain text when the terminal
|
|
478
|
+
has no color or curses has not been initialized, so a redraw is never aborted by
|
|
479
|
+
a color. `set_colors_enabled()` / `colors_enabled()` hold the process-wide
|
|
480
|
+
switch, and `resolve_appearance(cli_theme=None, no_color=False, saved_theme='',
|
|
481
|
+
saved_colors=True, environ=None)` returns the `(theme, colors)` to start with:
|
|
482
|
+
`--no-color` first, then an explicit `--theme`, then `NO_COLOR`, then the
|
|
483
|
+
remembered choice. Apps keep their preference file and their own pseudo-themes;
|
|
484
|
+
the library gives them the policy, not the storage. `SampleApp`'s `mono` choice
|
|
485
|
+
is the worked example of that split: it lives in the app, flips the switch, and
|
|
486
|
+
leaves the installed palette untouched.
|
|
422
487
|
|
|
423
488
|
## Versioning and Deprecation
|
|
424
489
|
|
|
425
|
-
`console-window` follows semantic versioning: patch releases fix
|
|
490
|
+
`console-window` follows semantic versioning: patch releases fix behavior
|
|
426
491
|
without changing the public API, minor releases add functionality, and major
|
|
427
492
|
releases may make incompatible changes.
|
|
428
493
|
|
|
@@ -472,3 +537,4 @@ For more extensive examples, you can look at some projects using `console-window
|
|
|
472
537
|
* viewer of /proc/meminfo that shows historical values
|
|
473
538
|
* [vappman](https://github.com/joedefen/vappman)
|
|
474
539
|
* convenince wrapper to mostly hide CLI of [AppMan](https://github.com/ivan-hc/AppMan), a life-cycle manager for 2000+ AppImages
|
|
540
|
+
|
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: console-window
|
|
3
|
-
Version: 2.1.3
|
|
4
|
-
Summary: A robust curses wrapper for creating Text User Interfaces (TUIs).
|
|
5
|
-
Author-email: Joe Defen <joedef@duck.com>
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
-
Classifier: Topic :: Terminals
|
|
13
|
-
Classifier: Topic :: Software Development :: User Interfaces
|
|
14
|
-
License-File: LICENSE
|
|
15
|
-
Project-URL: Changelog, https://github.com/joedefen/console-window/blob/main/CHANGELOG.md
|
|
16
|
-
Project-URL: Homepage, https://github.com/joedefen/console-window
|
|
17
|
-
Project-URL: Issues, https://github.com/joedefen/console-window/issues
|
|
18
|
-
Project-URL: Repository, https://github.com/joedefen/console-window
|
|
19
|
-
|
|
20
1
|
|
|
21
2
|
# console-window
|
|
22
3
|
|
|
@@ -64,6 +45,17 @@ no install or build step required:
|
|
|
64
45
|
(`runner` executes `SampleApp` as `__main__`, so live edits are picked
|
|
65
46
|
up immediately. The equivalent one-off is `python3 SampleApp.py`.)
|
|
66
47
|
|
|
48
|
+
The sample takes the same display switches as any app built on the library:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
./runner --no-color # or NO_COLOR=1 ./runner
|
|
52
|
+
./runner --theme nord
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`--no-color` starts in the monochrome mode that the `THEMES` picker also offers
|
|
56
|
+
as its `mono` choice, so `t` on the `THEMES` screen can turn color back on (or
|
|
57
|
+
off again) without restarting.
|
|
58
|
+
|
|
67
59
|
## Core Features (The "Secret Sauce")
|
|
68
60
|
|
|
69
61
|
The wrapper handles several advanced terminal interaction patterns automatically:
|
|
@@ -71,7 +63,7 @@ The wrapper handles several advanced terminal interaction patterns automatically
|
|
|
71
63
|
* **Viewport-Sized Pads:** Separate `head` and `body` pads give the static header band and the independently scrolling body, but the pads are blit buffers sized to the terminal rather than content stores. `add_body()`/`add_header()`/`draw()` write the model, and `render()` paints the visible page into pad row 0, so pad memory and per-frame `clear()` cost stay constant no matter how many rows an app adds. Row counts need no configuration at all: `opts.body_rows` / `opts.head_rows` are advisory hints only, and `opts.max_rows` (default `32767`, `None` for unlimited) is the single content ceiling.
|
|
72
64
|
* **Loud Content Limits:** A row added past `opts.max_rows` is discarded, but no longer quietly: the first discarded row of each fill episode emits a `UserWarning` naming the namespace and the option to change, and every discarded row is counted in `ConsoleWindow.rows_dropped`. It stays non-fatal — run `python -W error::UserWarning` if you would rather it killed the app.
|
|
73
65
|
* **Seamless Resizing:** Automatically detects terminal size changes (`KEY_RESIZE`) and recalculates layout dimensions, with robust error handling to prevent common `curses.error` crashes during resizing.
|
|
74
|
-
* **Graceful
|
|
66
|
+
* **Graceful Color Degradation:** Themes are written against the 256-color xterm palette, but installation asks the backend how many colors the terminal actually exposes. A 256-color terminal gets the theme's literal specs; 16-, 88- and 8-color terminals get the nearest available palette entry, with a contrast guard so a pair never collapses to `fg == bg`; below 8 colors (or with no color support) the theme falls back to attributes. `-1` is only emitted when `use_default_colors()` succeeds, and `COLOR_PAIRS` is respected. Stock tmux (`TERM=screen`, 8 colors) therefore works instead of raising `ValueError: Color number is greater than COLORS-1`.
|
|
75
67
|
* **Scroll & Pick Synchronization:** The `Window` class manages **scroll position** and a separate **pick position** (highlighted row) simultaneously.
|
|
76
68
|
* When using pick mode, scrolling automatically tracks the highlighted element to keep it visible in `.pick_pos`.
|
|
77
69
|
* To use that value, you must separately be able to associate `.pick_pos` to its value by, say, keeping an array of values for every added line to the `body`.
|
|
@@ -109,15 +101,20 @@ demonstrates:
|
|
|
109
101
|
`handle_prompt_key()`, and `N` edits a note in the header band with the
|
|
110
102
|
buffered text mode. Both use `PromptLine` underneath; completion callbacks
|
|
111
103
|
run after passthrough is restored, so they can report with `status()`.
|
|
112
|
-
* **Themes** — a `THEMES` screen of
|
|
104
|
+
* **Themes** — a `THEMES` screen of color samples backed by
|
|
113
105
|
`Theme.list_all()` / `get_current()` / `set()`. The active theme is now
|
|
114
106
|
per-window state: `ConsoleWindowOpts(theme=...)` seeds `window.theme`,
|
|
115
|
-
`window.set_theme()` switches it (and re-
|
|
107
|
+
`window.set_theme()` switches it (and re-initializes the terminal's color
|
|
116
108
|
pairs, which curses keeps per terminal rather than per window), and
|
|
117
109
|
`Theme.set()` remains the process-wide switch. Theme names are semantic:
|
|
118
|
-
asking for `nord` on an 8-
|
|
110
|
+
asking for `nord` on an 8-color terminal installs the closest supported
|
|
119
111
|
representation rather than raising, and startup, `set_theme()` and
|
|
120
|
-
`Theme.set()` all share `Theme.install()`.
|
|
112
|
+
`Theme.set()` all share `Theme.install()`. The demo's picker also offers a
|
|
113
|
+
`mono` choice that is not a palette: selecting it calls
|
|
114
|
+
`appearance.set_colors_enabled(False)` and the swatches switch from color
|
|
115
|
+
pairs to the bold/reverse attribute hierarchy — the same thing `--no-color`
|
|
116
|
+
does at startup. While color is off the header names `mono`, not the palette
|
|
117
|
+
still installed underneath, so one choice has one name.
|
|
121
118
|
* **Dialogs** — `a` calls `answer()` for text input, `l` calls `alert()`, and `n`
|
|
122
119
|
drives `answer()` from an `OptionSpinner` option (`add_key(..., prompt=...)`).
|
|
123
120
|
`answer()` returns `None` only when the user aborts; if the terminal cannot
|
|
@@ -288,7 +285,7 @@ loop. The window never learns which item the prompt belongs to.
|
|
|
288
285
|
`BasicTextScreen` is the small `Screen` for preformatted, scrollable text. It
|
|
289
286
|
resolves `app.win` at draw time, disables pick mode, adds each header before the
|
|
290
287
|
body, and adds each line with `add_body()` and no `Context` (so normal
|
|
291
|
-
navigation scrolls it). Content is
|
|
288
|
+
navigation scrolls it). Content is normalized by `set_text()`, which is safe
|
|
292
289
|
before or after the first draw:
|
|
293
290
|
|
|
294
291
|
```python
|
|
@@ -333,7 +330,7 @@ vertical key is allowed (and logged as "reduced navigation"), since the
|
|
|
333
330
|
application is then responsible for its own scrolling.
|
|
334
331
|
|
|
335
332
|
`ScreenStack.perform_actions()` is the normal router: it walks the spinner
|
|
336
|
-
options,
|
|
333
|
+
options, honors their scopes, and calls the matching `<attr>_ACTION` method.
|
|
337
334
|
For apps that dispatch an action themselves, `ScreenStack.act_in(action,
|
|
338
335
|
screens=None)` is the low-level single-flag variant: it clears that flag from
|
|
339
336
|
`stack.obj` and returns whether it was set and the current screen matched.
|
|
@@ -411,7 +408,7 @@ degrades by itself:
|
|
|
411
408
|
or pass it with the window: `ConsoleWindowOpts(glyphs={'tick': ('✓', 'OK')},
|
|
412
409
|
glyphs_probe=('tick',))`. Once registered, the glyph both degrades with
|
|
413
410
|
`Glyphs.get('tick')` and appears in the font check via `Glyphs.probe_text()`.
|
|
414
|
-
* **
|
|
411
|
+
* **The switches, with no argparse boilerplate.** The bundled helper adds them:
|
|
415
412
|
|
|
416
413
|
```python
|
|
417
414
|
parser = cli.add_display_arguments(argparse.ArgumentParser())
|
|
@@ -421,10 +418,11 @@ degrades by itself:
|
|
|
421
418
|
|
|
422
419
|
`--ascii` forces the fallback (`--ascii=no` forces the full set) and
|
|
423
420
|
`--glyphs {auto,ascii,unicode}` is its symmetric spelling; `--glyphs` wins if
|
|
424
|
-
both are given. `--theme` seeds the first window's
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
421
|
+
both are given. `--theme` seeds the first window's color theme and
|
|
422
|
+
`--no-color` draws the semantic roles with bold/reverse attributes instead of
|
|
423
|
+
color. Apps without argparse can use
|
|
424
|
+
`CONSOLE_WINDOW_GLYPHS=auto|ascii|unicode`, which outranks detection.
|
|
425
|
+
* **The mode is process-wide**, like the color-pair table, because there is one
|
|
428
426
|
terminal. `ConsoleWindowOpts(ascii=...)` seeds it and `win.ascii_mode` reads
|
|
429
427
|
it; a window constructed later never reverts an explicit choice.
|
|
430
428
|
* **Help screens can explain it for you.** Return
|
|
@@ -437,11 +435,28 @@ Themes work the same way: `Theme.create_picker_screen(base, cycle_key=None,
|
|
|
437
435
|
store=None, note=None)` discovers the key you bound to `spin_theme` for its hint
|
|
438
436
|
line, remembers the choice through any object with `load()`/`save()`, and
|
|
439
437
|
`Theme.initial(cli_theme, store)` returns the theme a new process should start
|
|
440
|
-
on. The library never opens a file.
|
|
438
|
+
on. The library never opens a file. Its swatches follow the shared color
|
|
439
|
+
switch, so a picker opened while color is off previews the monochrome
|
|
440
|
+
hierarchy rather than a previous theme's colors; override `swatch_attr()` on a
|
|
441
|
+
subclass to style swatches differently, and `theme_label()` to name an app
|
|
442
|
+
pseudo-theme such as `mono` in the header.
|
|
443
|
+
|
|
444
|
+
The appearance layer is shared too. `console_window.appearance.semantic_attr(role)`
|
|
445
|
+
returns the installed color pair for a semantic role, or the matching attribute
|
|
446
|
+
from `MONO_ROLES` when color is off; it degrades to plain text when the terminal
|
|
447
|
+
has no color or curses has not been initialized, so a redraw is never aborted by
|
|
448
|
+
a color. `set_colors_enabled()` / `colors_enabled()` hold the process-wide
|
|
449
|
+
switch, and `resolve_appearance(cli_theme=None, no_color=False, saved_theme='',
|
|
450
|
+
saved_colors=True, environ=None)` returns the `(theme, colors)` to start with:
|
|
451
|
+
`--no-color` first, then an explicit `--theme`, then `NO_COLOR`, then the
|
|
452
|
+
remembered choice. Apps keep their preference file and their own pseudo-themes;
|
|
453
|
+
the library gives them the policy, not the storage. `SampleApp`'s `mono` choice
|
|
454
|
+
is the worked example of that split: it lives in the app, flips the switch, and
|
|
455
|
+
leaves the installed palette untouched.
|
|
441
456
|
|
|
442
457
|
## Versioning and Deprecation
|
|
443
458
|
|
|
444
|
-
`console-window` follows semantic versioning: patch releases fix
|
|
459
|
+
`console-window` follows semantic versioning: patch releases fix behavior
|
|
445
460
|
without changing the public API, minor releases add functionality, and major
|
|
446
461
|
releases may make incompatible changes.
|
|
447
462
|
|
|
@@ -491,4 +506,3 @@ For more extensive examples, you can look at some projects using `console-window
|
|
|
491
506
|
* viewer of /proc/meminfo that shows historical values
|
|
492
507
|
* [vappman](https://github.com/joedefen/vappman)
|
|
493
508
|
* convenince wrapper to mostly hide CLI of [AppMan](https://github.com/ivan-hc/AppMan), a life-cycle manager for 2000+ AppImages
|
|
494
|
-
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# Single source of truth for the package version: pyproject.toml declares
|
|
2
2
|
# ``dynamic = ["version"]``, so flit reads this value instead of duplicating it.
|
|
3
|
-
__version__ = "2.
|
|
3
|
+
__version__ = "2.2.2"
|
|
4
4
|
|
|
5
|
+
from .appearance import (
|
|
6
|
+
MONO_ROLES,
|
|
7
|
+
colors_enabled,
|
|
8
|
+
resolve_appearance,
|
|
9
|
+
semantic_attr,
|
|
10
|
+
set_colors_enabled,
|
|
11
|
+
)
|
|
5
12
|
from .context import Context
|
|
6
13
|
from .glyphs import Glyph, Glyphs
|
|
7
14
|
from .header import HeaderLine, Span
|
|
@@ -22,6 +29,7 @@ from .window import ConsoleWindow, TerminalTooSmallError
|
|
|
22
29
|
|
|
23
30
|
__all__ = [
|
|
24
31
|
'HOME_ST',
|
|
32
|
+
'MONO_ROLES',
|
|
25
33
|
'NAVIGATION_KEYS',
|
|
26
34
|
'BasicHelpScreen',
|
|
27
35
|
'BasicTextScreen',
|
|
@@ -41,5 +49,9 @@ __all__ = [
|
|
|
41
49
|
'TerminalTooSmallError',
|
|
42
50
|
'Theme',
|
|
43
51
|
'ThemeStore',
|
|
52
|
+
'colors_enabled',
|
|
44
53
|
'reset_ctrl_c',
|
|
54
|
+
'resolve_appearance',
|
|
55
|
+
'semantic_attr',
|
|
56
|
+
'set_colors_enabled',
|
|
45
57
|
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Semantic color roles, and one resolver for the color switch.
|
|
2
|
+
|
|
3
|
+
Color here is decoration, never information. Every role degrades to an
|
|
4
|
+
attribute when color is off, so the same hierarchy survives on a monochrome
|
|
5
|
+
terminal or under ``--no-color``. Applications kept writing this layer twice:
|
|
6
|
+
the role-to-attribute mapping, the process-wide switch seeded by ``--no-color``
|
|
7
|
+
or ``NO_COLOR``, and a lookup that must never abort a redraw. It now lives
|
|
8
|
+
here.
|
|
9
|
+
|
|
10
|
+
The role ids are :class:`Theme`'s semantic pairs rather than literal color
|
|
11
|
+
numbers. The library installs those pairs at startup and re-installs them
|
|
12
|
+
whenever a theme is applied, so changing the active theme recolors the whole UI
|
|
13
|
+
without an edit to any draw code.
|
|
14
|
+
|
|
15
|
+
``--no-color`` is a per-run switch, not a saved preference: it replaces the
|
|
16
|
+
pairs with the attributes in :data:`MONO_ROLES` for this process. Picking a
|
|
17
|
+
theme from the color-theme screen is a request for color, so the caller can
|
|
18
|
+
lift the switch again -- :func:`resolve_appearance` does that when it sees an
|
|
19
|
+
explicit theme.
|
|
20
|
+
|
|
21
|
+
Nothing application-specific belongs in this module: preference files, app
|
|
22
|
+
vocabulary and app pseudo-themes stay with the application. An app that has a
|
|
23
|
+
``mono`` pseudo-theme normalizes that name to ``''`` plus ``saved_colors=False``
|
|
24
|
+
before calling :func:`resolve_appearance`.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import curses
|
|
30
|
+
import os
|
|
31
|
+
from collections.abc import Mapping
|
|
32
|
+
|
|
33
|
+
from .theme import Theme
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
'MONO_ROLES',
|
|
37
|
+
'colors_enabled',
|
|
38
|
+
'resolve_appearance',
|
|
39
|
+
'semantic_attr',
|
|
40
|
+
'set_colors_enabled',
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# Role -> attribute used when color is off. The hierarchy is kept with
|
|
44
|
+
# emphasis instead of hue: the destructive roles are bold+reverse, warnings,
|
|
45
|
+
# successes and emphasis are bold, and the informational roles carry nothing
|
|
46
|
+
# (their surrounding structure already distinguishes them).
|
|
47
|
+
MONO_ROLES: dict[int, int] = {
|
|
48
|
+
Theme.DANGER: curses.A_BOLD | curses.A_REVERSE,
|
|
49
|
+
Theme.ERROR: curses.A_BOLD | curses.A_REVERSE,
|
|
50
|
+
Theme.WARNING: curses.A_BOLD,
|
|
51
|
+
Theme.SUCCESS: curses.A_BOLD,
|
|
52
|
+
Theme.EMPHASIS: curses.A_BOLD,
|
|
53
|
+
Theme.INFO: curses.A_NORMAL,
|
|
54
|
+
Theme.PROGRESS: curses.A_NORMAL,
|
|
55
|
+
Theme.OLD_SUCCESS: curses.A_NORMAL,
|
|
56
|
+
Theme.HOTSWAP: curses.A_NORMAL,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Process-wide switch, seeded at startup by --no-color / NO_COLOR. One
|
|
60
|
+
# terminal, so one decision.
|
|
61
|
+
_COLORS_ENABLED = True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def set_colors_enabled(enabled: bool) -> None:
|
|
65
|
+
"""Turn semantic color on or off for the whole process."""
|
|
66
|
+
# One terminal, so one process-wide switch.
|
|
67
|
+
global _COLORS_ENABLED # pylint: disable=global-statement
|
|
68
|
+
_COLORS_ENABLED = bool(enabled)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def colors_enabled() -> bool:
|
|
72
|
+
"""True while semantic roles resolve to color pairs rather than attributes."""
|
|
73
|
+
return _COLORS_ENABLED
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_appearance(
|
|
77
|
+
cli_theme: str | None = None,
|
|
78
|
+
no_color: bool = False,
|
|
79
|
+
saved_theme: str | None = '',
|
|
80
|
+
saved_colors: bool = True,
|
|
81
|
+
environ: Mapping[str, str] | None = None,
|
|
82
|
+
) -> tuple[str, bool]:
|
|
83
|
+
"""Starting ``(theme, colors)`` from the switches, environment and memory.
|
|
84
|
+
|
|
85
|
+
Precedence, highest first: ``--no-color``, an explicit ``--theme`` (a
|
|
86
|
+
statement of intent, so it is honored even when ``NO_COLOR`` is set), the
|
|
87
|
+
``NO_COLOR`` convention (any non-empty value disables color, per
|
|
88
|
+
no-color.org), then the remembered choice.
|
|
89
|
+
|
|
90
|
+
``cli_theme``/``saved_theme`` are returned resolved to a known library
|
|
91
|
+
theme; ``''`` means "no explicit theme, keep the library default". An
|
|
92
|
+
unknown name resolves to ``'default'``, exactly like :meth:`Theme.resolve`.
|
|
93
|
+
|
|
94
|
+
The result is a decision, not an action: call
|
|
95
|
+
:func:`set_colors_enabled` with the second value (or pass both on to
|
|
96
|
+
whatever applies themes) when the process should start that way.
|
|
97
|
+
"""
|
|
98
|
+
env = os.environ if environ is None else environ
|
|
99
|
+
|
|
100
|
+
if no_color:
|
|
101
|
+
colors = False
|
|
102
|
+
elif cli_theme:
|
|
103
|
+
colors = True
|
|
104
|
+
elif env.get('NO_COLOR'):
|
|
105
|
+
colors = False
|
|
106
|
+
else:
|
|
107
|
+
colors = bool(saved_colors)
|
|
108
|
+
|
|
109
|
+
if cli_theme:
|
|
110
|
+
theme = Theme.resolve(cli_theme)
|
|
111
|
+
elif saved_theme:
|
|
112
|
+
theme = Theme.resolve(saved_theme)
|
|
113
|
+
else:
|
|
114
|
+
theme = ''
|
|
115
|
+
return theme, colors
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def semantic_attr(pair_id: int) -> int:
|
|
119
|
+
"""Attribute for a :class:`Theme` role, or plain when color is off.
|
|
120
|
+
|
|
121
|
+
With color off the role becomes an attribute from :data:`MONO_ROLES`.
|
|
122
|
+
Otherwise both ``curses.has_colors()`` and ``curses.color_pair()`` can raise
|
|
123
|
+
before ``initscr()`` (``color_pair()`` refuses to run until then), and a
|
|
124
|
+
failed lookup must never abort a redraw, so any failure degrades to plain
|
|
125
|
+
text. The ``except`` is deliberately broad for that reason: a guarded
|
|
126
|
+
redraw is worth more than a precise exception list, and the fallback is
|
|
127
|
+
always the same.
|
|
128
|
+
"""
|
|
129
|
+
if not _COLORS_ENABLED:
|
|
130
|
+
return MONO_ROLES.get(pair_id, curses.A_NORMAL)
|
|
131
|
+
try:
|
|
132
|
+
if not curses.has_colors():
|
|
133
|
+
return 0
|
|
134
|
+
return curses.color_pair(pair_id)
|
|
135
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
136
|
+
# The terminal is not initialized yet; never abort a redraw.
|
|
137
|
+
return 0
|
|
@@ -92,10 +92,10 @@ class RenderBackend(Protocol):
|
|
|
92
92
|
...
|
|
93
93
|
|
|
94
94
|
def colors(self) -> int:
|
|
95
|
-
"""Number of
|
|
95
|
+
"""Number of colors the terminal supports (0 when unknown/none)."""
|
|
96
96
|
|
|
97
97
|
def color_pairs(self) -> int:
|
|
98
|
-
"""Size of the terminal's
|
|
98
|
+
"""Size of the terminal's color-pair table."""
|
|
99
99
|
|
|
100
100
|
def start_color(self) -> None:
|
|
101
101
|
...
|
|
@@ -115,12 +115,12 @@ class RenderBackend(Protocol):
|
|
|
115
115
|
|
|
116
116
|
class CursesBackend:
|
|
117
117
|
# Curses exposes COLORS/COLOR_PAIRS only after start_color(), so the values
|
|
118
|
-
# are captured there. Before that,
|
|
118
|
+
# are captured there. Before that, colors()/color_pairs() fall back to the
|
|
119
119
|
# module globals (valid once initscr() has run) and finally to a conservative
|
|
120
120
|
# default so a caller never sees a bogus zero-capability terminal merely
|
|
121
121
|
# because it constructed a second backend for an already-running curses
|
|
122
122
|
# session.
|
|
123
|
-
_FALLBACK_COLOR_PAIRS = 64 # the ncurses floor for a
|
|
123
|
+
_FALLBACK_COLOR_PAIRS = 64 # the ncurses floor for a color terminal
|
|
124
124
|
|
|
125
125
|
def __init__(self) -> None:
|
|
126
126
|
self._colors = 0
|
|
@@ -156,7 +156,7 @@ class CursesBackend:
|
|
|
156
156
|
def has_colors(self) -> bool:
|
|
157
157
|
try:
|
|
158
158
|
return bool(curses.has_colors())
|
|
159
|
-
except curses.error: # curses not
|
|
159
|
+
except curses.error: # curses not initialized yet
|
|
160
160
|
return False
|
|
161
161
|
|
|
162
162
|
def colors(self) -> int:
|
|
@@ -176,7 +176,7 @@ class CursesBackend:
|
|
|
176
176
|
pairs = 0
|
|
177
177
|
if pairs > 0:
|
|
178
178
|
return pairs
|
|
179
|
-
# A
|
|
179
|
+
# A color-capable terminal always has at least the ncurses minimum;
|
|
180
180
|
# reporting 0 here would make a theme install silently do nothing.
|
|
181
181
|
return self._FALLBACK_COLOR_PAIRS if self.colors() > 0 else 0
|
|
182
182
|
|
|
@@ -184,7 +184,7 @@ class CursesBackend:
|
|
|
184
184
|
try:
|
|
185
185
|
curses.start_color()
|
|
186
186
|
except curses.error:
|
|
187
|
-
# No
|
|
187
|
+
# No color support after all: degrade to monochrome instead of
|
|
188
188
|
# letting the caller crash during startup.
|
|
189
189
|
self._colors = 0
|
|
190
190
|
self._color_pairs = 0
|
|
@@ -289,8 +289,8 @@ class HeadlessBackend:
|
|
|
289
289
|
self.pads: list[_NoopSurface] = []
|
|
290
290
|
self.stopped = False
|
|
291
291
|
# ``colors`` used to be a bare bool. It still accepts one (True means a
|
|
292
|
-
# 256-
|
|
293
|
-
# count, so the low-
|
|
292
|
+
# 256-color terminal, False means none) but now also accepts the exact
|
|
293
|
+
# count, so the low-color degradation paths are unit-testable without a
|
|
294
294
|
# real terminal.
|
|
295
295
|
if isinstance(colors, bool):
|
|
296
296
|
self._colors = 256 if colors else 0
|