console-window 2.1.3__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.3/README.md → console_window-2.2.0/PKG-INFO +48 -4
- console_window-2.1.3/PKG-INFO → console_window-2.2.0/README.md +16 -24
- {console_window-2.1.3 → console_window-2.2.0}/console_window/__init__.py +13 -1
- console_window-2.2.0/console_window/appearance.py +135 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/cli.py +43 -10
- {console_window-2.1.3 → console_window-2.2.0}/pyproject.toml +51 -0
- console_window-2.1.3/.github/workflows/ci.yml +0 -66
- console_window-2.1.3/.gitignore +0 -207
- console_window-2.1.3/SampleApp.py +0 -593
- console_window-2.1.3/docs/GLYPH-MIGRATION-PLAN.md +0 -380
- console_window-2.1.3/runner +0 -13
- console_window-2.1.3/tests/AUDIT.md +0 -120
- console_window-2.1.3/tests/CHANGELOG.md +0 -624
- console_window-2.1.3/tests/conftest.py +0 -23
- console_window-2.1.3/tests/test_backend.py +0 -463
- console_window-2.1.3/tests/test_cli_display_args.py +0 -131
- console_window-2.1.3/tests/test_context.py +0 -45
- console_window-2.1.3/tests/test_demo_mode.py +0 -97
- console_window-2.1.3/tests/test_dialogs.py +0 -177
- console_window-2.1.3/tests/test_flash.py +0 -130
- console_window-2.1.3/tests/test_geometry.py +0 -116
- console_window-2.1.3/tests/test_glyph_policy.py +0 -141
- console_window-2.1.3/tests/test_glyph_rendering.py +0 -124
- console_window-2.1.3/tests/test_glyph_transliteration.py +0 -107
- console_window-2.1.3/tests/test_glyphs.py +0 -121
- console_window-2.1.3/tests/test_header.py +0 -309
- console_window-2.1.3/tests/test_interactive.py +0 -725
- console_window-2.1.3/tests/test_low_colour.py +0 -221
- console_window-2.1.3/tests/test_overflow.py +0 -175
- console_window-2.1.3/tests/test_prompt_line.py +0 -133
- console_window-2.1.3/tests/test_prompt_window.py +0 -288
- console_window-2.1.3/tests/test_public_api.py +0 -133
- console_window-2.1.3/tests/test_render_retry.py +0 -88
- console_window-2.1.3/tests/test_sample_app.py +0 -225
- console_window-2.1.3/tests/test_screens.py +0 -172
- console_window-2.1.3/tests/test_signals.py +0 -166
- console_window-2.1.3/tests/test_spinner.py +0 -276
- console_window-2.1.3/tests/test_status.py +0 -240
- console_window-2.1.3/tests/test_taken_nav_keys.py +0 -251
- console_window-2.1.3/tests/test_textwidth.py +0 -197
- console_window-2.1.3/tests/test_theme.py +0 -218
- console_window-2.1.3/tests/test_theme_picker.py +0 -165
- console_window-2.1.3/tests/test_viewport_pad.py +0 -152
- console_window-2.1.3/tests/test_widechars.py +0 -194
- console_window-2.1.3/tests/test_widgets.py +0 -297
- {console_window-2.1.3 → console_window-2.2.0}/LICENSE +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/backend.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/context.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/geometry.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/glyphs.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/header.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/help.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/keys.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/opts.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/screens.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/signals.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/spinner.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/textwidth.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/theme.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/widgets.py +0 -0
- {console_window-2.1.3 → console_window-2.2.0}/console_window/window.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
|
|
|
@@ -392,7 +423,7 @@ degrades by itself:
|
|
|
392
423
|
or pass it with the window: `ConsoleWindowOpts(glyphs={'tick': ('✓', 'OK')},
|
|
393
424
|
glyphs_probe=('tick',))`. Once registered, the glyph both degrades with
|
|
394
425
|
`Glyphs.get('tick')` and appears in the font check via `Glyphs.probe_text()`.
|
|
395
|
-
* **
|
|
426
|
+
* **The switches, with no argparse boilerplate.** The bundled helper adds them:
|
|
396
427
|
|
|
397
428
|
```python
|
|
398
429
|
parser = cli.add_display_arguments(argparse.ArgumentParser())
|
|
@@ -402,9 +433,10 @@ degrades by itself:
|
|
|
402
433
|
|
|
403
434
|
`--ascii` forces the fallback (`--ascii=no` forces the full set) and
|
|
404
435
|
`--glyphs {auto,ascii,unicode}` is its symmetric spelling; `--glyphs` wins if
|
|
405
|
-
both are given. `--theme` seeds the first window's colour theme
|
|
406
|
-
|
|
407
|
-
|
|
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.
|
|
408
440
|
* **The mode is process-wide**, like the colour-pair table, because there is one
|
|
409
441
|
terminal. `ConsoleWindowOpts(ascii=...)` seeds it and `win.ascii_mode` reads
|
|
410
442
|
it; a window constructed later never reverts an explicit choice.
|
|
@@ -420,6 +452,17 @@ line, remembers the choice through any object with `load()`/`save()`, and
|
|
|
420
452
|
`Theme.initial(cli_theme, store)` returns the theme a new process should start
|
|
421
453
|
on. The library never opens a file.
|
|
422
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
|
+
|
|
423
466
|
## Versioning and Deprecation
|
|
424
467
|
|
|
425
468
|
`console-window` follows semantic versioning: patch releases fix behaviour
|
|
@@ -472,3 +515,4 @@ For more extensive examples, you can look at some projects using `console-window
|
|
|
472
515
|
* viewer of /proc/meminfo that shows historical values
|
|
473
516
|
* [vappman](https://github.com/joedefen/vappman)
|
|
474
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.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
|
|
|
@@ -411,7 +392,7 @@ degrades by itself:
|
|
|
411
392
|
or pass it with the window: `ConsoleWindowOpts(glyphs={'tick': ('✓', 'OK')},
|
|
412
393
|
glyphs_probe=('tick',))`. Once registered, the glyph both degrades with
|
|
413
394
|
`Glyphs.get('tick')` and appears in the font check via `Glyphs.probe_text()`.
|
|
414
|
-
* **
|
|
395
|
+
* **The switches, with no argparse boilerplate.** The bundled helper adds them:
|
|
415
396
|
|
|
416
397
|
```python
|
|
417
398
|
parser = cli.add_display_arguments(argparse.ArgumentParser())
|
|
@@ -421,9 +402,10 @@ degrades by itself:
|
|
|
421
402
|
|
|
422
403
|
`--ascii` forces the fallback (`--ascii=no` forces the full set) and
|
|
423
404
|
`--glyphs {auto,ascii,unicode}` is its symmetric spelling; `--glyphs` wins if
|
|
424
|
-
both are given. `--theme` seeds the first window's colour theme
|
|
425
|
-
|
|
426
|
-
|
|
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.
|
|
427
409
|
* **The mode is process-wide**, like the colour-pair table, because there is one
|
|
428
410
|
terminal. `ConsoleWindowOpts(ascii=...)` seeds it and `win.ascii_mode` reads
|
|
429
411
|
it; a window constructed later never reverts an explicit choice.
|
|
@@ -439,6 +421,17 @@ line, remembers the choice through any object with `load()`/`save()`, and
|
|
|
439
421
|
`Theme.initial(cli_theme, store)` returns the theme a new process should start
|
|
440
422
|
on. The library never opens a file.
|
|
441
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
|
+
|
|
442
435
|
## Versioning and Deprecation
|
|
443
436
|
|
|
444
437
|
`console-window` follows semantic versioning: patch releases fix behaviour
|
|
@@ -491,4 +484,3 @@ For more extensive examples, you can look at some projects using `console-window
|
|
|
491
484
|
* viewer of /proc/meminfo that shows historical values
|
|
492
485
|
* [vappman](https://github.com/joedefen/vappman)
|
|
493
486
|
* convenince wrapper to mostly hide CLI of [AppMan](https://github.com/ivan-hc/AppMan), a life-cycle manager for 2000+ AppImages
|
|
494
|
-
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# Single source of truth for the package version: pyproject.toml declares
|
|
2
2
|
# ``dynamic = ["version"]``, so flit reads this value instead of duplicating it.
|
|
3
|
-
__version__ = "2.
|
|
3
|
+
__version__ = "2.2.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
|
|
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,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
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
"""Argument-parser boilerplate for the display options.
|
|
2
2
|
|
|
3
|
-
Every curses app ends up writing the same
|
|
4
|
-
plain-ASCII symbol set, a way to pick a colour theme,
|
|
5
|
-
was detected. This module supplies them,
|
|
6
|
-
lines instead of sixty:
|
|
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
7
|
|
|
8
8
|
parser = cli.add_display_arguments(parser)
|
|
9
9
|
...
|
|
10
10
|
cli.apply_display_arguments(opts, prog='myapp')
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
boolean (``--ascii``, ``--ascii=no``) and ``--glyphs`` accepts a mode
|
|
12
|
+
``--ascii`` and ``--glyphs`` are deliberately symmetric. ``--ascii`` accepts an
|
|
13
|
+
optional boolean (``--ascii``, ``--ascii=no``) and ``--glyphs`` accepts a mode
|
|
14
14
|
(``auto``, ``ascii``, ``unicode``); ``--glyphs`` wins when both are given, and
|
|
15
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`.
|
|
16
20
|
"""
|
|
17
21
|
|
|
18
22
|
from __future__ import annotations
|
|
@@ -22,11 +26,16 @@ import sys
|
|
|
22
26
|
from collections.abc import Callable, Mapping
|
|
23
27
|
from typing import TextIO
|
|
24
28
|
|
|
25
|
-
from . import glyphs
|
|
29
|
+
from . import appearance, glyphs
|
|
26
30
|
from .glyphs import TerminalReport
|
|
27
31
|
from .theme import Theme, ThemeStore
|
|
28
32
|
|
|
29
|
-
__all__ = [
|
|
33
|
+
__all__ = [
|
|
34
|
+
'add_display_arguments',
|
|
35
|
+
'apply_display_arguments',
|
|
36
|
+
'resolve_force_ascii',
|
|
37
|
+
'resolve_no_color',
|
|
38
|
+
]
|
|
30
39
|
|
|
31
40
|
_TRUE_WORDS = frozenset({'1', 'true', 'yes', 'y', 'on'})
|
|
32
41
|
_FALSE_WORDS = frozenset({'0', 'false', 'no', 'n', 'off'})
|
|
@@ -51,7 +60,7 @@ def add_display_arguments(
|
|
|
51
60
|
theme_store: ThemeStore | None = None,
|
|
52
61
|
prog: str | None = None,
|
|
53
62
|
) -> argparse.ArgumentParser:
|
|
54
|
-
"""Add ``--ascii``, ``--glyphs`` and ``--
|
|
63
|
+
"""Add ``--ascii``, ``--glyphs``, ``--theme`` and ``--no-color`` to *parser*.
|
|
55
64
|
|
|
56
65
|
Returns *parser* so a call site can wrap its own construction. *theme_store*
|
|
57
66
|
is accepted for symmetry with :func:`apply_display_arguments` (an app that
|
|
@@ -87,6 +96,15 @@ def add_display_arguments(
|
|
|
87
96
|
default=None,
|
|
88
97
|
help=f'color theme{remembered}',
|
|
89
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
|
+
)
|
|
90
108
|
return parser
|
|
91
109
|
|
|
92
110
|
|
|
@@ -108,6 +126,11 @@ def resolve_force_ascii(args: argparse.Namespace) -> bool | None:
|
|
|
108
126
|
)
|
|
109
127
|
|
|
110
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
|
+
|
|
111
134
|
def apply_display_arguments(
|
|
112
135
|
args: argparse.Namespace,
|
|
113
136
|
*,
|
|
@@ -116,13 +139,19 @@ def apply_display_arguments(
|
|
|
116
139
|
environ: Mapping[str, str] | None = None,
|
|
117
140
|
stream: TextIO | None = None,
|
|
118
141
|
) -> TerminalReport:
|
|
119
|
-
"""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, honour colour.
|
|
120
143
|
|
|
121
144
|
Call this once, *before* any window is created, so the mode is decided while
|
|
122
145
|
nothing has been drawn. Returns the :class:`~console_window.glyphs.
|
|
123
146
|
TerminalReport` so a caller can react to ``ascii_required`` (for example by
|
|
124
147
|
printing a warning of its own).
|
|
125
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
|
+
|
|
126
155
|
*printer* is a callable taking one string; pass ``sys.stderr.write`` style
|
|
127
156
|
wrappers through ``lambda line: print(line, file=...)`` if needed.
|
|
128
157
|
"""
|
|
@@ -134,6 +163,10 @@ def apply_display_arguments(
|
|
|
134
163
|
for note in report.notes:
|
|
135
164
|
printer(f'{prog}: {note}' if prog else note)
|
|
136
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
|
+
|
|
137
170
|
theme = getattr(args, 'theme', None)
|
|
138
171
|
if theme:
|
|
139
172
|
if theme in Theme.list_all():
|
|
@@ -34,12 +34,50 @@ classifiers = [
|
|
|
34
34
|
"Topic :: Software Development :: User Interfaces",
|
|
35
35
|
]
|
|
36
36
|
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
# The minimum a contributor needs to run the suite, and the release toolchain.
|
|
39
|
+
# `./bless` refuses to start unless `twine` and `packaging` meet its own floors
|
|
40
|
+
# (`twine>=7`, `packaging>=26`), so the dev extra installs a toolchain the gate
|
|
41
|
+
# accepts. `tomli` is the 3.10 fallback for `bless` and for the tests' use of
|
|
42
|
+
# `tomllib`; `requires-python` still allows 3.10.
|
|
43
|
+
test = [
|
|
44
|
+
'pytest >= 7.0',
|
|
45
|
+
'pytest-cov >= 4.0',
|
|
46
|
+
'tomli >= 2.0; python_version < "3.11"',
|
|
47
|
+
]
|
|
48
|
+
dev = [
|
|
49
|
+
'pytest >= 7.0',
|
|
50
|
+
'pytest-cov >= 4.0',
|
|
51
|
+
'ruff >= 0.9',
|
|
52
|
+
'build >= 1.2',
|
|
53
|
+
'twine >= 7',
|
|
54
|
+
'packaging >= 26',
|
|
55
|
+
'tomli >= 2.0; python_version < "3.11"',
|
|
56
|
+
]
|
|
57
|
+
|
|
37
58
|
[project.urls]
|
|
38
59
|
Homepage = "https://github.com/joedefen/console-window"
|
|
39
60
|
Repository = "https://github.com/joedefen/console-window"
|
|
40
61
|
Issues = "https://github.com/joedefen/console-window/issues"
|
|
41
62
|
Changelog = "https://github.com/joedefen/console-window/blob/main/CHANGELOG.md"
|
|
42
63
|
|
|
64
|
+
# ----------------------------------------------------------------------
|
|
65
|
+
# RELEASE GATE CONFIGURATION
|
|
66
|
+
[tool.bless]
|
|
67
|
+
# The branch model `./bless` keeps. `["main"]` releases from the trunk alone;
|
|
68
|
+
# `["main", "dev"]` -- the default when this table says nothing -- requires a
|
|
69
|
+
# `dev` branch both locally and on origin, and fast-forwards whichever of the
|
|
70
|
+
# two lags onto the other before tagging. This tree commits to the trunk, so a
|
|
71
|
+
# release is `main` and the tag.
|
|
72
|
+
branches = ["main"]
|
|
73
|
+
|
|
74
|
+
# The rest is derived rather than written here: the distribution name comes
|
|
75
|
+
# from `[project] name`, the package holding `__version__` from that name with
|
|
76
|
+
# `-` and `.` turned into `_`, and the inspector from
|
|
77
|
+
# `tests/inspect_dist.py`. There is no `[project.scripts]` entry on purpose:
|
|
78
|
+
# this is a library, so the smoke stage of `bless` imports the installed
|
|
79
|
+
# package and prints `__version__` instead of running a console script.
|
|
80
|
+
|
|
43
81
|
[tool.pytest.ini_options]
|
|
44
82
|
testpaths = ["tests"]
|
|
45
83
|
# Pad overflow is a UserWarning rather than an error, by design: a user's app
|
|
@@ -48,6 +86,19 @@ testpaths = ["tests"]
|
|
|
48
86
|
# sample app's too-small head pad and its truncated help screen were found.
|
|
49
87
|
filterwarnings = ["error::UserWarning"]
|
|
50
88
|
|
|
89
|
+
[tool.coverage.run]
|
|
90
|
+
# Pin the measured package so the number the release gate prints is the same
|
|
91
|
+
# whichever directory pytest is started from.
|
|
92
|
+
source = ["console_window"]
|
|
93
|
+
omit = ["tests/*"]
|
|
94
|
+
|
|
95
|
+
[tool.coverage.report]
|
|
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 optimise;
|
|
98
|
+
# raise it when the number rises, never lower it.
|
|
99
|
+
fail_under = 90
|
|
100
|
+
show_missing = true
|
|
101
|
+
|
|
51
102
|
# ----------------------------------------------------------------------
|
|
52
103
|
# RUFF LINT CONFIGURATION
|
|
53
104
|
[tool.ruff]
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
pull_request:
|
|
6
|
-
|
|
7
|
-
jobs:
|
|
8
|
-
test:
|
|
9
|
-
name: tests (Python ${{ matrix.python-version }})
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
strategy:
|
|
12
|
-
fail-fast: false
|
|
13
|
-
matrix:
|
|
14
|
-
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
17
|
-
- name: Set up Python
|
|
18
|
-
uses: actions/setup-python@v5
|
|
19
|
-
with:
|
|
20
|
-
python-version: ${{ matrix.python-version }}
|
|
21
|
-
- name: Install test dependencies
|
|
22
|
-
run: python -m pip install pytest==9.1.1 pytest-cov==7.1.0
|
|
23
|
-
- name: Run tests (TTY-free) with coverage floor
|
|
24
|
-
run: python -m pytest -q --cov=console_window --cov-fail-under=80
|
|
25
|
-
|
|
26
|
-
lint:
|
|
27
|
-
name: ruff
|
|
28
|
-
runs-on: ubuntu-latest
|
|
29
|
-
steps:
|
|
30
|
-
- uses: actions/checkout@v4
|
|
31
|
-
- name: Set up Python
|
|
32
|
-
uses: actions/setup-python@v5
|
|
33
|
-
with:
|
|
34
|
-
python-version: "3.13"
|
|
35
|
-
- name: Install ruff
|
|
36
|
-
run: python -m pip install ruff==0.17.0
|
|
37
|
-
- name: Lint
|
|
38
|
-
run: ruff check .
|
|
39
|
-
|
|
40
|
-
types:
|
|
41
|
-
name: mypy
|
|
42
|
-
runs-on: ubuntu-latest
|
|
43
|
-
steps:
|
|
44
|
-
- uses: actions/checkout@v4
|
|
45
|
-
- name: Set up Python
|
|
46
|
-
uses: actions/setup-python@v5
|
|
47
|
-
with:
|
|
48
|
-
python-version: "3.13"
|
|
49
|
-
- name: Install mypy
|
|
50
|
-
run: python -m pip install mypy==2.4.0
|
|
51
|
-
- name: Type check
|
|
52
|
-
run: python -m mypy
|
|
53
|
-
|
|
54
|
-
pylint:
|
|
55
|
-
name: pylint
|
|
56
|
-
runs-on: ubuntu-latest
|
|
57
|
-
steps:
|
|
58
|
-
- uses: actions/checkout@v4
|
|
59
|
-
- name: Set up Python
|
|
60
|
-
uses: actions/setup-python@v5
|
|
61
|
-
with:
|
|
62
|
-
python-version: "3.13"
|
|
63
|
-
- name: Install pylint
|
|
64
|
-
run: python -m pip install pylint==3.3.4
|
|
65
|
-
- name: Lint
|
|
66
|
-
run: python -m pylint console_window SampleApp.py runner --disable=C,R
|