console-window 2.0.0.7__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.
- {console_window-2.0.0.7 → console_window-2.0.0.9}/CHANGELOG.md +14 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/PKG-INFO +7 -2
- {console_window-2.0.0.7 → console_window-2.0.0.9}/README.md +6 -1
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/__init__.py +1 -1
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/window.py +83 -21
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_backend.py +78 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_header.py +54 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/.github/workflows/ci.yml +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/.gitignore +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/AUDIT.md +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/LICENSE +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/SampleApp.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/backend.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/context.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/geometry.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/header.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/keys.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/opts.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/screens.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/signals.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/spinner.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/textwidth.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/theme.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/console_window/widgets.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/pyproject.toml +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/runner +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_context.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_demo_mode.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_dialogs.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_flash.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_geometry.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_interactive.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_low_colour.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_overflow.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_public_api.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_render_retry.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_sample_app.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_screens.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_signals.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_spinner.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_textwidth.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_theme.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_viewport_pad.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.9}/tests/test_widechars.py +0 -0
- {console_window-2.0.0.7 → 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
|
|
@@ -135,6 +139,16 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
135
139
|
No in-repo code used it.
|
|
136
140
|
|
|
137
141
|
### Fixed
|
|
142
|
+
- `win._start_curses()` once again *reattaches* the window. The 1.4.x
|
|
143
|
+
compatibility hook is still called in the wild (psistat's "dump event log"
|
|
144
|
+
path uses `self.window._start_curses()` to return from shell mode), but the
|
|
145
|
+
2.0 implementation only created and returned a screen, leaving `win.scr` as
|
|
146
|
+
`None`; the next `clear()`/`render()` then raised
|
|
147
|
+
`AttributeError: 'NoneType' object has no attribute 'clear'`. It now resumes
|
|
148
|
+
a suspended window through `resume_curses()` (singleton mode reattaches every
|
|
149
|
+
window sharing the screen), so the pads and the content model survive, and
|
|
150
|
+
`clear()` while suspended resets the content model without dereferencing the
|
|
151
|
+
missing screen.
|
|
138
152
|
- Starting a window on a terminal with fewer than 256 colours (including tmux's
|
|
139
153
|
stock `TERM=screen`, whose terminfo entry has 8 colours) no longer crashes with
|
|
140
154
|
`ValueError: Color number is greater than COLORS-1`. Theme installation now
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: console-window
|
|
3
|
-
Version: 2.0.0.
|
|
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
|
|
@@ -223,6 +225,9 @@ screen handle, backend, dimensions, pad backgrounds and redraw bookkeeping.
|
|
|
223
225
|
In singleton mode `suspend_curses()` suspends every window that shares the
|
|
224
226
|
screen, and `resume_curses()` reattaches all of them. The legacy pair works the
|
|
225
227
|
same way there: `ConsoleWindow.stop_curses(); ...; ConsoleWindow.start_curses()`.
|
|
228
|
+
The pre-2.0 instance hook `win._start_curses()` is also supported: it reattaches
|
|
229
|
+
that window (resuming a suspended screen) and returns the live screen, so the
|
|
230
|
+
old `win.stop_curses(); ...; win._start_curses()` sequence keeps working.
|
|
226
231
|
Only `stop_all()` is a permanent teardown.
|
|
227
232
|
|
|
228
233
|
The first successful construction latches the mode for the whole process.
|
|
@@ -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
|
|
@@ -204,6 +206,9 @@ screen handle, backend, dimensions, pad backgrounds and redraw bookkeeping.
|
|
|
204
206
|
In singleton mode `suspend_curses()` suspends every window that shares the
|
|
205
207
|
screen, and `resume_curses()` reattaches all of them. The legacy pair works the
|
|
206
208
|
same way there: `ConsoleWindow.stop_curses(); ...; ConsoleWindow.start_curses()`.
|
|
209
|
+
The pre-2.0 instance hook `win._start_curses()` is also supported: it reattaches
|
|
210
|
+
that window (resuming a suspended screen) and returns the live screen, so the
|
|
211
|
+
old `win.stop_curses(); ...; win._start_curses()` sequence keeps working.
|
|
207
212
|
Only `stop_all()` is a permanent teardown.
|
|
208
213
|
|
|
209
214
|
The first successful construction latches the mode for the whole process.
|
|
@@ -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.
|
|
3
|
+
__version__ = "2.0.0.9"
|
|
4
4
|
|
|
5
5
|
from .context import Context
|
|
6
6
|
from .header import HeaderLine, Span
|
|
@@ -291,7 +291,7 @@ class ConsoleWindow:
|
|
|
291
291
|
self._backend = ConsoleWindow._static_backend or self._backend
|
|
292
292
|
self.scr = ConsoleWindow.static_scr
|
|
293
293
|
else:
|
|
294
|
-
self.scr = self.
|
|
294
|
+
self.scr = self._new_screen()
|
|
295
295
|
if self.singleton:
|
|
296
296
|
ConsoleWindow.static_scr = self.scr
|
|
297
297
|
ConsoleWindow._static_backend = self._backend
|
|
@@ -581,9 +581,42 @@ class ConsoleWindow:
|
|
|
581
581
|
|
|
582
582
|
return True
|
|
583
583
|
|
|
584
|
-
def
|
|
584
|
+
def _new_screen(self) -> CursesSurface:
|
|
585
|
+
"""Create and initialise a brand-new screen for this window."""
|
|
585
586
|
return self._init_screen(self._backend, self.theme)
|
|
586
587
|
|
|
588
|
+
def _start_curses(self) -> CursesSurface:
|
|
589
|
+
"""Re-enter curses for this window and return the live screen.
|
|
590
|
+
|
|
591
|
+
Pre-2.0 exposed ``_start_curses()`` as the "come back after
|
|
592
|
+
``stop_curses()``" call, and applications (psistat among them) kept
|
|
593
|
+
calling it on the instance. It therefore has to *reattach* the
|
|
594
|
+
window, not merely create a screen: the old implementation returned a
|
|
595
|
+
fresh screen without storing it, so ``self.scr`` stayed ``None`` and
|
|
596
|
+
the first ``clear()``/``render()`` after returning from shell mode
|
|
597
|
+
raised ``AttributeError: 'NoneType' object has no attribute 'clear'``.
|
|
598
|
+
|
|
599
|
+
A suspended window resumes through :meth:`resume_curses` (in singleton
|
|
600
|
+
mode that reattaches every window sharing the screen), so the pads and
|
|
601
|
+
the content model survive. ``start_curses()`` remains the class-level
|
|
602
|
+
entry point for both models.
|
|
603
|
+
"""
|
|
604
|
+
if self.suspended:
|
|
605
|
+
scr = self.resume_curses()
|
|
606
|
+
if scr is not None:
|
|
607
|
+
return scr
|
|
608
|
+
if self.scr is not None:
|
|
609
|
+
return self.scr
|
|
610
|
+
# Not suspended but screen-less: restart a screen and attach this
|
|
611
|
+
# window to it. Singleton mode owns a process-wide screen, so reuse the
|
|
612
|
+
# class-level starter; per-instance windows create their own.
|
|
613
|
+
scr = ConsoleWindow.start_curses() if self.singleton else self._new_screen()
|
|
614
|
+
if self.scr is None: # start_curses() may have reattached it already
|
|
615
|
+
backend = (ConsoleWindow._static_backend if self.singleton else None) \
|
|
616
|
+
or self._backend
|
|
617
|
+
self._attach_screen(scr, backend)
|
|
618
|
+
return scr
|
|
619
|
+
|
|
587
620
|
def _attach_screen(self, scr: CursesSurface, backend: RenderBackend) -> None:
|
|
588
621
|
"""Point this window at a (re)started screen and force a full repaint.
|
|
589
622
|
|
|
@@ -952,7 +985,8 @@ class ConsoleWindow:
|
|
|
952
985
|
self._add(self.body, text, attr, resume, context, is_body=True)
|
|
953
986
|
|
|
954
987
|
def add_fancy_header(self, line: str | HeaderLine, mode: str = 'Underline',
|
|
955
|
-
search_pattern_attr: int | None = None
|
|
988
|
+
search_pattern_attr: int | None = None,
|
|
989
|
+
line_attr: int | None = None) -> None:
|
|
956
990
|
"""Add a header line with legacy ``[x]``/``KEY:``/``/pattern`` markup.
|
|
957
991
|
|
|
958
992
|
String input is parsed unless ``mode='Off'``. A :class:`HeaderLine`
|
|
@@ -960,44 +994,67 @@ class ConsoleWindow:
|
|
|
960
994
|
treated as markup too: in every mode except ``'Off'`` they are removed,
|
|
961
995
|
while the enclosed key keeps its ``KEY`` styling. Use
|
|
962
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.
|
|
963
1002
|
"""
|
|
964
1003
|
if isinstance(line, str):
|
|
965
1004
|
if mode == 'Off':
|
|
966
|
-
# Legacy: no markup processing, add the raw line unchanged
|
|
967
|
-
|
|
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)
|
|
968
1008
|
return
|
|
969
1009
|
spans = parse_fancy_header(line)
|
|
970
1010
|
else:
|
|
971
1011
|
spans = list(line)
|
|
972
1012
|
if mode != 'Off':
|
|
973
1013
|
spans = strip_key_brackets(spans)
|
|
974
|
-
self._emit_header_spans(spans, mode, search_pattern_attr)
|
|
1014
|
+
self._emit_header_spans(spans, mode, search_pattern_attr, line_attr)
|
|
975
1015
|
|
|
976
1016
|
def add_header_line(self, header_line: HeaderLine | Iterable[Span],
|
|
977
1017
|
mode: str = 'Underline',
|
|
978
|
-
search_pattern_attr: int | None = None
|
|
979
|
-
|
|
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)
|
|
980
1028
|
|
|
981
1029
|
@staticmethod
|
|
982
1030
|
def _resolve_header_attr(role: str, mode: str,
|
|
983
|
-
search_pattern_attr: int | None
|
|
1031
|
+
search_pattern_attr: int | None,
|
|
1032
|
+
line_attr: int | None = None) -> int | None:
|
|
984
1033
|
if mode == 'Off' or role == PLAIN:
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1034
|
+
attr = None
|
|
1035
|
+
elif role == LABEL:
|
|
1036
|
+
attr = curses.A_BOLD
|
|
1037
|
+
elif role == SEARCH:
|
|
989
1038
|
if search_pattern_attr is not None:
|
|
990
|
-
|
|
991
|
-
|
|
1039
|
+
attr = search_pattern_attr
|
|
1040
|
+
else:
|
|
1041
|
+
attr = curses.A_BOLD | curses.A_REVERSE
|
|
992
1042
|
# KEY (and any unknown role) follows the mode's key styling.
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
|
996
1050
|
|
|
997
1051
|
def _emit_header_spans(self, spans: Iterable[Span], mode: str,
|
|
998
|
-
search_pattern_attr: int | None = None
|
|
1052
|
+
search_pattern_attr: int | None = None,
|
|
1053
|
+
line_attr: int | None = None) -> None:
|
|
999
1054
|
result_sections = [
|
|
1000
|
-
(span.text,
|
|
1055
|
+
(span.text,
|
|
1056
|
+
self._resolve_header_attr(span.role, mode, search_pattern_attr,
|
|
1057
|
+
line_attr))
|
|
1001
1058
|
for span in spans
|
|
1002
1059
|
]
|
|
1003
1060
|
|
|
@@ -2149,7 +2206,12 @@ class ConsoleWindow:
|
|
|
2149
2206
|
pad.refresh(scroll_top, 0, row0 + 1, col0 + 1, row0 + visible_rows, col9 - 1)
|
|
2150
2207
|
|
|
2151
2208
|
def clear(self) -> None:
|
|
2152
|
-
|
|
2209
|
+
# A suspended window has no screen handle until resume_curses()/
|
|
2210
|
+
# start_curses(); the content model still has to be reset so the next
|
|
2211
|
+
# repaint is complete. Calling clear() while suspended used to raise
|
|
2212
|
+
# AttributeError on self.scr.
|
|
2213
|
+
if self.scr is not None:
|
|
2214
|
+
self.scr.clear()
|
|
2153
2215
|
# Viewport-sized pads: clearing costs one screenful, not one high-water
|
|
2154
2216
|
# mark, and no shrink/grow decision is needed at a frame boundary.
|
|
2155
2217
|
self.head.pad.clear()
|
|
@@ -355,6 +355,84 @@ def test_per_instance_suspend_and_resume_curses():
|
|
|
355
355
|
assert win not in ConsoleWindow.active_windows()
|
|
356
356
|
|
|
357
357
|
|
|
358
|
+
def test_clear_while_suspended_only_resets_the_content_model():
|
|
359
|
+
# A suspended window has no screen, but clear() must still work: the next
|
|
360
|
+
# repaint after resume comes from the content model, not a stale screen.
|
|
361
|
+
backend = HeadlessBackend()
|
|
362
|
+
win = _make(backend)
|
|
363
|
+
try:
|
|
364
|
+
win.add_body('old row')
|
|
365
|
+
win.suspend_curses()
|
|
366
|
+
win.clear() # regression: raised AttributeError on self.scr
|
|
367
|
+
assert win.body.texts == []
|
|
368
|
+
assert win.scr is None
|
|
369
|
+
win.add_body('new row')
|
|
370
|
+
scr = win._start_curses()
|
|
371
|
+
assert win.scr is scr
|
|
372
|
+
assert win.body.texts == ['new row']
|
|
373
|
+
win.clear() # the live screen is cleared again
|
|
374
|
+
assert scr.calls_named('clear')
|
|
375
|
+
finally:
|
|
376
|
+
win.stop_curses()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_legacy_instance_start_curses_reattaches_after_suspend():
|
|
380
|
+
# psistat's dump/return path calls self.window._start_curses() (the 1.4.x
|
|
381
|
+
# hook that was kept as a compatibility method). It must leave the window
|
|
382
|
+
# usable, not create a screen and leave self.scr as None.
|
|
383
|
+
backend = HeadlessBackend()
|
|
384
|
+
win = _make(backend)
|
|
385
|
+
try:
|
|
386
|
+
win.add_body('kept across the dump')
|
|
387
|
+
win.suspend_curses()
|
|
388
|
+
assert win.scr is None and win.suspended
|
|
389
|
+
|
|
390
|
+
scr = win._start_curses()
|
|
391
|
+
assert scr is not None
|
|
392
|
+
assert win.scr is scr
|
|
393
|
+
assert win.suspended is False
|
|
394
|
+
assert win in ConsoleWindow.active_windows()
|
|
395
|
+
assert win.body.texts == ['kept across the dump']
|
|
396
|
+
win.clear() # the call that crashed psistat after returning
|
|
397
|
+
assert scr.calls_named('clear')
|
|
398
|
+
win.add_body('after clear')
|
|
399
|
+
finally:
|
|
400
|
+
win.stop_curses()
|
|
401
|
+
assert win.scr is None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def test_singleton_legacy_instance_start_curses_resumes_shared_screen():
|
|
405
|
+
# Same compatibility hook, but in the pre-2.0 singleton model, where
|
|
406
|
+
# stop_curses() suspends the shared screen rather than tearing it down.
|
|
407
|
+
code = '''
|
|
408
|
+
from console_window.backend import HeadlessBackend
|
|
409
|
+
from console_window.opts import ConsoleWindowOpts
|
|
410
|
+
from console_window.window import ConsoleWindow
|
|
411
|
+
|
|
412
|
+
backend = HeadlessBackend()
|
|
413
|
+
win = ConsoleWindow(opts=ConsoleWindowOpts(body_rows=3, head_rows=2),
|
|
414
|
+
backend=backend, singleton=True)
|
|
415
|
+
win.add_body('kept across the dump')
|
|
416
|
+
win.stop_curses() # pre-2.0 call: suspend
|
|
417
|
+
assert win.scr is None and win.suspended
|
|
418
|
+
|
|
419
|
+
scr = win._start_curses() # psistat's "come back" call
|
|
420
|
+
assert scr is not None
|
|
421
|
+
assert win.scr is scr
|
|
422
|
+
assert ConsoleWindow.static_scr is scr
|
|
423
|
+
assert win.suspended is False
|
|
424
|
+
assert win in ConsoleWindow.active_windows()
|
|
425
|
+
assert win.body.texts == ['kept across the dump']
|
|
426
|
+
win.clear()
|
|
427
|
+
assert scr.calls_named('clear')
|
|
428
|
+
win.add_body('after clear')
|
|
429
|
+
|
|
430
|
+
ConsoleWindow.stop_all() # permanent teardown
|
|
431
|
+
assert win.scr is None and ConsoleWindow.static_scr is None
|
|
432
|
+
'''
|
|
433
|
+
subprocess.run([sys.executable, '-c', code], check=True)
|
|
434
|
+
|
|
435
|
+
|
|
358
436
|
def test_mixing_singleton_modes_asserts():
|
|
359
437
|
code = '''
|
|
360
438
|
from console_window.backend import HeadlessBackend
|
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|