console-window 2.0.0.8__tar.gz → 2.0.0.9__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 (45) hide show
  1. {console_window-2.0.0.8 → console_window-2.0.0.9}/CHANGELOG.md +4 -0
  2. {console_window-2.0.0.8 → console_window-2.0.0.9}/PKG-INFO +4 -2
  3. {console_window-2.0.0.8 → console_window-2.0.0.9}/README.md +3 -1
  4. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/__init__.py +1 -1
  5. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/window.py +42 -18
  6. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_header.py +54 -0
  7. {console_window-2.0.0.8 → console_window-2.0.0.9}/.github/workflows/ci.yml +0 -0
  8. {console_window-2.0.0.8 → console_window-2.0.0.9}/.gitignore +0 -0
  9. {console_window-2.0.0.8 → console_window-2.0.0.9}/AUDIT.md +0 -0
  10. {console_window-2.0.0.8 → console_window-2.0.0.9}/LICENSE +0 -0
  11. {console_window-2.0.0.8 → console_window-2.0.0.9}/SampleApp.py +0 -0
  12. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/backend.py +0 -0
  13. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/context.py +0 -0
  14. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/geometry.py +0 -0
  15. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/header.py +0 -0
  16. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/keys.py +0 -0
  17. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/opts.py +0 -0
  18. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/screens.py +0 -0
  19. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/signals.py +0 -0
  20. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/spinner.py +0 -0
  21. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/textwidth.py +0 -0
  22. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/theme.py +0 -0
  23. {console_window-2.0.0.8 → console_window-2.0.0.9}/console_window/widgets.py +0 -0
  24. {console_window-2.0.0.8 → console_window-2.0.0.9}/pyproject.toml +0 -0
  25. {console_window-2.0.0.8 → console_window-2.0.0.9}/runner +0 -0
  26. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_backend.py +0 -0
  27. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_context.py +0 -0
  28. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_demo_mode.py +0 -0
  29. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_dialogs.py +0 -0
  30. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_flash.py +0 -0
  31. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_geometry.py +0 -0
  32. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_interactive.py +0 -0
  33. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_low_colour.py +0 -0
  34. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_overflow.py +0 -0
  35. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_public_api.py +0 -0
  36. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_render_retry.py +0 -0
  37. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_sample_app.py +0 -0
  38. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_screens.py +0 -0
  39. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_signals.py +0 -0
  40. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_spinner.py +0 -0
  41. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_textwidth.py +0 -0
  42. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_theme.py +0 -0
  43. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_viewport_pad.py +0 -0
  44. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_widechars.py +0 -0
  45. {console_window-2.0.0.8 → console_window-2.0.0.9}/tests/test_widgets.py +0 -0
