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.
Files changed (61) hide show
  1. console_window-2.1.3/README.md → console_window-2.2.2/PKG-INFO +80 -14
  2. console_window-2.1.3/PKG-INFO → console_window-2.2.2/README.md +48 -34
  3. {console_window-2.1.3 → console_window-2.2.2}/console_window/__init__.py +13 -1
  4. console_window-2.2.2/console_window/appearance.py +137 -0
  5. {console_window-2.1.3 → console_window-2.2.2}/console_window/backend.py +9 -9
  6. {console_window-2.1.3 → console_window-2.2.2}/console_window/cli.py +43 -10
  7. {console_window-2.1.3 → console_window-2.2.2}/console_window/glyphs.py +9 -3
  8. {console_window-2.1.3 → console_window-2.2.2}/console_window/help.py +1 -1
  9. {console_window-2.1.3 → console_window-2.2.2}/console_window/opts.py +1 -1
  10. {console_window-2.1.3 → console_window-2.2.2}/console_window/screens.py +1 -1
  11. {console_window-2.1.3 → console_window-2.2.2}/console_window/spinner.py +2 -2
  12. {console_window-2.1.3 → console_window-2.2.2}/console_window/theme.py +66 -25
  13. {console_window-2.1.3 → console_window-2.2.2}/console_window/window.py +18 -18
  14. {console_window-2.1.3 → console_window-2.2.2}/pyproject.toml +51 -0
  15. console_window-2.1.3/.github/workflows/ci.yml +0 -66
  16. console_window-2.1.3/.gitignore +0 -207
  17. console_window-2.1.3/SampleApp.py +0 -593
  18. console_window-2.1.3/docs/GLYPH-MIGRATION-PLAN.md +0 -380
  19. console_window-2.1.3/runner +0 -13
  20. console_window-2.1.3/tests/AUDIT.md +0 -120
  21. console_window-2.1.3/tests/CHANGELOG.md +0 -624
  22. console_window-2.1.3/tests/conftest.py +0 -23
  23. console_window-2.1.3/tests/test_backend.py +0 -463
  24. console_window-2.1.3/tests/test_cli_display_args.py +0 -131
  25. console_window-2.1.3/tests/test_context.py +0 -45
  26. console_window-2.1.3/tests/test_demo_mode.py +0 -97
  27. console_window-2.1.3/tests/test_dialogs.py +0 -177
  28. console_window-2.1.3/tests/test_flash.py +0 -130
  29. console_window-2.1.3/tests/test_geometry.py +0 -116
  30. console_window-2.1.3/tests/test_glyph_policy.py +0 -141
  31. console_window-2.1.3/tests/test_glyph_rendering.py +0 -124
  32. console_window-2.1.3/tests/test_glyph_transliteration.py +0 -107
  33. console_window-2.1.3/tests/test_glyphs.py +0 -121
  34. console_window-2.1.3/tests/test_header.py +0 -309
  35. console_window-2.1.3/tests/test_interactive.py +0 -725
  36. console_window-2.1.3/tests/test_low_colour.py +0 -221
  37. console_window-2.1.3/tests/test_overflow.py +0 -175
  38. console_window-2.1.3/tests/test_prompt_line.py +0 -133
  39. console_window-2.1.3/tests/test_prompt_window.py +0 -288
  40. console_window-2.1.3/tests/test_public_api.py +0 -133
  41. console_window-2.1.3/tests/test_render_retry.py +0 -88
  42. console_window-2.1.3/tests/test_sample_app.py +0 -225
  43. console_window-2.1.3/tests/test_screens.py +0 -172
  44. console_window-2.1.3/tests/test_signals.py +0 -166
  45. console_window-2.1.3/tests/test_spinner.py +0 -276
  46. console_window-2.1.3/tests/test_status.py +0 -240
  47. console_window-2.1.3/tests/test_taken_nav_keys.py +0 -251
  48. console_window-2.1.3/tests/test_textwidth.py +0 -197
  49. console_window-2.1.3/tests/test_theme.py +0 -218
  50. console_window-2.1.3/tests/test_theme_picker.py +0 -165
  51. console_window-2.1.3/tests/test_viewport_pad.py +0 -152
  52. console_window-2.1.3/tests/test_widechars.py +0 -194
  53. console_window-2.1.3/tests/test_widgets.py +0 -297
  54. {console_window-2.1.3 → console_window-2.2.2}/LICENSE +0 -0
  55. {console_window-2.1.3 → console_window-2.2.2}/console_window/context.py +0 -0
  56. {console_window-2.1.3 → console_window-2.2.2}/console_window/geometry.py +0 -0
  57. {console_window-2.1.3 → console_window-2.2.2}/console_window/header.py +0 -0
  58. {console_window-2.1.3 → console_window-2.2.2}/console_window/keys.py +0 -0
  59. {console_window-2.1.3 → console_window-2.2.2}/console_window/signals.py +0 -0
  60. {console_window-2.1.3 → console_window-2.2.2}/console_window/textwidth.py +0 -0
  61. {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 Colour Degradation:** Themes are written against the 256-colour xterm palette, but installation asks the backend how many colours the terminal actually exposes. A 256-colour terminal gets the theme's literal specs; 16-, 88- and 8-colour terminals get the nearest available palette entry, with a contrast guard so a pair never collapses to `fg == bg`; below 8 colours (or with no colour 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 colours) therefore works instead of raising `ValueError: Color number is greater than COLORS-1`.
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 colour samples backed by
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-initialises the terminal's colour
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-colour terminal installs the closest supported
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 normalised by `set_text()`, which is safe
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, honours their scopes, and calls the matching `<attr>_ACTION` method.
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
- * **Two switches, no argparse boilerplate.** The bundled helper adds them:
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 colour theme. Apps without
406
- argparse can use `CONSOLE_WINDOW_GLYPHS=auto|ascii|unicode`, which outranks
407
- detection.
408
- * **The mode is process-wide**, like the colour-pair table, because there is one
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 behaviour
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 Colour Degradation:** Themes are written against the 256-colour xterm palette, but installation asks the backend how many colours the terminal actually exposes. A 256-colour terminal gets the theme's literal specs; 16-, 88- and 8-colour terminals get the nearest available palette entry, with a contrast guard so a pair never collapses to `fg == bg`; below 8 colours (or with no colour 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 colours) therefore works instead of raising `ValueError: Color number is greater than COLORS-1`.
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 colour samples backed by
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-initialises the terminal's colour
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-colour terminal installs the closest supported
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 normalised by `set_text()`, which is safe
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, honours their scopes, and calls the matching `<attr>_ACTION` method.
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
- * **Two switches, no argparse boilerplate.** The bundled helper adds them:
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 colour theme. Apps without
425
- argparse can use `CONSOLE_WINDOW_GLYPHS=auto|ascii|unicode`, which outranks
426
- detection.
427
- * **The mode is process-wide**, like the colour-pair table, because there is one
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 behaviour
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.1.3"
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 colours the terminal supports (0 when unknown/none)."""
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 colour-pair table."""
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, colours()/color_pairs() fall back to the
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 colour terminal
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 initialised yet
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 colour-capable terminal always has at least the ncurses minimum;
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 colour support after all: degrade to monochrome instead of
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-colour terminal, False means none) but now also accepts the exact
293
- # count, so the low-colour degradation paths are unit-testable without a
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