console-window 2.0.0.7__tar.gz → 2.0.0.8__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.8}/CHANGELOG.md +10 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/PKG-INFO +4 -1
- {console_window-2.0.0.7 → console_window-2.0.0.8}/README.md +3 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/__init__.py +1 -1
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/window.py +41 -3
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_backend.py +78 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/.github/workflows/ci.yml +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/.gitignore +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/AUDIT.md +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/LICENSE +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/SampleApp.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/backend.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/context.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/geometry.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/header.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/keys.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/opts.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/screens.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/signals.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/spinner.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/textwidth.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/theme.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/console_window/widgets.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/pyproject.toml +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/runner +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_context.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_demo_mode.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_dialogs.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_flash.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_geometry.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_header.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_interactive.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_low_colour.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_overflow.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_public_api.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_render_retry.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_sample_app.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_screens.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_signals.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_spinner.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_textwidth.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_theme.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_viewport_pad.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_widechars.py +0 -0
- {console_window-2.0.0.7 → console_window-2.0.0.8}/tests/test_widgets.py +0 -0
|
@@ -135,6 +135,16 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
135
135
|
No in-repo code used it.
|
|
136
136
|
|
|
137
137
|
### Fixed
|
|
138
|
+
- `win._start_curses()` once again *reattaches* the window. The 1.4.x
|
|
139
|
+
compatibility hook is still called in the wild (psistat's "dump event log"
|
|
140
|
+
path uses `self.window._start_curses()` to return from shell mode), but the
|
|
141
|
+
2.0 implementation only created and returned a screen, leaving `win.scr` as
|
|
142
|
+
`None`; the next `clear()`/`render()` then raised
|
|
143
|
+
`AttributeError: 'NoneType' object has no attribute 'clear'`. It now resumes
|
|
144
|
+
a suspended window through `resume_curses()` (singleton mode reattaches every
|
|
145
|
+
window sharing the screen), so the pads and the content model survive, and
|
|
146
|
+
`clear()` while suspended resets the content model without dereferencing the
|
|
147
|
+
missing screen.
|
|
138
148
|
- Starting a window on a terminal with fewer than 256 colours (including tmux's
|
|
139
149
|
stock `TERM=screen`, whose terminfo entry has 8 colours) no longer crashes with
|
|
140
150
|
`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.8
|
|
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
|
|
@@ -223,6 +223,9 @@ screen handle, backend, dimensions, pad backgrounds and redraw bookkeeping.
|
|
|
223
223
|
In singleton mode `suspend_curses()` suspends every window that shares the
|
|
224
224
|
screen, and `resume_curses()` reattaches all of them. The legacy pair works the
|
|
225
225
|
same way there: `ConsoleWindow.stop_curses(); ...; ConsoleWindow.start_curses()`.
|
|
226
|
+
The pre-2.0 instance hook `win._start_curses()` is also supported: it reattaches
|
|
227
|
+
that window (resuming a suspended screen) and returns the live screen, so the
|
|
228
|
+
old `win.stop_curses(); ...; win._start_curses()` sequence keeps working.
|
|
226
229
|
Only `stop_all()` is a permanent teardown.
|
|
227
230
|
|
|
228
231
|
The first successful construction latches the mode for the whole process.
|
|
@@ -204,6 +204,9 @@ screen handle, backend, dimensions, pad backgrounds and redraw bookkeeping.
|
|
|
204
204
|
In singleton mode `suspend_curses()` suspends every window that shares the
|
|
205
205
|
screen, and `resume_curses()` reattaches all of them. The legacy pair works the
|
|
206
206
|
same way there: `ConsoleWindow.stop_curses(); ...; ConsoleWindow.start_curses()`.
|
|
207
|
+
The pre-2.0 instance hook `win._start_curses()` is also supported: it reattaches
|
|
208
|
+
that window (resuming a suspended screen) and returns the live screen, so the
|
|
209
|
+
old `win.stop_curses(); ...; win._start_curses()` sequence keeps working.
|
|
207
210
|
Only `stop_all()` is a permanent teardown.
|
|
208
211
|
|
|
209
212
|
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.8"
|
|
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
|
|
|
@@ -2149,7 +2182,12 @@ class ConsoleWindow:
|
|
|
2149
2182
|
pad.refresh(scroll_top, 0, row0 + 1, col0 + 1, row0 + visible_rows, col9 - 1)
|
|
2150
2183
|
|
|
2151
2184
|
def clear(self) -> None:
|
|
2152
|
-
|
|
2185
|
+
# A suspended window has no screen handle until resume_curses()/
|
|
2186
|
+
# start_curses(); the content model still has to be reset so the next
|
|
2187
|
+
# repaint is complete. Calling clear() while suspended used to raise
|
|
2188
|
+
# AttributeError on self.scr.
|
|
2189
|
+
if self.scr is not None:
|
|
2190
|
+
self.scr.clear()
|
|
2153
2191
|
# Viewport-sized pads: clearing costs one screenful, not one high-water
|
|
2154
2192
|
# mark, and no shrink/grow decision is needed at a frame boundary.
|
|
2155
2193
|
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
|
|
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
|
|
File without changes
|