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.
Files changed (61) hide show
  1. console_window-2.1.3/README.md → console_window-2.2.0/PKG-INFO +48 -4
  2. console_window-2.1.3/PKG-INFO → console_window-2.2.0/README.md +16 -24
  3. {console_window-2.1.3 → console_window-2.2.0}/console_window/__init__.py +13 -1
  4. console_window-2.2.0/console_window/appearance.py +135 -0
  5. {console_window-2.1.3 → console_window-2.2.0}/console_window/cli.py +43 -10
  6. {console_window-2.1.3 → console_window-2.2.0}/pyproject.toml +51 -0
  7. console_window-2.1.3/.github/workflows/ci.yml +0 -66
  8. console_window-2.1.3/.gitignore +0 -207
  9. console_window-2.1.3/SampleApp.py +0 -593
  10. console_window-2.1.3/docs/GLYPH-MIGRATION-PLAN.md +0 -380
  11. console_window-2.1.3/runner +0 -13
  12. console_window-2.1.3/tests/AUDIT.md +0 -120
  13. console_window-2.1.3/tests/CHANGELOG.md +0 -624
  14. console_window-2.1.3/tests/conftest.py +0 -23
  15. console_window-2.1.3/tests/test_backend.py +0 -463
  16. console_window-2.1.3/tests/test_cli_display_args.py +0 -131
  17. console_window-2.1.3/tests/test_context.py +0 -45
  18. console_window-2.1.3/tests/test_demo_mode.py +0 -97
  19. console_window-2.1.3/tests/test_dialogs.py +0 -177
  20. console_window-2.1.3/tests/test_flash.py +0 -130
  21. console_window-2.1.3/tests/test_geometry.py +0 -116
  22. console_window-2.1.3/tests/test_glyph_policy.py +0 -141
  23. console_window-2.1.3/tests/test_glyph_rendering.py +0 -124
  24. console_window-2.1.3/tests/test_glyph_transliteration.py +0 -107
  25. console_window-2.1.3/tests/test_glyphs.py +0 -121
  26. console_window-2.1.3/tests/test_header.py +0 -309
  27. console_window-2.1.3/tests/test_interactive.py +0 -725
  28. console_window-2.1.3/tests/test_low_colour.py +0 -221
  29. console_window-2.1.3/tests/test_overflow.py +0 -175
  30. console_window-2.1.3/tests/test_prompt_line.py +0 -133
  31. console_window-2.1.3/tests/test_prompt_window.py +0 -288
  32. console_window-2.1.3/tests/test_public_api.py +0 -133
  33. console_window-2.1.3/tests/test_render_retry.py +0 -88
  34. console_window-2.1.3/tests/test_sample_app.py +0 -225
  35. console_window-2.1.3/tests/test_screens.py +0 -172
  36. console_window-2.1.3/tests/test_signals.py +0 -166
  37. console_window-2.1.3/tests/test_spinner.py +0 -276
  38. console_window-2.1.3/tests/test_status.py +0 -240
  39. console_window-2.1.3/tests/test_taken_nav_keys.py +0 -251
  40. console_window-2.1.3/tests/test_textwidth.py +0 -197
  41. console_window-2.1.3/tests/test_theme.py +0 -218
  42. console_window-2.1.3/tests/test_theme_picker.py +0 -165
  43. console_window-2.1.3/tests/test_viewport_pad.py +0 -152
  44. console_window-2.1.3/tests/test_widechars.py +0 -194
  45. console_window-2.1.3/tests/test_widgets.py +0 -297
  46. {console_window-2.1.3 → console_window-2.2.0}/LICENSE +0 -0
  47. {console_window-2.1.3 → console_window-2.2.0}/console_window/backend.py +0 -0
  48. {console_window-2.1.3 → console_window-2.2.0}/console_window/context.py +0 -0
  49. {console_window-2.1.3 → console_window-2.2.0}/console_window/geometry.py +0 -0
  50. {console_window-2.1.3 → console_window-2.2.0}/console_window/glyphs.py +0 -0
  51. {console_window-2.1.3 → console_window-2.2.0}/console_window/header.py +0 -0
  52. {console_window-2.1.3 → console_window-2.2.0}/console_window/help.py +0 -0
  53. {console_window-2.1.3 → console_window-2.2.0}/console_window/keys.py +0 -0
  54. {console_window-2.1.3 → console_window-2.2.0}/console_window/opts.py +0 -0
  55. {console_window-2.1.3 → console_window-2.2.0}/console_window/screens.py +0 -0
  56. {console_window-2.1.3 → console_window-2.2.0}/console_window/signals.py +0 -0
  57. {console_window-2.1.3 → console_window-2.2.0}/console_window/spinner.py +0 -0
  58. {console_window-2.1.3 → console_window-2.2.0}/console_window/textwidth.py +0 -0
  59. {console_window-2.1.3 → console_window-2.2.0}/console_window/theme.py +0 -0
  60. {console_window-2.1.3 → console_window-2.2.0}/console_window/widgets.py +0 -0
  61. {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
- * **Two switches, no argparse boilerplate.** The bundled helper adds them:
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. Apps without
406
- argparse can use `CONSOLE_WINDOW_GLYPHS=auto|ascii|unicode`, which outranks
407
- detection.
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
- * **Two switches, no argparse boilerplate.** The bundled helper adds them:
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. Apps without
425
- argparse can use `CONSOLE_WINDOW_GLYPHS=auto|ascii|unicode`, which outranks
426
- detection.
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.1.3"
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 three switches: a way to force the
4
- plain-ASCII symbol set, a way to pick a colour theme, and a way to print what
5
- was detected. This module supplies them, so an app's ``main()`` gains two
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
- The two switches are deliberately symmetric. ``--ascii`` accepts an optional
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__ = ['add_display_arguments', 'apply_display_arguments', 'resolve_force_ascii']
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 ``--theme`` to *parser*.
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