@@ -6,6 +6,10 @@ This project follows [Semantic Versioning](https://semver.org/).
6
6
  ## [Unreleased]
7
7
 
8
8
  ### Added
9
+ - `line_attr` on `add_fancy_header()`/`add_header_line()`: an attribute OR'd
10
+ into every span of the header (including plain spans and the raw line emitted
11
+ for `mode='Off'`), so a caller can dim a whole fancy header while keeping the
12
+ per-span bold/underline/reverse styling.
9
13
  - `suspend_curses()`/`resume_curses()` on `ConsoleWindow`: leave curses mode
10
14
  with `endwin()` while keeping the window (pads, content model, pick state)
11
15
  alive, then reattach a screen and repaint. This is the supported way for an
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: console-window
3
- Version: 2.0.0.8
3
+ Version: 2.0.0.9
4
4
  Summary: A robust curses wrapper for creating Text User Interfaces (TUIs).
5
5
  Author-email: Joe Defen <joedef@duck.com>
6
6
  Requires-Python: >=3.10
@@ -122,7 +122,9 @@ demonstrates:
122
122
  sizes.
123
123
  * **`flash()`** — transient, non-destructive status messages.
124
124
  * **Token-based headers** — `HeaderLine` (`text`, `bracket_key`, `key`, `extend`)
125
- rendered with `add_header_line()`, rather than string markup.
125
+ rendered with `add_header_line()`, rather than string markup. Both header
126
+ helpers accept `line_attr`, an attribute OR'd into every span, so a whole
127
+ header can be dimmed (`curses.A_DIM`) without losing its per-span styling.
126
128
  * **`Ctrl-C` as an application key** — `ctrl_c_terminates=False` and
127
129
  `keys={0x3, ord('q')}` on the quit action. This works only while no live
128
130
  window in the process has requested terminating Ctrl-C: once any window uses
@@ -103,7 +103,9 @@ demonstrates:
103
103
  sizes.
104
104
  * **`flash()`** — transient, non-destructive status messages.
105
105
  * **Token-based headers** — `HeaderLine` (`text`, `bracket_key`, `key`, `extend`)
106
- rendered with `add_header_line()`, rather than string markup.
106
+ rendered with `add_header_line()`, rather than string markup. Both header
107
+ helpers accept `line_attr`, an attribute OR'd into every span, so a whole
108
+ header can be dimmed (`curses.A_DIM`) without losing its per-span styling.
107
109
  * **`Ctrl-C` as an application key** — `ctrl_c_terminates=False` and
108
110
  `keys={0x3, ord('q')}` on the quit action. This works only while no live
109
111
  window in the process has requested terminating Ctrl-C: once any window uses
@@ -1,6 +1,6 @@
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.0.0.8"
3
+ __version__ = "2.0.0.9"
4
4
 
5
5
  from .context import Context
6
6
  from .header import HeaderLine, Span
@@ -985,7 +985,8 @@ class ConsoleWindow:
985
985
  self._add(self.body, text, attr, resume, context, is_body=True)
986
986
 
987
987
  def add_fancy_header(self, line: str | HeaderLine, mode: str = 'Underline',
988
- search_pattern_attr: int | None = None) -> None:
988
+ search_pattern_attr: int | None = None,
989
+ line_attr: int | None = None) -> None:
989
990
  """Add a header line with legacy ``[x]``/``KEY:``/``/pattern`` markup.
990
991
 
991
992
  String input is parsed unless ``mode='Off'``. A :class:`HeaderLine`
@@ -993,44 +994,67 @@ class ConsoleWindow:
993
994
  treated as markup too: in every mode except ``'Off'`` they are removed,
994
995
  while the enclosed key keeps its ``KEY`` styling. Use
995
996
  :meth:`add_header_line` when literal brackets are wanted.
997
+
998
+ *line_attr* is OR'd into the attribute of every span (and into the raw
999
+ line emitted for ``mode='Off'``), so a caller can, for example, dim a
1000
+ whole header with ``line_attr=curses.A_DIM`` while keeping each span's
1001
+ own bold/underline/reverse styling.
996
1002
  """
997
1003
  if isinstance(line, str):
998
1004
  if mode == 'Off':
999
- # Legacy: no markup processing, add the raw line unchanged.
1000
- self.add_header(line)
1005
+ # Legacy: no markup processing, add the raw line unchanged
1006
+ # (except for the caller's whole-line attribute).
1007
+ self.add_header(line, attr=line_attr)
1001
1008
  return
1002
1009
  spans = parse_fancy_header(line)
1003
1010
  else:
1004
1011
  spans = list(line)
1005
1012
  if mode != 'Off':
1006
1013
  spans = strip_key_brackets(spans)
1007
- self._emit_header_spans(spans, mode, search_pattern_attr)
1014
+ self._emit_header_spans(spans, mode, search_pattern_attr, line_attr)
1008
1015
 
1009
1016
  def add_header_line(self, header_line: HeaderLine | Iterable[Span],
1010
1017
  mode: str = 'Underline',
1011
- search_pattern_attr: int | None = None) -> None:
1012
- self._emit_header_spans(list(header_line), mode, search_pattern_attr)
1018
+ search_pattern_attr: int | None = None,
1019
+ line_attr: int | None = None) -> None:
1020
+ """Render explicit header spans; see :meth:`add_fancy_header`.
1021
+
1022
+ *line_attr* is OR'd into every span's resolved attribute, including
1023
+ plain spans, so the whole line can carry an extra style such as
1024
+ ``curses.A_DIM``.
1025
+ """
1026
+ self._emit_header_spans(list(header_line), mode, search_pattern_attr,
1027
+ line_attr)
1013
1028
 
1014
1029
  @staticmethod
1015
1030
  def _resolve_header_attr(role: str, mode: str,
1016
- search_pattern_attr: int | None) -> int | None:
1031
+ search_pattern_attr: int | None,
1032
+ line_attr: int | None = None) -> int | None:
1017
1033
  if mode == 'Off' or role == PLAIN:
1018
- return None
1019
- if role == LABEL:
1020
- return curses.A_BOLD
1021
- if role == SEARCH:
1034
+ attr = None
1035
+ elif role == LABEL:
1036
+ attr = curses.A_BOLD
1037
+ elif role == SEARCH:
1022
1038
  if search_pattern_attr is not None:
1023
- return search_pattern_attr
1024
- return curses.A_BOLD | curses.A_REVERSE
1039
+ attr = search_pattern_attr
1040
+ else:
1041
+ attr = curses.A_BOLD | curses.A_REVERSE
1025
1042
  # KEY (and any unknown role) follows the mode's key styling.
