console-window 2.1.2__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- console_window-2.1.2/README.md → console_window-2.2.0/PKG-INFO +98 -0
- console_window-2.1.2/PKG-INFO → console_window-2.2.0/README.md +66 -20
- {console_window-2.1.2 → console_window-2.2.0}/console_window/__init__.py +18 -2
- console_window-2.2.0/console_window/appearance.py +135 -0
- console_window-2.2.0/console_window/cli.py +178 -0
- console_window-2.2.0/console_window/glyphs.py +555 -0
- console_window-2.2.0/console_window/help.py +66 -0
- {console_window-2.1.2 → console_window-2.2.0}/console_window/opts.py +73 -3
- {console_window-2.1.2 → console_window-2.2.0}/console_window/screens.py +36 -0
- {console_window-2.1.2 → console_window-2.2.0}/console_window/spinner.py +2 -1
- {console_window-2.1.2 → console_window-2.2.0}/console_window/theme.py +87 -5
- {console_window-2.1.2 → console_window-2.2.0}/console_window/widgets.py +21 -5
- {console_window-2.1.2 → console_window-2.2.0}/console_window/window.py +67 -14
- {console_window-2.1.2 → console_window-2.2.0}/pyproject.toml +51 -0
- console_window-2.1.2/.github/workflows/ci.yml +0 -66
- console_window-2.1.2/.gitignore +0 -207
- console_window-2.1.2/SampleApp.py +0 -568
- console_window-2.1.2/runner +0 -13
- console_window-2.1.2/tests/AUDIT.md +0 -120
- console_window-2.1.2/tests/CHANGELOG.md +0 -562
- console_window-2.1.2/tests/test_backend.py +0 -463
- console_window-2.1.2/tests/test_context.py +0 -45
- console_window-2.1.2/tests/test_demo_mode.py +0 -97
- console_window-2.1.2/tests/test_dialogs.py +0 -177
- console_window-2.1.2/tests/test_flash.py +0 -130
- console_window-2.1.2/tests/test_geometry.py +0 -116
- console_window-2.1.2/tests/test_header.py +0 -309
- console_window-2.1.2/tests/test_interactive.py +0 -725
- console_window-2.1.2/tests/test_low_colour.py +0 -221
- console_window-2.1.2/tests/test_overflow.py +0 -175
- console_window-2.1.2/tests/test_prompt_line.py +0 -133
- console_window-2.1.2/tests/test_prompt_window.py +0 -288
- console_window-2.1.2/tests/test_public_api.py +0 -130
- console_window-2.1.2/tests/test_render_retry.py +0 -88
- console_window-2.1.2/tests/test_sample_app.py +0 -225
- console_window-2.1.2/tests/test_screens.py +0 -172
- console_window-2.1.2/tests/test_signals.py +0 -166
- console_window-2.1.2/tests/test_spinner.py +0 -276
- console_window-2.1.2/tests/test_status.py +0 -240
- console_window-2.1.2/tests/test_taken_nav_keys.py +0 -251
- console_window-2.1.2/tests/test_textwidth.py +0 -197
- console_window-2.1.2/tests/test_theme.py +0 -218
- console_window-2.1.2/tests/test_viewport_pad.py +0 -152
- console_window-2.1.2/tests/test_widechars.py +0 -194
- console_window-2.1.2/tests/test_widgets.py +0 -297
- {console_window-2.1.2 → console_window-2.2.0}/LICENSE +0 -0
- {console_window-2.1.2 → console_window-2.2.0}/console_window/backend.py +0 -0
- {console_window-2.1.2 → console_window-2.2.0}/console_window/context.py +0 -0
- {console_window-2.1.2 → console_window-2.2.0}/console_window/geometry.py +0 -0
- {console_window-2.1.2 → console_window-2.2.0}/console_window/header.py +0 -0
- {console_window-2.1.2 → console_window-2.2.0}/console_window/keys.py +0 -0
- {console_window-2.1.2 → console_window-2.2.0}/console_window/signals.py +0 -0
- {console_window-2.1.2 → console_window-2.2.0}/console_window/textwidth.py +0 -0
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: console-window
|
|
3
|
+
Version: 2.2.0
|
|
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
|
|
|
@@ -366,6 +397,72 @@ Mixing the two values raises `AssertionError` rather than silently changing what
|
|
|
366
397
|
teardown means. Use `singleton=False` in tests and in applications that create
|
|
367
398
|
independent windows (for example, one real window plus a headless one).
|
|
368
399
|
|
|
400
|
+
### Symbols on terminals that cannot draw them
|
|
401
|
+
|
|
402
|
+
The UI is built around a handful of non-ASCII symbols (`│`, `⮞`, `█`, `←`, `→`,
|
|
403
|
+
`·`, box drawing). A terminal that cannot encode them does not fail loudly: it
|
|
404
|
+
paints a box, or nothing, and the row quietly loses its meaning. `console-window`
|
|
405
|
+
therefore keeps every decorative symbol in one registry, in two spellings, and
|
|
406
|
+
degrades by itself:
|
|
407
|
+
|
|
408
|
+
* **No code change.** A window detects the terminal when it is constructed — a
|
|
409
|
+
non-UTF-8 output encoding or `TERM=dumb` selects the ASCII spellings — and
|
|
410
|
+
text handed to `add_header()` / `add_body()` / `draw()` / `status()` /
|
|
411
|
+
`flash()` is transliterated before it is measured, so widths and the
|
|
412
|
+
run-length attribute counts stay correct.
|
|
413
|
+
* **Only decoration is rewritten.** `Grüße` stays `Grüße`: the substitution table
|
|
414
|
+
contains registered glyphs, never arbitrary text. Set
|
|
415
|
+
`CONSOLE_WINDOW_GLYPH_AUDIT=1` to log each decorative code point you use but
|
|
416
|
+
did not register, with the exact `register()` call to add.
|
|
417
|
+
* **App vocabulary.** Register what only your app draws:
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
Glyphs.register_many({'tick': ('✓', 'OK')}, probe=('tick',))
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
or pass it with the window: `ConsoleWindowOpts(glyphs={'tick': ('✓', 'OK')},
|
|
424
|
+
glyphs_probe=('tick',))`. Once registered, the glyph both degrades with
|
|
425
|
+
`Glyphs.get('tick')` and appears in the font check via `Glyphs.probe_text()`.
|
|
426
|
+
* **The switches, with no argparse boilerplate.** The bundled helper adds them:
|
|
427
|
+
|
|
428
|
+
```python
|
|
429
|
+
parser = cli.add_display_arguments(argparse.ArgumentParser())
|
|
430
|
+
opts = parser.parse_args()
|
|
431
|
+
cli.apply_display_arguments(opts, prog='myapp')
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
`--ascii` forces the fallback (`--ascii=no` forces the full set) and
|
|
435
|
+
`--glyphs {auto,ascii,unicode}` is its symmetric spelling; `--glyphs` wins if
|
|
436
|
+
both are given. `--theme` seeds the first window's colour theme and
|
|
437
|
+
`--no-color` draws the semantic roles with bold/reverse attributes instead of
|
|
438
|
+
colour. Apps without argparse can use
|
|
439
|
+
`CONSOLE_WINDOW_GLYPHS=auto|ascii|unicode`, which outranks detection.
|
|
440
|
+
* **The mode is process-wide**, like the colour-pair table, because there is one
|
|
441
|
+
terminal. `ConsoleWindowOpts(ascii=...)` seeds it and `win.ascii_mode` reads
|
|
442
|
+
it; a window constructed later never reverts an explicit choice.
|
|
443
|
+
* **Help screens can explain it for you.** Return
|
|
444
|
+
`help.theme_paragraph()` from `Screen.extra_help_paragraphs()` and set
|
|
445
|
+
`show_display_notes = True` on a `BasicHelpScreen` subclass to append the font
|
|
446
|
+
check (Unicode mode) or the way back to the full set (ASCII mode). Both are
|
|
447
|
+
off by default, so an upgrading app's help output is unchanged.
|
|
448
|
+
|
|
449
|
+
Themes work the same way: `Theme.create_picker_screen(base, cycle_key=None,
|
|
450
|
+
store=None, note=None)` discovers the key you bound to `spin_theme` for its hint
|
|
451
|
+
line, remembers the choice through any object with `load()`/`save()`, and
|
|
452
|
+
`Theme.initial(cli_theme, store)` returns the theme a new process should start
|
|
453
|
+
on. The library never opens a file.
|
|
454
|
+
|
|
455
|
+
The appearance layer is shared too. `console_window.appearance.semantic_attr(role)`
|
|
456
|
+
returns the installed colour pair for a semantic role, or the matching attribute
|
|
457
|
+
from `MONO_ROLES` when colour is off; it degrades to plain text when the terminal
|
|
458
|
+
has no colour or curses has not been initialised, so a redraw is never aborted by
|
|
459
|
+
a colour. `set_colors_enabled()` / `colors_enabled()` hold the process-wide
|
|
460
|
+
switch, and `resolve_appearance(cli_theme=None, no_color=False, saved_theme='',
|
|
461
|
+
saved_colors=True, environ=None)` returns the `(theme, colors)` to start with:
|
|
462
|
+
`--no-color` first, then an explicit `--theme`, then `NO_COLOR`, then the
|
|
463
|
+
remembered choice. Apps keep their preference file and their own pseudo-themes;
|
|
464
|
+
the library gives them the policy, not the storage.
|
|
465
|
+
|
|
369
466
|
## Versioning and Deprecation
|
|
370
467
|
|
|
371
468
|
`console-window` follows semantic versioning: patch releases fix behaviour
|
|
@@ -418,3 +515,4 @@ For more extensive examples, you can look at some projects using `console-window
|
|
|
418
515
|
* viewer of /proc/meminfo that shows historical values
|
|
419
516
|
* [vappman](https://github.com/joedefen/vappman)
|
|
420
517
|
* convenince wrapper to mostly hide CLI of [AppMan](https://github.com/ivan-hc/AppMan), a life-cycle manager for 2000+ AppImages
|
|
518
|
+
|
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: console-window
|
|
3
|
-
Version: 2.1.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
|
-
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
|
|
|
@@ -385,6 +366,72 @@ Mixing the two values raises `AssertionError` rather than silently changing what
|
|
|
385
366
|
teardown means. Use `singleton=False` in tests and in applications that create
|
|
386
367
|
independent windows (for example, one real window plus a headless one).
|
|
387
368
|
|
|
369
|
+
### Symbols on terminals that cannot draw them
|
|
370
|
+
|
|
371
|
+
The UI is built around a handful of non-ASCII symbols (`│`, `⮞`, `█`, `←`, `→`,
|
|
372
|
+
`·`, box drawing). A terminal that cannot encode them does not fail loudly: it
|
|
373
|
+
paints a box, or nothing, and the row quietly loses its meaning. `console-window`
|
|
374
|
+
therefore keeps every decorative symbol in one registry, in two spellings, and
|
|
375
|
+
degrades by itself:
|
|
376
|
+
|
|
377
|
+
* **No code change.** A window detects the terminal when it is constructed — a
|
|
378
|
+
non-UTF-8 output encoding or `TERM=dumb` selects the ASCII spellings — and
|
|
379
|
+
text handed to `add_header()` / `add_body()` / `draw()` / `status()` /
|
|
380
|
+
`flash()` is transliterated before it is measured, so widths and the
|
|
381
|
+
run-length attribute counts stay correct.
|
|
382
|
+
* **Only decoration is rewritten.** `Grüße` stays `Grüße`: the substitution table
|
|
383
|
+
contains registered glyphs, never arbitrary text. Set
|
|
384
|
+
`CONSOLE_WINDOW_GLYPH_AUDIT=1` to log each decorative code point you use but
|
|
385
|
+
did not register, with the exact `register()` call to add.
|
|
386
|
+
* **App vocabulary.** Register what only your app draws:
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
Glyphs.register_many({'tick': ('✓', 'OK')}, probe=('tick',))
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
or pass it with the window: `ConsoleWindowOpts(glyphs={'tick': ('✓', 'OK')},
|
|
393
|
+
glyphs_probe=('tick',))`. Once registered, the glyph both degrades with
|
|
394
|
+
`Glyphs.get('tick')` and appears in the font check via `Glyphs.probe_text()`.
|
|
395
|
+
* **The switches, with no argparse boilerplate.** The bundled helper adds them:
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
parser = cli.add_display_arguments(argparse.ArgumentParser())
|
|
399
|
+
opts = parser.parse_args()
|
|
400
|
+
cli.apply_display_arguments(opts, prog='myapp')
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`--ascii` forces the fallback (`--ascii=no` forces the full set) and
|
|
404
|
+
`--glyphs {auto,ascii,unicode}` is its symmetric spelling; `--glyphs` wins if
|
|
405
|
+
both are given. `--theme` seeds the first window's colour theme and
|
|
406
|
+
`--no-color` draws the semantic roles with bold/reverse attributes instead of
|
|
407
|
+
colour. Apps without argparse can use
|
|
408
|
+
`CONSOLE_WINDOW_GLYPHS=auto|ascii|unicode`, which outranks detection.
|
|
409
|
+
* **The mode is process-wide**, like the colour-pair table, because there is one
|
|
410
|
+
terminal. `ConsoleWindowOpts(ascii=...)` seeds it and `win.ascii_mode` reads
|
|
411
|
+
it; a window constructed later never reverts an explicit choice.
|
|
412
|
+
* **Help screens can explain it for you.** Return
|
|
413
|
+
`help.theme_paragraph()` from `Screen.extra_help_paragraphs()` and set
|
|
414
|
+
`show_display_notes = True` on a `BasicHelpScreen` subclass to append the font
|
|
415
|
+
check (Unicode mode) or the way back to the full set (ASCII mode). Both are
|
|
416
|
+
off by default, so an upgrading app's help output is unchanged.
|
|
417
|
+
|
|
418
|
+
Themes work the same way: `Theme.create_picker_screen(base, cycle_key=None,
|
|
419
|
+
store=None, note=None)` discovers the key you bound to `spin_theme` for its hint
|
|
420
|
+
line, remembers the choice through any object with `load()`/`save()`, and
|
|
421
|
+
`Theme.initial(cli_theme, store)` returns the theme a new process should start
|
|
422
|
+
on. The library never opens a file.
|
|
423
|
+
|
|
424
|
+
The appearance layer is shared too. `console_window.appearance.semantic_attr(role)`
|
|
425
|
+
returns the installed colour pair for a semantic role, or the matching attribute
|
|
426
|
+
from `MONO_ROLES` when colour is off; it degrades to plain text when the terminal
|
|
427
|
+
has no colour or curses has not been initialised, so a redraw is never aborted by
|
|
428
|
+
a colour. `set_colors_enabled()` / `colors_enabled()` hold the process-wide
|
|
429
|
+
switch, and `resolve_appearance(cli_theme=None, no_color=False, saved_theme='',
|
|
430
|
+
saved_colors=True, environ=None)` returns the `(theme, colors)` to start with:
|
|
431
|
+
`--no-color` first, then an explicit `--theme`, then `NO_COLOR`, then the
|
|
432
|
+
remembered choice. Apps keep their preference file and their own pseudo-themes;
|
|
433
|
+
the library gives them the policy, not the storage.
|
|
434
|
+
|
|
388
435
|
## Versioning and Deprecation
|
|
389
436
|
|
|
390
437
|
`console-window` follows semantic versioning: patch releases fix behaviour
|
|
@@ -437,4 +484,3 @@ For more extensive examples, you can look at some projects using `console-window
|
|
|
437
484
|
* viewer of /proc/meminfo that shows historical values
|
|
438
485
|
* [vappman](https://github.com/joedefen/vappman)
|
|
439
486
|
* convenince wrapper to mostly hide CLI of [AppMan](https://github.com/ivan-hc/AppMan), a life-cycle manager for 2000+ AppImages
|
|
440
|
-
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# Single source of truth for the package version: pyproject.toml declares
|
|
2
2
|
# ``dynamic = ["version"]``, so flit reads this value instead of duplicating it.
|
|
3
|
-
__version__ = "2.
|
|
3
|
+
__version__ = "2.2.0"
|
|
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
|
|
13
|
+
from .glyphs import Glyph, Glyphs
|
|
6
14
|
from .header import HeaderLine, Span
|
|
7
15
|
from .keys import NAVIGATION_KEYS
|
|
8
16
|
from .opts import ConsoleWindowOpts
|
|
@@ -15,18 +23,21 @@ from .screens import (
|
|
|
15
23
|
)
|
|
16
24
|
from .signals import reset_ctrl_c
|
|
17
25
|
from .spinner import OptionSpinner
|
|
18
|
-
from .theme import Theme
|
|
26
|
+
from .theme import Theme, ThemeStore
|
|
19
27
|
from .widgets import IncrementalSearchBar, InlineConfirmation, PromptLine
|
|
20
28
|
from .window import ConsoleWindow, TerminalTooSmallError
|
|
21
29
|
|
|
22
30
|
__all__ = [
|
|
23
31
|
'HOME_ST',
|
|
32
|
+
'MONO_ROLES',
|
|
24
33
|
'NAVIGATION_KEYS',
|
|
25
34
|
'BasicHelpScreen',
|
|
26
35
|
'BasicTextScreen',
|
|
27
36
|
'ConsoleWindow',
|
|
28
37
|
'ConsoleWindowOpts',
|
|
29
38
|
'Context',
|
|
39
|
+
'Glyph',
|
|
40
|
+
'Glyphs',
|
|
30
41
|
'HeaderLine',
|
|
31
42
|
'IncrementalSearchBar',
|
|
32
43
|
'InlineConfirmation',
|
|
@@ -37,5 +48,10 @@ __all__ = [
|
|
|
37
48
|
'Span',
|
|
38
49
|
'TerminalTooSmallError',
|
|
39
50
|
'Theme',
|
|
51
|
+
'ThemeStore',
|
|
52
|
+
'colors_enabled',
|
|
40
53
|
'reset_ctrl_c',
|
|
54
|
+
'resolve_appearance',
|
|
55
|
+
'semantic_attr',
|
|
56
|
+
'set_colors_enabled',
|
|
41
57
|
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Semantic colour roles, and one resolver for the colour switch.
|
|
2
|
+
|
|
3
|
+
Colour here is decoration, never information. Every role degrades to an
|
|
4
|
+
attribute when colour 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 colour
|
|
11
|
+
numbers. The library installs those pairs at startup and re-installs them
|
|
12
|
+
whenever a theme is applied, so changing the active theme recolours 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 colour-theme screen is a request for colour, 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 normalises 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 colour 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 colour on or off for the whole process."""
|
|
66
|
+
global _COLORS_ENABLED # one terminal, one process-wide switch
|
|
67
|
+
_COLORS_ENABLED = bool(enabled)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def colors_enabled() -> bool:
|
|
71
|
+
"""True while semantic roles resolve to colour pairs rather than attributes."""
|
|
72
|
+
return _COLORS_ENABLED
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def resolve_appearance(
|
|
76
|
+
cli_theme: str | None = None,
|
|
77
|
+
no_color: bool = False,
|
|
78
|
+
saved_theme: str | None = '',
|
|
79
|
+
saved_colors: bool = True,
|
|
80
|
+
environ: Mapping[str, str] | None = None,
|
|
81
|
+
) -> tuple[str, bool]:
|
|
82
|
+
"""Starting ``(theme, colors)`` from the switches, environment and memory.
|
|
83
|
+
|
|
84
|
+
Precedence, highest first: ``--no-color``, an explicit ``--theme`` (a
|
|
85
|
+
statement of intent, so it is honoured even when ``NO_COLOR`` is set), the
|
|
86
|
+
``NO_COLOR`` convention (any non-empty value disables colour, per
|
|
87
|
+
no-color.org), then the remembered choice.
|
|
88
|
+
|
|
89
|
+
``cli_theme``/``saved_theme`` are returned resolved to a known library
|
|
90
|
+
theme; ``''`` means "no explicit theme, keep the library default". An
|
|
91
|
+
unknown name resolves to ``'default'``, exactly like :meth:`Theme.resolve`.
|
|
92
|
+
|
|
93
|
+
The result is a decision, not an action: call
|
|
94
|
+
:func:`set_colors_enabled` with the second value (or pass both on to
|
|
95
|
+
whatever applies themes) when the process should start that way.
|
|
96
|
+
"""
|
|
97
|
+
env = os.environ if environ is None else environ
|
|
98
|
+
|
|
99
|
+
if no_color:
|
|
100
|
+
colors = False
|
|
101
|
+
elif cli_theme:
|
|
102
|
+
colors = True
|
|
103
|
+
elif env.get('NO_COLOR'):
|
|
104
|
+
colors = False
|
|
105
|
+
else:
|
|
106
|
+
colors = bool(saved_colors)
|
|
107
|
+
|
|
108
|
+
if cli_theme:
|
|
109
|
+
theme = Theme.resolve(cli_theme)
|
|
110
|
+
elif saved_theme:
|
|
111
|
+
theme = Theme.resolve(saved_theme)
|
|
112
|
+
else:
|
|
113
|
+
theme = ''
|
|
114
|
+
return theme, colors
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def semantic_attr(pair_id: int) -> int:
|
|
118
|
+
"""Attribute for a :class:`Theme` role, or plain when colour is off.
|
|
119
|
+
|
|
120
|
+
With colour off the role becomes an attribute from :data:`MONO_ROLES`.
|
|
121
|
+
Otherwise both ``curses.has_colors()`` and ``curses.color_pair()`` can raise
|
|
122
|
+
before ``initscr()`` (``color_pair()`` refuses to run until then), and a
|
|
123
|
+
failed lookup must never abort a redraw, so any failure degrades to plain
|
|
124
|
+
text. The ``except`` is deliberately broad for that reason: a guarded
|
|
125
|
+
redraw is worth more than a precise exception list, and the fallback is
|
|
126
|
+
always the same.
|
|
127
|
+
"""
|
|
128
|
+
if not _COLORS_ENABLED:
|
|
129
|
+
return MONO_ROLES.get(pair_id, curses.A_NORMAL)
|
|
130
|
+
try:
|
|
131
|
+
if not curses.has_colors():
|
|
132
|
+
return 0
|
|
133
|
+
return curses.color_pair(pair_id)
|
|
134
|
+
except Exception: # terminal not initialised yet; never abort a redraw
|
|
135
|
+
return 0
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Argument-parser boilerplate for the display options.
|
|
2
|
+
|
|
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 colour theme, a way to turn semantic
|
|
5
|
+
colour off, and a way to print what was detected. This module supplies them,
|
|
6
|
+
so an app's ``main()`` gains two lines instead of sixty:
|
|
7
|
+
|
|
8
|
+
parser = cli.add_display_arguments(parser)
|
|
9
|
+
...
|
|
10
|
+
cli.apply_display_arguments(opts, prog='myapp')
|
|
11
|
+
|
|
12
|
+
``--ascii`` and ``--glyphs`` are deliberately symmetric. ``--ascii`` accepts an
|
|
13
|
+
optional boolean (``--ascii``, ``--ascii=no``) and ``--glyphs`` accepts a mode
|
|
14
|
+
(``auto``, ``ascii``, ``unicode``); ``--glyphs`` wins when both are given, and
|
|
15
|
+
"absent" everywhere means *detect*, never "assume Unicode".
|
|
16
|
+
|
|
17
|
+
``--no-color`` is the appearance switch. :func:`apply_display_arguments` acts on
|
|
18
|
+
it, and an app that remembers a colour preference can settle the whole decision
|
|
19
|
+
with :func:`console_window.appearance.resolve_appearance`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import sys
|
|
26
|
+
from collections.abc import Callable, Mapping
|
|
27
|
+
from typing import TextIO
|
|
28
|
+
|
|
29
|
+
from . import appearance, glyphs
|
|
30
|
+
from .glyphs import TerminalReport
|
|
31
|
+
from .theme import Theme, ThemeStore
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
'add_display_arguments',
|
|
35
|
+
'apply_display_arguments',
|
|
36
|
+
'resolve_force_ascii',
|
|
37
|
+
'resolve_no_color',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
_TRUE_WORDS = frozenset({'1', 'true', 'yes', 'y', 'on'})
|
|
41
|
+
_FALSE_WORDS = frozenset({'0', 'false', 'no', 'n', 'off'})
|
|
42
|
+
_MODES = ('auto', 'ascii', 'unicode')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _bool_argument(value: str) -> bool:
|
|
46
|
+
"""argparse ``type=`` for ``--ascii=VALUE``; bare ``--ascii`` uses ``const``."""
|
|
47
|
+
lowered = value.strip().lower()
|
|
48
|
+
if lowered in _TRUE_WORDS:
|
|
49
|
+
return True
|
|
50
|
+
if lowered in _FALSE_WORDS:
|
|
51
|
+
return False
|
|
52
|
+
raise argparse.ArgumentTypeError(
|
|
53
|
+
f'expected a boolean (yes/no, true/false, on/off), got {value!r}'
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def add_display_arguments(
|
|
58
|
+
parser: argparse.ArgumentParser,
|
|
59
|
+
*,
|
|
60
|
+
theme_store: ThemeStore | None = None,
|
|
61
|
+
prog: str | None = None,
|
|
62
|
+
) -> argparse.ArgumentParser:
|
|
63
|
+
"""Add ``--ascii``, ``--glyphs``, ``--theme`` and ``--no-color`` to *parser*.
|
|
64
|
+
|
|
65
|
+
Returns *parser* so a call site can wrap its own construction. *theme_store*
|
|
66
|
+
is accepted for symmetry with :func:`apply_display_arguments` (an app that
|
|
67
|
+
passes one here is one that remembers the choice); it does not change the
|
|
68
|
+
switches. *prog* only shapes the help text.
|
|
69
|
+
"""
|
|
70
|
+
name = prog or parser.prog or 'this program'
|
|
71
|
+
remembered = ' (remembered for the next start)' if theme_store is not None else ''
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
'--ascii',
|
|
74
|
+
nargs='?',
|
|
75
|
+
const=True,
|
|
76
|
+
default=None,
|
|
77
|
+
type=_bool_argument,
|
|
78
|
+
metavar='BOOL',
|
|
79
|
+
help=(
|
|
80
|
+
f'use plain-ASCII symbols; --ascii=no forces the Unicode set. '
|
|
81
|
+
f'Without the switch {name} detects the terminal'
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
'--glyphs',
|
|
86
|
+
choices=_MODES,
|
|
87
|
+
default=None,
|
|
88
|
+
help=(
|
|
89
|
+
'symbol mode: auto detects the terminal, ascii forces the fallback, '
|
|
90
|
+
'unicode forces the full set (same thing as --ascii / --ascii=no)'
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
'--theme',
|
|
95
|
+
choices=Theme.list_all(),
|
|
96
|
+
default=None,
|
|
97
|
+
help=f'color theme{remembered}',
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
'--no-color',
|
|
101
|
+
action='store_true',
|
|
102
|
+
default=None,
|
|
103
|
+
help=(
|
|
104
|
+
'draw the semantic roles with attributes instead of colour; wins '
|
|
105
|
+
'over --theme, and NO_COLOR does the same when the switch is absent'
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
return parser
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def resolve_force_ascii(args: argparse.Namespace) -> bool | None:
|
|
112
|
+
"""Map a parsed namespace to ``force_ascii``: True, False or None (detect)."""
|
|
113
|
+
mode = getattr(args, 'glyphs', None)
|
|
114
|
+
if mode == 'ascii':
|
|
115
|
+
return True
|
|
116
|
+
if mode == 'unicode':
|
|
117
|
+
return False
|
|
118
|
+
if mode == 'auto':
|
|
119
|
+
return None
|
|
120
|
+
flag = getattr(args, 'ascii', None)
|
|
121
|
+
if flag is None or isinstance(flag, bool):
|
|
122
|
+
return flag
|
|
123
|
+
raise TypeError(
|
|
124
|
+
f'args.ascii must be None or a bool (it is {flag!r}); parse it with '
|
|
125
|
+
'add_display_arguments() or pass a bool'
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def resolve_no_color(args: argparse.Namespace) -> bool:
|
|
130
|
+
"""Map a parsed namespace to the ``--no-color`` switch (absent means off)."""
|
|
131
|
+
return bool(getattr(args, 'no_color', False))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def apply_display_arguments(
|
|
135
|
+
args: argparse.Namespace,
|
|
136
|
+
*,
|
|
137
|
+
printer: Callable[[str], object] = print,
|
|
138
|
+
prog: str = '',
|
|
139
|
+
environ: Mapping[str, str] | None = None,
|
|
140
|
+
stream: TextIO | None = None,
|
|
141
|
+
) -> TerminalReport:
|
|
142
|
+
"""Set the glyph mode, print the report's notes, seed the theme, honour colour.
|
|
143
|
+
|
|
144
|
+
Call this once, *before* any window is created, so the mode is decided while
|
|
145
|
+
nothing has been drawn. Returns the :class:`~console_window.glyphs.
|
|
146
|
+
TerminalReport` so a caller can react to ``ascii_required`` (for example by
|
|
147
|
+
printing a warning of its own).
|
|
148
|
+
|
|
149
|
+
An explicit ``--no-color`` turns the process-wide colour switch off here; an
|
|
150
|
+
absent switch is left alone, so an app with a remembered preference is free
|
|
151
|
+
to call ``set_colors_enabled()`` with its own decision. The two halves of
|
|
152
|
+
that decision -- the theme to install and whether colour is on -- come from
|
|
153
|
+
:func:`console_window.appearance.resolve_appearance`.
|
|
154
|
+
|
|
155
|
+
*printer* is a callable taking one string; pass ``sys.stderr.write`` style
|
|
156
|
+
wrappers through ``lambda line: print(line, file=...)`` if needed.
|
|
157
|
+
"""
|
|
158
|
+
report = glyphs.apply_policy(
|
|
159
|
+
resolve_force_ascii(args),
|
|
160
|
+
environ=environ,
|
|
161
|
+
stream=sys.stdout if stream is None else stream,
|
|
162
|
+
)
|
|
163
|
+
for note in report.notes:
|
|
164
|
+
printer(f'{prog}: {note}' if prog else note)
|
|
165
|
+
|
|
166
|
+
if resolve_no_color(args):
|
|
167
|
+
# Only the explicit flag is acted on; "absent" belongs to the caller.
|
|
168
|
+
appearance.set_colors_enabled(False)
|
|
169
|
+
|
|
170
|
+
theme = getattr(args, 'theme', None)
|
|
171
|
+
if theme:
|
|
172
|
+
if theme in Theme.list_all():
|
|
173
|
+
# No window exists yet, so record the choice for the first one.
|
|
174
|
+
Theme.set_active(theme)
|
|
175
|
+
else:
|
|
176
|
+
message = f'unknown theme {theme!r}; keeping {Theme.resolve(None)!r}'
|
|
177
|
+
printer(f'{prog}: {message}' if prog else message)
|
|
178
|
+
return report
|