console-window 2.2.0__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.2.0 → console_window-2.2.2}/PKG-INFO +40 -18
- {console_window-2.2.0 → console_window-2.2.2}/README.md +39 -17
- {console_window-2.2.0 → console_window-2.2.2}/console_window/__init__.py +1 -1
- {console_window-2.2.0 → console_window-2.2.2}/console_window/appearance.py +18 -16
- {console_window-2.2.0 → console_window-2.2.2}/console_window/backend.py +9 -9
- {console_window-2.2.0 → console_window-2.2.2}/console_window/cli.py +7 -7
- {console_window-2.2.0 → console_window-2.2.2}/console_window/glyphs.py +9 -3
- {console_window-2.2.0 → console_window-2.2.2}/console_window/help.py +1 -1
- {console_window-2.2.0 → console_window-2.2.2}/console_window/opts.py +1 -1
- {console_window-2.2.0 → console_window-2.2.2}/console_window/screens.py +1 -1
- {console_window-2.2.0 → console_window-2.2.2}/console_window/spinner.py +2 -2
- {console_window-2.2.0 → console_window-2.2.2}/console_window/theme.py +66 -25
- {console_window-2.2.0 → console_window-2.2.2}/console_window/window.py +18 -18
- {console_window-2.2.0 → console_window-2.2.2}/pyproject.toml +1 -1
- {console_window-2.2.0 → console_window-2.2.2}/LICENSE +0 -0
- {console_window-2.2.0 → console_window-2.2.2}/console_window/context.py +0 -0
- {console_window-2.2.0 → console_window-2.2.2}/console_window/geometry.py +0 -0
- {console_window-2.2.0 → console_window-2.2.2}/console_window/header.py +0 -0
- {console_window-2.2.0 → console_window-2.2.2}/console_window/keys.py +0 -0
- {console_window-2.2.0 → console_window-2.2.2}/console_window/signals.py +0 -0
- {console_window-2.2.0 → console_window-2.2.2}/console_window/textwidth.py +0 -0
- {console_window-2.2.0 → console_window-2.2.2}/console_window/widgets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: console-window
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: A robust curses wrapper for creating Text User Interfaces (TUIs).
|
|
5
5
|
Author-email: Joe Defen <joedef@duck.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -76,6 +76,17 @@ no install or build step required:
|
|
|
76
76
|
(`runner` executes `SampleApp` as `__main__`, so live edits are picked
|
|
77
77
|
up immediately. The equivalent one-off is `python3 SampleApp.py`.)
|
|
78
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
|
+
|
|
79
90
|
## Core Features (The "Secret Sauce")
|
|
80
91
|
|
|
81
92
|
The wrapper handles several advanced terminal interaction patterns automatically:
|
|
@@ -83,7 +94,7 @@ The wrapper handles several advanced terminal interaction patterns automatically
|
|
|
83
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.
|
|
84
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.
|
|
85
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.
|
|
86
|
-
* **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`.
|
|
87
98
|
* **Scroll & Pick Synchronization:** The `Window` class manages **scroll position** and a separate **pick position** (highlighted row) simultaneously.
|
|
88
99
|
* When using pick mode, scrolling automatically tracks the highlighted element to keep it visible in `.pick_pos`.
|
|
89
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`.
|
|
@@ -121,15 +132,20 @@ demonstrates:
|
|
|
121
132
|
`handle_prompt_key()`, and `N` edits a note in the header band with the
|
|
122
133
|
buffered text mode. Both use `PromptLine` underneath; completion callbacks
|
|
123
134
|
run after passthrough is restored, so they can report with `status()`.
|
|
124
|
-
* **Themes** — a `THEMES` screen of
|
|
135
|
+
* **Themes** — a `THEMES` screen of color samples backed by
|
|
125
136
|
`Theme.list_all()` / `get_current()` / `set()`. The active theme is now
|
|
126
137
|
per-window state: `ConsoleWindowOpts(theme=...)` seeds `window.theme`,
|
|
127
|
-
`window.set_theme()` switches it (and re-
|
|
138
|
+
`window.set_theme()` switches it (and re-initializes the terminal's color
|
|
128
139
|
pairs, which curses keeps per terminal rather than per window), and
|
|
129
140
|
`Theme.set()` remains the process-wide switch. Theme names are semantic:
|
|
130
|
-
asking for `nord` on an 8-
|
|
141
|
+
asking for `nord` on an 8-color terminal installs the closest supported
|
|
131
142
|
representation rather than raising, and startup, `set_theme()` and
|
|
132
|
-
`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.
|
|
133
149
|
* **Dialogs** — `a` calls `answer()` for text input, `l` calls `alert()`, and `n`
|
|
134
150
|
drives `answer()` from an `OptionSpinner` option (`add_key(..., prompt=...)`).
|
|
135
151
|
`answer()` returns `None` only when the user aborts; if the terminal cannot
|
|
@@ -300,7 +316,7 @@ loop. The window never learns which item the prompt belongs to.
|
|
|
300
316
|
`BasicTextScreen` is the small `Screen` for preformatted, scrollable text. It
|
|
301
317
|
resolves `app.win` at draw time, disables pick mode, adds each header before the
|
|
302
318
|
body, and adds each line with `add_body()` and no `Context` (so normal
|
|
303
|
-
navigation scrolls it). Content is
|
|
319
|
+
navigation scrolls it). Content is normalized by `set_text()`, which is safe
|
|
304
320
|
before or after the first draw:
|
|
305
321
|
|
|
306
322
|
```python
|
|
@@ -345,7 +361,7 @@ vertical key is allowed (and logged as "reduced navigation"), since the
|
|
|
345
361
|
application is then responsible for its own scrolling.
|
|
346
362
|
|
|
347
363
|
`ScreenStack.perform_actions()` is the normal router: it walks the spinner
|
|
348
|
-
options,
|
|
364
|
+
options, honors their scopes, and calls the matching `<attr>_ACTION` method.
|
|
349
365
|
For apps that dispatch an action themselves, `ScreenStack.act_in(action,
|
|
350
366
|
screens=None)` is the low-level single-flag variant: it clears that flag from
|
|
351
367
|
`stack.obj` and returns whether it was set and the current screen matched.
|
|
@@ -433,11 +449,11 @@ degrades by itself:
|
|
|
433
449
|
|
|
434
450
|
`--ascii` forces the fallback (`--ascii=no` forces the full set) and
|
|
435
451
|
`--glyphs {auto,ascii,unicode}` is its symmetric spelling; `--glyphs` wins if
|
|
436
|
-
both are given. `--theme` seeds the first window's
|
|
452
|
+
both are given. `--theme` seeds the first window's color theme and
|
|
437
453
|
`--no-color` draws the semantic roles with bold/reverse attributes instead of
|
|
438
|
-
|
|
454
|
+
color. Apps without argparse can use
|
|
439
455
|
`CONSOLE_WINDOW_GLYPHS=auto|ascii|unicode`, which outranks detection.
|
|
440
|
-
* **The mode is process-wide**, like the
|
|
456
|
+
* **The mode is process-wide**, like the color-pair table, because there is one
|
|
441
457
|
terminal. `ConsoleWindowOpts(ascii=...)` seeds it and `win.ascii_mode` reads
|
|
442
458
|
it; a window constructed later never reverts an explicit choice.
|
|
443
459
|
* **Help screens can explain it for you.** Return
|
|
@@ -450,22 +466,28 @@ Themes work the same way: `Theme.create_picker_screen(base, cycle_key=None,
|
|
|
450
466
|
store=None, note=None)` discovers the key you bound to `spin_theme` for its hint
|
|
451
467
|
line, remembers the choice through any object with `load()`/`save()`, and
|
|
452
468
|
`Theme.initial(cli_theme, store)` returns the theme a new process should start
|
|
453
|
-
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.
|
|
454
474
|
|
|
455
475
|
The appearance layer is shared too. `console_window.appearance.semantic_attr(role)`
|
|
456
|
-
returns the installed
|
|
457
|
-
from `MONO_ROLES` when
|
|
458
|
-
has no
|
|
459
|
-
a
|
|
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
|
|
460
480
|
switch, and `resolve_appearance(cli_theme=None, no_color=False, saved_theme='',
|
|
461
481
|
saved_colors=True, environ=None)` returns the `(theme, colors)` to start with:
|
|
462
482
|
`--no-color` first, then an explicit `--theme`, then `NO_COLOR`, then the
|
|
463
483
|
remembered choice. Apps keep their preference file and their own pseudo-themes;
|
|
464
|
-
the library gives them the policy, not the storage.
|
|
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.
|
|
465
487
|
|
|
466
488
|
## Versioning and Deprecation
|
|
467
489
|
|
|
468
|
-
`console-window` follows semantic versioning: patch releases fix
|
|
490
|
+
`console-window` follows semantic versioning: patch releases fix behavior
|
|
469
491
|
without changing the public API, minor releases add functionality, and major
|
|
470
492
|
releases may make incompatible changes.
|
|
471
493
|
|
|
@@ -45,6 +45,17 @@ no install or build step required:
|
|
|
45
45
|
(`runner` executes `SampleApp` as `__main__`, so live edits are picked
|
|
46
46
|
up immediately. The equivalent one-off is `python3 SampleApp.py`.)
|
|
47
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
|
+
|
|
48
59
|
## Core Features (The "Secret Sauce")
|
|
49
60
|
|
|
50
61
|
The wrapper handles several advanced terminal interaction patterns automatically:
|
|
@@ -52,7 +63,7 @@ The wrapper handles several advanced terminal interaction patterns automatically
|
|
|
52
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.
|
|
53
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.
|
|
54
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.
|
|
55
|
-
* **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`.
|
|
56
67
|
* **Scroll & Pick Synchronization:** The `Window` class manages **scroll position** and a separate **pick position** (highlighted row) simultaneously.
|
|
57
68
|
* When using pick mode, scrolling automatically tracks the highlighted element to keep it visible in `.pick_pos`.
|
|
58
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`.
|
|
@@ -90,15 +101,20 @@ demonstrates:
|
|
|
90
101
|
`handle_prompt_key()`, and `N` edits a note in the header band with the
|
|
91
102
|
buffered text mode. Both use `PromptLine` underneath; completion callbacks
|
|
92
103
|
run after passthrough is restored, so they can report with `status()`.
|
|
93
|
-
* **Themes** — a `THEMES` screen of
|
|
104
|
+
* **Themes** — a `THEMES` screen of color samples backed by
|
|
94
105
|
`Theme.list_all()` / `get_current()` / `set()`. The active theme is now
|
|
95
106
|
per-window state: `ConsoleWindowOpts(theme=...)` seeds `window.theme`,
|
|
96
|
-
`window.set_theme()` switches it (and re-
|
|
107
|
+
`window.set_theme()` switches it (and re-initializes the terminal's color
|
|
97
108
|
pairs, which curses keeps per terminal rather than per window), and
|
|
98
109
|
`Theme.set()` remains the process-wide switch. Theme names are semantic:
|
|
99
|
-
asking for `nord` on an 8-
|
|
110
|
+
asking for `nord` on an 8-color terminal installs the closest supported
|
|
100
111
|
representation rather than raising, and startup, `set_theme()` and
|
|
101
|
-
`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.
|
|
102
118
|
* **Dialogs** — `a` calls `answer()` for text input, `l` calls `alert()`, and `n`
|
|
103
119
|
drives `answer()` from an `OptionSpinner` option (`add_key(..., prompt=...)`).
|
|
104
120
|
`answer()` returns `None` only when the user aborts; if the terminal cannot
|
|
@@ -269,7 +285,7 @@ loop. The window never learns which item the prompt belongs to.
|
|
|
269
285
|
`BasicTextScreen` is the small `Screen` for preformatted, scrollable text. It
|
|
270
286
|
resolves `app.win` at draw time, disables pick mode, adds each header before the
|
|
271
287
|
body, and adds each line with `add_body()` and no `Context` (so normal
|
|
272
|
-
navigation scrolls it). Content is
|
|
288
|
+
navigation scrolls it). Content is normalized by `set_text()`, which is safe
|
|
273
289
|
before or after the first draw:
|
|
274
290
|
|
|
275
291
|
```python
|
|
@@ -314,7 +330,7 @@ vertical key is allowed (and logged as "reduced navigation"), since the
|
|
|
314
330
|
application is then responsible for its own scrolling.
|
|
315
331
|
|
|
316
332
|
`ScreenStack.perform_actions()` is the normal router: it walks the spinner
|
|
317
|
-
options,
|
|
333
|
+
options, honors their scopes, and calls the matching `<attr>_ACTION` method.
|
|
318
334
|
For apps that dispatch an action themselves, `ScreenStack.act_in(action,
|
|
319
335
|
screens=None)` is the low-level single-flag variant: it clears that flag from
|
|
320
336
|
`stack.obj` and returns whether it was set and the current screen matched.
|
|
@@ -402,11 +418,11 @@ degrades by itself:
|
|
|
402
418
|
|
|
403
419
|
`--ascii` forces the fallback (`--ascii=no` forces the full set) and
|
|
404
420
|
`--glyphs {auto,ascii,unicode}` is its symmetric spelling; `--glyphs` wins if
|
|
405
|
-
both are given. `--theme` seeds the first window's
|
|
421
|
+
both are given. `--theme` seeds the first window's color theme and
|
|
406
422
|
`--no-color` draws the semantic roles with bold/reverse attributes instead of
|
|
407
|
-
|
|
423
|
+
color. Apps without argparse can use
|
|
408
424
|
`CONSOLE_WINDOW_GLYPHS=auto|ascii|unicode`, which outranks detection.
|
|
409
|
-
* **The mode is process-wide**, like the
|
|
425
|
+
* **The mode is process-wide**, like the color-pair table, because there is one
|
|
410
426
|
terminal. `ConsoleWindowOpts(ascii=...)` seeds it and `win.ascii_mode` reads
|
|
411
427
|
it; a window constructed later never reverts an explicit choice.
|
|
412
428
|
* **Help screens can explain it for you.** Return
|
|
@@ -419,22 +435,28 @@ Themes work the same way: `Theme.create_picker_screen(base, cycle_key=None,
|
|
|
419
435
|
store=None, note=None)` discovers the key you bound to `spin_theme` for its hint
|
|
420
436
|
line, remembers the choice through any object with `load()`/`save()`, and
|
|
421
437
|
`Theme.initial(cli_theme, store)` returns the theme a new process should start
|
|
422
|
-
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.
|
|
423
443
|
|
|
424
444
|
The appearance layer is shared too. `console_window.appearance.semantic_attr(role)`
|
|
425
|
-
returns the installed
|
|
426
|
-
from `MONO_ROLES` when
|
|
427
|
-
has no
|
|
428
|
-
a
|
|
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
|
|
429
449
|
switch, and `resolve_appearance(cli_theme=None, no_color=False, saved_theme='',
|
|
430
450
|
saved_colors=True, environ=None)` returns the `(theme, colors)` to start with:
|
|
431
451
|
`--no-color` first, then an explicit `--theme`, then `NO_COLOR`, then the
|
|
432
452
|
remembered choice. Apps keep their preference file and their own pseudo-themes;
|
|
433
|
-
the library gives them the policy, not the storage.
|
|
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.
|
|
434
456
|
|
|
435
457
|
## Versioning and Deprecation
|
|
436
458
|
|
|
437
|
-
`console-window` follows semantic versioning: patch releases fix
|
|
459
|
+
`console-window` follows semantic versioning: patch releases fix behavior
|
|
438
460
|
without changing the public API, minor releases add functionality, and major
|
|
439
461
|
releases may make incompatible changes.
|
|
440
462
|
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
"""Semantic
|
|
1
|
+
"""Semantic color roles, and one resolver for the color switch.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
attribute when
|
|
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
5
|
terminal or under ``--no-color``. Applications kept writing this layer twice:
|
|
6
6
|
the role-to-attribute mapping, the process-wide switch seeded by ``--no-color``
|
|
7
7
|
or ``NO_COLOR``, and a lookup that must never abort a redraw. It now lives
|
|
8
8
|
here.
|
|
9
9
|
|
|
10
|
-
The role ids are :class:`Theme`'s semantic pairs rather than literal
|
|
10
|
+
The role ids are :class:`Theme`'s semantic pairs rather than literal color
|
|
11
11
|
numbers. The library installs those pairs at startup and re-installs them
|
|
12
|
-
whenever a theme is applied, so changing the active theme
|
|
12
|
+
whenever a theme is applied, so changing the active theme recolors the whole UI
|
|
13
13
|
without an edit to any draw code.
|
|
14
14
|
|
|
15
15
|
``--no-color`` is a per-run switch, not a saved preference: it replaces the
|
|
16
16
|
pairs with the attributes in :data:`MONO_ROLES` for this process. Picking a
|
|
17
|
-
theme from the
|
|
17
|
+
theme from the color-theme screen is a request for color, so the caller can
|
|
18
18
|
lift the switch again -- :func:`resolve_appearance` does that when it sees an
|
|
19
19
|
explicit theme.
|
|
20
20
|
|
|
21
21
|
Nothing application-specific belongs in this module: preference files, app
|
|
22
22
|
vocabulary and app pseudo-themes stay with the application. An app that has a
|
|
23
|
-
``mono`` pseudo-theme
|
|
23
|
+
``mono`` pseudo-theme normalizes that name to ``''`` plus ``saved_colors=False``
|
|
24
24
|
before calling :func:`resolve_appearance`.
|
|
25
25
|
"""
|
|
26
26
|
|
|
@@ -40,7 +40,7 @@ __all__ = [
|
|
|
40
40
|
'set_colors_enabled',
|
|
41
41
|
]
|
|
42
42
|
|
|
43
|
-
# Role -> attribute used when
|
|
43
|
+
# Role -> attribute used when color is off. The hierarchy is kept with
|
|
44
44
|
# emphasis instead of hue: the destructive roles are bold+reverse, warnings,
|
|
45
45
|
# successes and emphasis are bold, and the informational roles carry nothing
|
|
46
46
|
# (their surrounding structure already distinguishes them).
|
|
@@ -62,13 +62,14 @@ _COLORS_ENABLED = True
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def set_colors_enabled(enabled: bool) -> None:
|
|
65
|
-
"""Turn semantic
|
|
66
|
-
|
|
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
|
|
67
68
|
_COLORS_ENABLED = bool(enabled)
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
def colors_enabled() -> bool:
|
|
71
|
-
"""True while semantic roles resolve to
|
|
72
|
+
"""True while semantic roles resolve to color pairs rather than attributes."""
|
|
72
73
|
return _COLORS_ENABLED
|
|
73
74
|
|
|
74
75
|
|
|
@@ -82,8 +83,8 @@ def resolve_appearance(
|
|
|
82
83
|
"""Starting ``(theme, colors)`` from the switches, environment and memory.
|
|
83
84
|
|
|
84
85
|
Precedence, highest first: ``--no-color``, an explicit ``--theme`` (a
|
|
85
|
-
statement of intent, so it is
|
|
86
|
-
``NO_COLOR`` convention (any non-empty value disables
|
|
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
|
|
87
88
|
no-color.org), then the remembered choice.
|
|
88
89
|
|
|
89
90
|
``cli_theme``/``saved_theme`` are returned resolved to a known library
|
|
@@ -115,9 +116,9 @@ def resolve_appearance(
|
|
|
115
116
|
|
|
116
117
|
|
|
117
118
|
def semantic_attr(pair_id: int) -> int:
|
|
118
|
-
"""Attribute for a :class:`Theme` role, or plain when
|
|
119
|
+
"""Attribute for a :class:`Theme` role, or plain when color is off.
|
|
119
120
|
|
|
120
|
-
With
|
|
121
|
+
With color off the role becomes an attribute from :data:`MONO_ROLES`.
|
|
121
122
|
Otherwise both ``curses.has_colors()`` and ``curses.color_pair()`` can raise
|
|
122
123
|
before ``initscr()`` (``color_pair()`` refuses to run until then), and a
|
|
123
124
|
failed lookup must never abort a redraw, so any failure degrades to plain
|
|
@@ -131,5 +132,6 @@ def semantic_attr(pair_id: int) -> int:
|
|
|
131
132
|
if not curses.has_colors():
|
|
132
133
|
return 0
|
|
133
134
|
return curses.color_pair(pair_id)
|
|
134
|
-
except Exception: #
|
|
135
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
136
|
+
# The terminal is not initialized yet; never abort a redraw.
|
|
135
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
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Argument-parser boilerplate for the display options.
|
|
2
2
|
|
|
3
3
|
Every curses app ends up writing the same four switches: a way to force the
|
|
4
|
-
plain-ASCII symbol set, a way to pick a
|
|
5
|
-
|
|
4
|
+
plain-ASCII symbol set, a way to pick a color theme, a way to turn semantic
|
|
5
|
+
color off, and a way to print what was detected. This module supplies them,
|
|
6
6
|
so an app's ``main()`` gains two lines instead of sixty:
|
|
7
7
|
|
|
8
8
|
parser = cli.add_display_arguments(parser)
|
|
@@ -15,7 +15,7 @@ optional boolean (``--ascii``, ``--ascii=no``) and ``--glyphs`` accepts a mode
|
|
|
15
15
|
"absent" everywhere means *detect*, never "assume Unicode".
|
|
16
16
|
|
|
17
17
|
``--no-color`` is the appearance switch. :func:`apply_display_arguments` acts on
|
|
18
|
-
it, and an app that remembers a
|
|
18
|
+
it, and an app that remembers a color preference can settle the whole decision
|
|
19
19
|
with :func:`console_window.appearance.resolve_appearance`.
|
|
20
20
|
"""
|
|
21
21
|
|
|
@@ -101,7 +101,7 @@ def add_display_arguments(
|
|
|
101
101
|
action='store_true',
|
|
102
102
|
default=None,
|
|
103
103
|
help=(
|
|
104
|
-
'draw the semantic roles with attributes instead of
|
|
104
|
+
'draw the semantic roles with attributes instead of color; wins '
|
|
105
105
|
'over --theme, and NO_COLOR does the same when the switch is absent'
|
|
106
106
|
),
|
|
107
107
|
)
|
|
@@ -139,17 +139,17 @@ def apply_display_arguments(
|
|
|
139
139
|
environ: Mapping[str, str] | None = None,
|
|
140
140
|
stream: TextIO | None = None,
|
|
141
141
|
) -> TerminalReport:
|
|
142
|
-
"""Set the glyph mode, print the report's notes, seed the theme,
|
|
142
|
+
"""Set the glyph mode, print the report's notes, seed the theme, honor color.
|
|
143
143
|
|
|
144
144
|
Call this once, *before* any window is created, so the mode is decided while
|
|
145
145
|
nothing has been drawn. Returns the :class:`~console_window.glyphs.
|
|
146
146
|
TerminalReport` so a caller can react to ``ascii_required`` (for example by
|
|
147
147
|
printing a warning of its own).
|
|
148
148
|
|
|
149
|
-
An explicit ``--no-color`` turns the process-wide
|
|
149
|
+
An explicit ``--no-color`` turns the process-wide color switch off here; an
|
|
150
150
|
absent switch is left alone, so an app with a remembered preference is free
|
|
151
151
|
to call ``set_colors_enabled()`` with its own decision. The two halves of
|
|
152
|
-
that decision -- the theme to install and whether
|
|
152
|
+
that decision -- the theme to install and whether color is on -- come from
|
|
153
153
|
:func:`console_window.appearance.resolve_appearance`.
|
|
154
154
|
|
|
155
155
|
*printer* is a callable taking one string; pass ``sys.stderr.write`` style
|
|
@@ -132,7 +132,7 @@ BUILTIN_GLYPHS: dict[str, Glyph] = {
|
|
|
132
132
|
'nav_prev': Glyph('\u2190', '<-'), # help legend, "left arrow"
|
|
133
133
|
'nav_next': Glyph('\u2192', '->'), # help legend, "right arrow"
|
|
134
134
|
'bullet': Glyph('\u00b7', '.'), # help-list bullet
|
|
135
|
-
'swatch': Glyph('\u2588', '#'), # theme
|
|
135
|
+
'swatch': Glyph('\u2588', '#'), # theme color swatch
|
|
136
136
|
# The message-box frame. `curses.textpad.rectangle()` already draws with
|
|
137
137
|
# ACS line art, but a message box painted by hand needs its own fallback;
|
|
138
138
|
# registering the corners keeps the ASCII mode invariant ("every painted
|
|
@@ -178,7 +178,13 @@ def _reset_state() -> None:
|
|
|
178
178
|
_audited.clear()
|
|
179
179
|
|
|
180
180
|
|
|
181
|
-
def register(
|
|
181
|
+
def register(
|
|
182
|
+
name: str,
|
|
183
|
+
unicode_text: str,
|
|
184
|
+
ascii_text: str,
|
|
185
|
+
*,
|
|
186
|
+
probe: bool = False, # pylint: disable=redefined-outer-name
|
|
187
|
+
) -> None:
|
|
182
188
|
"""Teach the library one decorative glyph.
|
|
183
189
|
|
|
184
190
|
*ascii_text* must be plain ASCII (it may be empty). ``probe=True`` adds the
|
|
@@ -196,7 +202,7 @@ def register(name: str, unicode_text: str, ascii_text: str, *, probe: bool = Fal
|
|
|
196
202
|
def register_many(
|
|
197
203
|
mapping: Mapping[str, Glyph | tuple[str, str] | list[str]],
|
|
198
204
|
*,
|
|
199
|
-
probe: Iterable[str] = (),
|
|
205
|
+
probe: Iterable[str] = (), # pylint: disable=redefined-outer-name
|
|
200
206
|
) -> None:
|
|
201
207
|
"""Register a whole table at once.
|
|
202
208
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Ready-made help-screen paragraphs for the display features.
|
|
2
2
|
|
|
3
3
|
A help screen is where a user goes when the display looks wrong, so the two
|
|
4
|
-
things that most often look wrong -- the
|
|
4
|
+
things that most often look wrong -- the color theme and the symbol set --
|
|
5
5
|
deserve standard wording. Keeping the wording here means every app that uses
|
|
6
6
|
this library says the same true thing, and an app that adds a theme picker
|
|
7
7
|
does not have to re-explain what ``t`` does.
|
|
@@ -207,7 +207,7 @@ class ConsoleWindowOpts:
|
|
|
207
207
|
self.relax_handled_keys = kwargs.get('relax_handled_keys', True)
|
|
208
208
|
# Per-window theme (ConsoleWindow.theme). None means "inherit whatever
|
|
209
209
|
# theme is current when this window starts", which is the pre-existing
|
|
210
|
-
# process-default
|
|
210
|
+
# process-default behavior.
|
|
211
211
|
self.theme = kwargs.get('theme')
|
|
212
212
|
# Glyph mode and app vocabulary. The mode itself is process state (one
|
|
213
213
|
# terminal); the option only seeds it, exactly as ``theme`` seeds the
|
|
@@ -300,7 +300,7 @@ class BasicTextScreen(Screen):
|
|
|
300
300
|
objects before the ConsoleWindow exists (BasicHelpScreen relies on the same
|
|
301
301
|
rule). ``lines`` items are ``str`` or ``(str, attr)``; ``head`` is a
|
|
302
302
|
``str``, a :class:`HeaderLine`, or an iterable of either. Content is
|
|
303
|
-
|
|
303
|
+
normalized by :meth:`set_text`, so ``draw_screen()`` is a flat loop and
|
|
304
304
|
nothing is cached across draws.
|
|
305
305
|
|
|
306
306
|
There is no pick mode, no action handling and no spinner. ESC/back stays
|
|
@@ -76,7 +76,7 @@ class OptionSpinner:
|
|
|
76
76
|
return getattr(ns.obj, ns.attr, None)
|
|
77
77
|
|
|
78
78
|
def normalize(self, attr: str) -> object:
|
|
79
|
-
"""
|
|
79
|
+
"""Initialize a missing option to its first value (or '' for a prompt).
|
|
80
80
|
|
|
81
81
|
This is the only path that writes while an option is being registered;
|
|
82
82
|
ordinary reads (help rendering, key handling) use get_value() and stay
|
|
@@ -265,7 +265,7 @@ class OptionSpinner:
|
|
|
265
265
|
@staticmethod
|
|
266
266
|
def show_help_nav_keys(win: ConsoleWindow) -> None:
|
|
267
267
|
# Render from the live window so its own opts.taken_nav_keys are
|
|
268
|
-
#
|
|
268
|
+
# honored: keys the app owns are not advertised as navigation.
|
|
269
269
|
for line in win.nav_help_lines():
|
|
270
270
|
win.add_header(line)
|
|
271
271
|
|
|
@@ -46,11 +46,11 @@ def _discover_cycle_key(screen: object, fallback: str = 't') -> str:
|
|
|
46
46
|
return fallback
|
|
47
47
|
|
|
48
48
|
# --- palette model -----------------------------------------------------------
|
|
49
|
-
# Standard xterm palette model. 0..15 are the ANSI/base
|
|
50
|
-
# 6x6x6
|
|
49
|
+
# Standard xterm palette model. 0..15 are the ANSI/base colors, 16..231 the
|
|
50
|
+
# 6x6x6 color cube with the levels below, and 232..255 the grayscale ramp
|
|
51
51
|
# ``8 + 10 * n``. The RGB values are the conventional xterm ones; exact
|
|
52
52
|
# fidelity is not required because the result only picks the closest entry a
|
|
53
|
-
# low-
|
|
53
|
+
# low-color terminal can actually display.
|
|
54
54
|
_ANSI_RGB: tuple[tuple[int, int, int], ...] = (
|
|
55
55
|
(0, 0, 0), # 0 black
|
|
56
56
|
(128, 0, 0), # 1 red
|
|
@@ -99,12 +99,12 @@ def _rgb_distance(left: tuple[int, int, int],
|
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
def nearest_palette_index(index: int, colors: int) -> int:
|
|
102
|
-
"""Map 256-
|
|
102
|
+
"""Map 256-color *index* to the nearest entry a *colors*-terminal has.
|
|
103
103
|
|
|
104
104
|
Entries 0..15 are the base palette, so the search is restricted to
|
|
105
|
-
``0 .. min(colors, 16) - 1``. A 256-
|
|
105
|
+
``0 .. min(colors, 16) - 1``. A 256-color terminal gets *index* back
|
|
106
106
|
unchanged (exact theme specs, no visual regression). Returns ``-1`` when
|
|
107
|
-
the terminal has no usable
|
|
107
|
+
the terminal has no usable colors at all.
|
|
108
108
|
"""
|
|
109
109
|
if colors >= 256:
|
|
110
110
|
return index
|
|
@@ -148,10 +148,10 @@ def resolve_color_pair(fg: int, bg: int, colors: int,
|
|
|
148
148
|
|
|
149
149
|
``-1`` is passed through only when :meth:`RenderBackend.use_default_colors`
|
|
150
150
|
succeeded; otherwise it is replaced with concrete defaults (fg 7, bg 0).
|
|
151
|
-
On terminals with fewer than 256
|
|
151
|
+
On terminals with fewer than 256 colors each index is mapped to the
|
|
152
152
|
nearest available palette entry. If the mapping would collapse the pair to
|
|
153
153
|
``fg == bg`` the foreground is moved to the most contrasting entry. Returns
|
|
154
|
-
``None`` when no legible pair exists (e.g. a 1-
|
|
154
|
+
``None`` when no legible pair exists (e.g. a 1-color terminal), in which
|
|
155
155
|
case the caller falls back to attributes and installs nothing.
|
|
156
156
|
"""
|
|
157
157
|
if colors <= 0:
|
|
@@ -166,7 +166,7 @@ def resolve_color_pair(fg: int, bg: int, colors: int,
|
|
|
166
166
|
if bg != -1:
|
|
167
167
|
bg = nearest_palette_index(bg, colors)
|
|
168
168
|
# A pair of terminal defaults is left alone: the terminal guarantees a
|
|
169
|
-
# legible default combination, and preserving (-1, -1) keeps the 256-
|
|
169
|
+
# legible default combination, and preserving (-1, -1) keeps the 256-color
|
|
170
170
|
# path byte-for-byte identical to the theme spec.
|
|
171
171
|
if fg != -1 and bg != -1 and fg == bg:
|
|
172
172
|
partner = _contrast_partner(bg, colors)
|
|
@@ -195,10 +195,10 @@ class Theme:
|
|
|
195
195
|
# to be the bare literal ``8`` in two places.
|
|
196
196
|
DEFAULT_BG = 8
|
|
197
197
|
|
|
198
|
-
# The semantic roles, in
|
|
199
|
-
# (ConsoleWindow._init_screen) and Theme.set()
|
|
198
|
+
# The semantic roles, in initialization order. Both startup
|
|
199
|
+
# (ConsoleWindow._init_screen) and Theme.set() reinitialize exactly this set,
|
|
200
200
|
# so the two paths cannot drift apart: startup used to omit OLD_SUCCESS,
|
|
201
|
-
# leaving it on an
|
|
201
|
+
# leaving it on an uninitialized pair until a theme change.
|
|
202
202
|
SEMANTIC_PAIRS: ClassVar[tuple[int, ...]] = (
|
|
203
203
|
DANGER, SUCCESS, OLD_SUCCESS, WARNING, INFO, EMPHASIS, ERROR, PROGRESS, HOTSWAP,
|
|
204
204
|
)
|
|
@@ -337,7 +337,8 @@ class Theme:
|
|
|
337
337
|
if cli_theme is None and store is not None:
|
|
338
338
|
try:
|
|
339
339
|
remembered = store.load()
|
|
340
|
-
|
|
340
|
+
# A broken prefs file must not kill start-up.
|
|
341
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
341
342
|
logger.warning('theme store could not be read; using the default',
|
|
342
343
|
exc_info=True)
|
|
343
344
|
return cls.resolve(cli_theme or remembered)
|
|
@@ -349,7 +350,7 @@ class Theme:
|
|
|
349
350
|
``DEFAULT_BG`` comes first (the screen and pad background), then the
|
|
350
351
|
semantic roles in ``SEMANTIC_PAIRS`` order. Startup
|
|
351
352
|
(``ConsoleWindow._init_screen``), :meth:`set` and
|
|
352
|
-
``ConsoleWindow.set_theme`` all install
|
|
353
|
+
``ConsoleWindow.set_theme`` all install colors through :meth:`install`
|
|
353
354
|
from this one list, so the paths cannot drift apart.
|
|
354
355
|
"""
|
|
355
356
|
theme = cls.THEMES[cls.resolve(theme_name)]
|
|
@@ -369,13 +370,13 @@ class Theme:
|
|
|
369
370
|
@classmethod
|
|
370
371
|
def install(cls, backend: RenderBackend,
|
|
371
372
|
theme_name: str | None = None) -> str:
|
|
372
|
-
"""Install *theme_name*'s
|
|
373
|
+
"""Install *theme_name*'s colors through *backend*, degrading safely.
|
|
373
374
|
|
|
374
375
|
This is the single installation path used by startup
|
|
375
376
|
(``ConsoleWindow._init_screen``), ``ConsoleWindow.set_theme()`` and
|
|
376
|
-
:meth:`set`. It consults the backend's
|
|
377
|
-
``curses`` globals: on a 256-
|
|
378
|
-
are used unchanged, while fewer
|
|
377
|
+
:meth:`set`. It consults the backend's color capability instead of the
|
|
378
|
+
``curses`` globals: on a 256-color terminal the theme's literal specs
|
|
379
|
+
are used unchanged, while fewer colors are mapped to the nearest
|
|
379
380
|
available palette entry and ``-1`` is only emitted when the terminal
|
|
380
381
|
accepted ``use_default_colors()``. Pair ids beyond ``COLOR_PAIRS`` and
|
|
381
382
|
pairs with no legible mapping are skipped rather than raising. Returns
|
|
@@ -403,10 +404,10 @@ class Theme:
|
|
|
403
404
|
# Late import to avoid a circular import at module load time.
|
|
404
405
|
from .window import ConsoleWindow
|
|
405
406
|
|
|
406
|
-
#
|
|
407
|
+
# Color pairs are terminal-global, so a live window's backend is the
|
|
407
408
|
# authoritative seam. Without one, fall back to curses itself, but go
|
|
408
409
|
# through CursesBackend so this method never calls curses.init_pair()
|
|
409
|
-
# directly (that path used to bypass every low-
|
|
410
|
+
# directly (that path used to bypass every low-color guard).
|
|
410
411
|
win = ConsoleWindow.primary_window()
|
|
411
412
|
backend = getattr(win, '_backend', None) if win is not None else None
|
|
412
413
|
if backend is None:
|
|
@@ -471,7 +472,7 @@ class Theme:
|
|
|
471
472
|
note: str | None = None) -> type:
|
|
472
473
|
"""Build a theme-picker screen class on top of *base_class*.
|
|
473
474
|
|
|
474
|
-
The generated screen renders the current theme, a set of
|
|
475
|
+
The generated screen renders the current theme, a set of labeled color
|
|
475
476
|
swatches, and an action (``spin_theme_ACTION``) that cycles themes. The
|
|
476
477
|
host application is expected to expose ``win``, and may expose
|
|
477
478
|
``opts.theme``: when present, that option is treated as the selected
|
|
@@ -485,6 +486,15 @@ class Theme:
|
|
|
485
486
|
``store.save()`` after each cycle, and a failing store is logged rather
|
|
486
487
|
than raised. *note* is one extra dim line (for example "the choice is
|
|
487
488
|
remembered for the next start").
|
|
489
|
+
|
|
490
|
+
Swatches are painted through the appearance layer rather than
|
|
491
|
+
``curses.color_pair()``, so a picker opened while color is off shows the
|
|
492
|
+
monochrome role hierarchy instead of re-painting a previous theme's
|
|
493
|
+
colors. A host that wants a different swatch style can subclass the
|
|
494
|
+
generated screen and override its ``swatch_attr()`` hook, and a host
|
|
495
|
+
with its own pseudo-theme (``mono``, say) can override
|
|
496
|
+
``theme_label()`` so the header names the choice the user made rather
|
|
497
|
+
than the palette left installed underneath it.
|
|
488
498
|
"""
|
|
489
499
|
swatch = glyphs.get('swatch') * 8
|
|
490
500
|
|
|
@@ -504,7 +514,7 @@ class Theme:
|
|
|
504
514
|
|
|
505
515
|
# Name the theme this window is on (default when unset).
|
|
506
516
|
current_theme = cls.theme_of(win)
|
|
507
|
-
display_theme = current_theme
|
|
517
|
+
display_theme = self.theme_label(current_theme)
|
|
508
518
|
hint_key = cycle_key or _discover_cycle_key(self)
|
|
509
519
|
|
|
510
520
|
# Add header showing current theme
|
|
@@ -531,11 +541,41 @@ class Theme:
|
|
|
531
541
|
# is a registered glyph, so a terminal that cannot draw U+2588
|
|
532
542
|
# shows '#' blocks instead of nine empty cells.
|
|
533
543
|
line = f'{label:12} {swatch} {description}'
|
|
534
|
-
attr
|
|
535
|
-
win.add_body(line, attr=attr)
|
|
544
|
+
win.add_body(line, attr=self.swatch_attr(color_id))
|
|
536
545
|
if note:
|
|
537
546
|
win.add_body(note, attr=curses.A_DIM)
|
|
538
547
|
|
|
548
|
+
def theme_label(self, theme_name: str | None) -> str:
|
|
549
|
+
"""Header label for the theme the window is on.
|
|
550
|
+
|
|
551
|
+
The picker names the palette it is previewing. A host with its
|
|
552
|
+
own pseudo-theme can override this to name the mode actually in
|
|
553
|
+
effect -- for example ``mono`` while color is off -- so the
|
|
554
|
+
header and the swatches tell one story instead of two.
|
|
555
|
+
|
|
556
|
+
This is a hook, like :meth:`swatch_attr`.
|
|
557
|
+
"""
|
|
558
|
+
return theme_name if theme_name else '(default)'
|
|
559
|
+
|
|
560
|
+
@staticmethod
|
|
561
|
+
def swatch_attr(color_id: int) -> int:
|
|
562
|
+
"""Attribute for one role's swatch in the picker.
|
|
563
|
+
|
|
564
|
+
Delegates to :func:`console_window.appearance.semantic_attr`, so
|
|
565
|
+
the swatch obeys the same process-wide color switch as every
|
|
566
|
+
other semantic role. With color off the role's monochrome
|
|
567
|
+
attribute carries the hierarchy (bold+reverse, bold, plain) and
|
|
568
|
+
the preview shows the real no-color appearance; with color on
|
|
569
|
+
it is the installed pair.
|
|
570
|
+
|
|
571
|
+
This is a hook: a host may subclass the generated screen and
|
|
572
|
+
override it to style swatches differently.
|
|
573
|
+
"""
|
|
574
|
+
# Late import: appearance imports Theme at module load time.
|
|
575
|
+
from .appearance import semantic_attr
|
|
576
|
+
|
|
577
|
+
return semantic_attr(color_id)
|
|
578
|
+
|
|
539
579
|
def spin_theme_ACTION(self) -> None:
|
|
540
580
|
win = self.app.win
|
|
541
581
|
vals = cls.list_all()
|
|
@@ -551,7 +591,8 @@ class Theme:
|
|
|
551
591
|
if store is not None:
|
|
552
592
|
try:
|
|
553
593
|
store.save(value)
|
|
554
|
-
|
|
594
|
+
# Persistence never breaks a cycle.
|
|
595
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
555
596
|
logger.warning('could not remember theme %r', value,
|
|
556
597
|
exc_info=True)
|
|
557
598
|
|
|
@@ -181,7 +181,7 @@ def render_nav_help(effective: Iterable[int],
|
|
|
181
181
|
# dedented triple-quoted string: editing the indentation can no longer change
|
|
182
182
|
# what the help screen renders. ``ConsoleWindow.nav_keys`` remains the joined
|
|
183
183
|
# string for callers that want the whole blurb at once; live windows render
|
|
184
|
-
# through ``nav_help_lines()`` so their own ``taken_nav_keys`` are
|
|
184
|
+
# through ``nav_help_lines()`` so their own ``taken_nav_keys`` are honored.
|
|
185
185
|
NAV_KEY_LINES: tuple[str, ...] = render_nav_help(NAVIGATION_KEYS)
|
|
186
186
|
|
|
187
187
|
# Editing keys that passthrough mode hands to the application unchanged, in
|
|
@@ -200,7 +200,7 @@ _PASSTHROUGH_EDITING_KEYS = frozenset({
|
|
|
200
200
|
|
|
201
201
|
# Compact demo-mode names for the keys whose plain curses name would be
|
|
202
202
|
# unwieldy or ambiguous in the header indicator. Printable keys are handled
|
|
203
|
-
# separately (
|
|
203
|
+
# separately (centered with spaces), so this table only covers the rest.
|
|
204
204
|
_DEMO_KEY_LABELS: dict[int, str] = {
|
|
205
205
|
27: 'ESC',
|
|
206
206
|
ord('\t'): 'TAB',
|
|
@@ -659,11 +659,11 @@ class ConsoleWindow:
|
|
|
659
659
|
glyphs.ensure_policy()
|
|
660
660
|
|
|
661
661
|
def set_theme(self, theme_name: str) -> bool:
|
|
662
|
-
"""Make *theme_name* this window's theme and install its
|
|
662
|
+
"""Make *theme_name* this window's theme and install its colors.
|
|
663
663
|
|
|
664
|
-
Returns False for an unknown theme, or when the backend has no
|
|
665
|
-
support (the choice is still recorded, so a later
|
|
666
|
-
uses it).
|
|
664
|
+
Returns False for an unknown theme, or when the backend has no color
|
|
665
|
+
support (the choice is still recorded, so a later color-capable start
|
|
666
|
+
uses it). Color pairs are per-terminal, so installing a palette
|
|
667
667
|
necessarily also affects any other window drawing on the same screen —
|
|
668
668
|
that is a curses constraint, not a choice. What is per-window is the
|
|
669
669
|
selection: it is used at startup, when pads are reallocated, and
|
|
@@ -686,9 +686,9 @@ class ConsoleWindow:
|
|
|
686
686
|
return True
|
|
687
687
|
|
|
688
688
|
def _apply_pad_theme(self, pad: CursesSurface) -> None:
|
|
689
|
-
# A freshly created pad has no background, so re-apply the theme
|
|
689
|
+
# A freshly created pad has no background, so re-apply the theme color
|
|
690
690
|
# after every reallocation; otherwise a resize leaves the new pad on the
|
|
691
|
-
# terminal default instead of the configured background. The
|
|
691
|
+
# terminal default instead of the configured background. The color
|
|
692
692
|
# comes from *this window's* theme, not the process default.
|
|
693
693
|
if not self._backend.has_colors():
|
|
694
694
|
return
|
|
@@ -696,7 +696,7 @@ class ConsoleWindow:
|
|
|
696
696
|
if ('default_fg' in theme and 'default_bg' in theme
|
|
697
697
|
and Theme.DEFAULT_BG < self._backend.color_pairs()):
|
|
698
698
|
# Theme.install() has already installed this pair with either the
|
|
699
|
-
# literal values or the low-
|
|
699
|
+
# literal values or the low-color mapping, so bkgd() never sees a
|
|
700
700
|
# stale/out-of-range pair.
|
|
701
701
|
pad.bkgd(' ', self._backend.color_pair(Theme.DEFAULT_BG))
|
|
702
702
|
|
|
@@ -815,7 +815,7 @@ class ConsoleWindow:
|
|
|
815
815
|
return True
|
|
816
816
|
|
|
817
817
|
def _new_screen(self) -> CursesSurface:
|
|
818
|
-
"""Create and
|
|
818
|
+
"""Create and initialize a brand-new screen for this window."""
|
|
819
819
|
return self._init_screen(self._backend, self.theme)
|
|
820
820
|
|
|
821
821
|
def _start_curses(self) -> CursesSurface:
|
|
@@ -972,11 +972,11 @@ class ConsoleWindow:
|
|
|
972
972
|
@staticmethod
|
|
973
973
|
def _install_theme(backend: RenderBackend, scr: CursesSurface,
|
|
974
974
|
theme_name: str | None) -> str:
|
|
975
|
-
"""Install *theme_name*'s
|
|
975
|
+
"""Install *theme_name*'s colors into the terminal's pair table.
|
|
976
976
|
|
|
977
|
-
|
|
977
|
+
Color pairs are per-terminal (one table, not one per window), so both
|
|
978
978
|
startup and ``set_theme()`` call this; the caller decides *whose*
|
|
979
|
-
theme wins. The actual installation (including low-
|
|
979
|
+
theme wins. The actual installation (including low-color mapping and
|
|
980
980
|
``-1`` resolution) lives in :meth:`Theme.install`, which ``Theme.set()``
|
|
981
981
|
uses too, so the two entry points cannot diverge. Returns the resolved
|
|
982
982
|
theme name.
|
|
@@ -984,7 +984,7 @@ class ConsoleWindow:
|
|
|
984
984
|
name = Theme.install(backend, theme_name)
|
|
985
985
|
theme = Theme.THEMES[name]
|
|
986
986
|
# Can't change pair 0, so the screen background is set directly. The
|
|
987
|
-
# pair has just been installed above (with mapped values on low-
|
|
987
|
+
# pair has just been installed above (with mapped values on low-color
|
|
988
988
|
# terminals), so this never references a stale or out-of-range pair.
|
|
989
989
|
if ('default_fg' in theme and 'default_bg' in theme
|
|
990
990
|
and Theme.DEFAULT_BG < backend.color_pairs()):
|
|
@@ -998,11 +998,11 @@ class ConsoleWindow:
|
|
|
998
998
|
signals.setup_ctrl_c()
|
|
999
999
|
scr = cast(CursesSurface, backend.start(ConsoleWindow.timeout_ms))
|
|
1000
1000
|
|
|
1001
|
-
# Initialize color support with theme. Every
|
|
1002
|
-
# the backend, so a non-curses backend that reports
|
|
1001
|
+
# Initialize color support with theme. Every color call goes through
|
|
1002
|
+
# the backend, so a non-curses backend that reports colors can use it.
|
|
1003
1003
|
if backend.has_colors():
|
|
1004
1004
|
# start_color() is where a curses backend captures COLORS and
|
|
1005
|
-
# COLOR_PAIRS; Theme.install() then enables default
|
|
1005
|
+
# COLOR_PAIRS; Theme.install() then enables default colors (and
|
|
1006
1006
|
# remembers whether -1 is usable) before resolving each pair.
|
|
1007
1007
|
backend.start_color()
|
|
1008
1008
|
# The window's own theme (per-window since AUDIT.md §3.1) or the
|
|
@@ -2541,7 +2541,7 @@ class ConsoleWindow:
|
|
|
2541
2541
|
def _alert_scroll_target(key: int, scroll_top: int, visible_rows: int,
|
|
2542
2542
|
max_scroll: int) -> int:
|
|
2543
2543
|
# "vim"-style scroll commands, matching the navigation family prompt()
|
|
2544
|
-
#
|
|
2544
|
+
# honors. Any other key is not a scroll command and leaves the view put.
|
|
2545
2545
|
if key in (ord('k'), curses.KEY_UP):
|
|
2546
2546
|
target = scroll_top - 1
|
|
2547
2547
|
elif key in (ord('j'), curses.KEY_DOWN):
|
|
@@ -94,7 +94,7 @@ omit = ["tests/*"]
|
|
|
94
94
|
|
|
95
95
|
[tool.coverage.report]
|
|
96
96
|
# Measured at 90.69% when the shared release gate was adopted. The floor is a
|
|
97
|
-
# ratchet against new code arriving without a test, not a target to
|
|
97
|
+
# ratchet against new code arriving without a test, not a target to optimize;
|
|
98
98
|
# raise it when the number rises, never lower it.
|
|
99
99
|
fail_under = 90
|
|
100
100
|
show_missing = true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|