1026
- if mode == 'Underline':
1027
- return curses.A_UNDERLINE | curses.A_BOLD
1028
- return curses.A_REVERSE
1043
+ elif mode == 'Underline':
1044
+ attr = curses.A_UNDERLINE | curses.A_BOLD
1045
+ else:
1046
+ attr = curses.A_REVERSE
1047
+ if line_attr is not None:
1048
+ attr = (attr or 0) | line_attr
1049
+ return attr
1029
1050
 
1030
1051
  def _emit_header_spans(self, spans: Iterable[Span], mode: str,
1031
- search_pattern_attr: int | None = None) -> None:
1052
+ search_pattern_attr: int | None = None,
1053
+ line_attr: int | None = None) -> None:
1032
1054
  result_sections = [
1033
- (span.text, self._resolve_header_attr(span.role, mode, search_pattern_attr))
1055
+ (span.text,
1056
+ self._resolve_header_attr(span.role, mode, search_pattern_attr,
1057
+ line_attr))
1034
1058
  for span in spans
1035
1059
  ]
1036
1060
 
@@ -207,6 +207,48 @@ def test_add_fancy_header_headerline_preserves_explicit_roles():
207
207
  win.stop_curses()
208
208
 
209
209
 
210
+ def test_add_fancy_header_line_attr_dims_every_run():
211
+ # line_attr is OR'd into each span's own style, so a dimmed legend keeps
212
+ # its underlined keys and its explicitly styled search field.
213
+ win = _make_window()
214
+ try:
215
+ win.add_fancy_header('[v]iew /pat', mode='Underline',
216
+ search_pattern_attr=curses.A_BOLD,
217
+ line_attr=curses.A_DIM)
218
+ assert win.head.texts[0] == 'view /pat'
219
+ attrs = [attr for attr, _count in win.head.text_attrs[0]]
220
+ assert all(attr & curses.A_DIM for attr in attrs)
221
+ assert KEY_ATTR | curses.A_DIM in attrs
222
+ assert curses.A_BOLD | curses.A_DIM in attrs
223
+ finally:
224
+ win.stop_curses()
225
+
226
+
227
+ def test_add_fancy_header_line_attr_applies_in_off_mode():
228
+ # mode='Off' emits the raw string, but the whole-line attribute still
229
+ # applies (otherwise a caller could not dim it at all).
230
+ win = _make_window()
231
+ try:
232
+ win.add_fancy_header('[v]iew', mode='Off', line_attr=curses.A_DIM)
233
+ assert win.head.texts[0] == '[v]iew'
234
+ assert all(attr & curses.A_DIM
235
+ for attr, _count in win.head.text_attrs[0])
236
+ finally:
237
+ win.stop_curses()
238
+
239
+
240
+ def test_add_header_line_line_attr_dims_plain_spans():
241
+ win = _make_window()
242
+ try:
243
+ win.add_header_line(HeaderLine().text('plain').label('T'),
244
+ mode='Underline', line_attr=curses.A_DIM)
245
+ attrs = [attr for attr, _count in win.head.text_attrs[0]]
246
+ assert curses.A_DIM in attrs # the PLAIN run
247
+ assert curses.A_BOLD | curses.A_DIM in attrs # the LABEL run
248
+ finally:
249
+ win.stop_curses()
250
+
251
+
210
252
  def test_string_reparse_drops_keys_the_legacy_grammar_cannot_spell():
211
253
  # str(line) is pure text, so the legacy scanner's two KEY rules ('[x]' and
212
254
  # 'UPPERCASE:') are all that survive a round trip. 'l:' and '?:' match
@@ -253,3 +295,15 @@ def test_token_and_string_paths_converge_for_legacy_spelled_keys():
253
295
 
254
296
  def test_resolve_header_attr_off_is_plain(role):
255
297
  assert ConsoleWindow._resolve_header_attr(role, 'Off', None) is None
298
+
299
+
300
+ def test_resolve_header_attr_line_attr_combines_with_roles():
301
+ assert (ConsoleWindow._resolve_header_attr(
302
+ PLAIN, 'Underline', None, curses.A_DIM)
303
+ == curses.A_DIM)
304
+ assert (ConsoleWindow._resolve_header_attr(
305
+ KEY, 'Underline', None, curses.A_DIM)
306
+ == KEY_ATTR | curses.A_DIM)
307
+ assert (ConsoleWindow._resolve_header_attr(
308
+ SEARCH, 'Underline', curses.A_BOLD, curses.A_DIM)
309
+ == curses.A_BOLD | curses.A_DIM)