Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
9db7236
Stage 3: choose between reserved and legacy toolbar rendering
tleonhardt Sep 10, 2026
4d9cb58
Stage 3: own the reservation and its bindings for one command loop
tleonhardt Sep 10, 2026
3025372
Stage 3: hide the native toolbar window and paint the band instead
tleonhardt Sep 10, 2026
5210e85
Stage 3: choose the mode and own the toolbar for the command loop
tleonhardt Sep 10, 2026
c253de5
Stage 3 review: roll back a failed startup, and restore only what is …
tleonhardt Sep 10, 2026
d1ef338
Stage 3 review: put the terminal back after a half-written paint
tleonhardt Sep 10, 2026
411b124
Stage 3 review: only restore a cursor this paint actually saved
tleonhardt Sep 10, 2026
4071b6c
Stage 3: treat the cursor as unknown after a failed paint, and fall b…
tleonhardt Sep 10, 2026
c3a8c34
Stage 3: write command output through the terminal transaction
tleonhardt Sep 10, 2026
db9b04d
Stage 3: install the serializer for commands running under a reservation
tleonhardt Sep 10, 2026
dd9ec8c
Stage 3 review: invalidate after a failed write, and decide the desti…
tleonhardt Sep 10, 2026
7c4a664
Stage 3: route the application's own renders through the bridge
tleonhardt Sep 10, 2026
5c08606
Stage 3 review: release before falling back, and invalidate on the fa…
tleonhardt Sep 10, 2026
b01eeab
Stage 3 review: route the real abandonment through the owner, and mar…
tleonhardt Sep 10, 2026
ea8aaa9
Stage 3: separate pausing the display from giving the terminal away
tleonhardt Sep 10, 2026
6059956
Stage 3 review: stop the display in serialized mode, and forget what …
tleonhardt Sep 10, 2026
991f9a9
Stage 3 review: only the outermost suspension takes the rows back
tleonhardt Sep 10, 2026
bc3adad
Stage 3: route cursor reports through the bridge, and paint on commit…
tleonhardt Sep 10, 2026
471095d
Stage 3 review: keep the main prompt inside the reservation, and gate…
tleonhardt Sep 10, 2026
5fac73e
Stage 3 review: bound the display's teardown, not just the wait for it
tleonhardt Sep 10, 2026
aaccfcb
Stage 3 review: a pause that timed out relinquished nothing, and now …
tleonhardt Sep 10, 2026
aab9e23
Stage 3 review: refuse the terminal while a display that would not st…
tleonhardt Sep 10, 2026
c9a5abe
Stage 3 review: finish the deferred teardown before letting go of the…
tleonhardt Sep 10, 2026
21a0041
Stage 3: cover the command loop's own lifetime
tleonhardt Sep 10, 2026
8e70d3c
Fix two test-only failures CI found and this machine could not
tleonhardt Sep 10, 2026
8f745a2
Stage 3 review: an unbound bridge passes calls through instead of bre…
tleonhardt Sep 10, 2026
1a6a556
Stage 3 review: clear passes through when unbound, like the rest
tleonhardt Sep 10, 2026
290e9bc
Make the toolbar modes a StrEnum instead of loose strings
tleonhardt Sep 11, 2026
d36e36b
Fix the docs build the toolbar mode page broke
tleonhardt Sep 11, 2026
1c934de
Merge reserved_row_toolbar into stage3-lifecycle-integration
tleonhardt Sep 11, 2026
61d2969
Consolidate toolbar enablement into ToolbarMode and speed up lifecycl…
tleonhardt Sep 11, 2026
cb61617
Speed up toolbar tests with explicit synchronization
tleonhardt Sep 11, 2026
2621908
Reduce pager and subprocess test overhead
tleonhardt Sep 11, 2026
a734941
Fix reserved toolbar corruption at the terminal bottom
tleonhardt Sep 11, 2026
ec1cb98
Fix intermittent toolbar refresh assertion on Windows
tleonhardt Sep 11, 2026
de5cf49
Show reserved toolbar truncation and clip lines without wrapping
tleonhardt Sep 11, 2026
2184b71
Mark reserved toolbar truncation only for visible content, and show c…
tleonhardt Sep 11, 2026
5f640c1
Merge branch 'reserved_row_toolbar' into stage3-lifecycle-integration
tleonhardt Sep 12, 2026
08c7355
Establish the prompt origin natively on Windows, and fall back instea…
tleonhardt Sep 12, 2026
f3e8009
Give the corrupt-history tests their own temp files
tleonhardt Sep 12, 2026
61e7fd9
Fix reserved toolbar resize and partial-output redraws
tleonhardt Sep 12, 2026
ef04db1
Render the built-in pager in reserved toolbar mode
tleonhardt Sep 12, 2026
32f48d2
Fix partial-output loss at shutdown and two resize lifecycle defects
tleonhardt Sep 12, 2026
974290b
Clear suppression when inactive and preserve output across resize
tleonhardt Sep 12, 2026
98e62d5
Move displaced output out of the band when a resize shrinks onto it
tleonhardt Sep 12, 2026
756dbbb
Fixed a few edge-case bugs
tleonhardt Sep 12, 2026
1fe8bc1
Address five review findings on the edge-case fixes
tleonhardt Sep 12, 2026
33647c9
Keep the fallback layout when the pager closes
tleonhardt Sep 12, 2026
690c38e
Harden the pager's exit and the fallback under an open pager
tleonhardt Sep 12, 2026
e1b04c3
Fix the reserved-to-legacy fallback during paging on a console-less t…
tleonhardt Sep 12, 2026
bc69f36
Restore command display state after pager exit failures
tleonhardt Sep 12, 2026
e219606
Wait for completed resize redraws in terminal tests
tleonhardt Sep 12, 2026
0c8c09f
Preserve Windows VT processing for serialized command output
tleonhardt Sep 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Stage 3 review: clear passes through when unbound, like the rest
The pass-through guard reached every intercepted method except this one. A
retained delegating wrapper therefore had a retired bridge take its lock and
invalidate its state on someone else's behalf -- and taking the lock is not
harmless: the caller may hold a higher-level lock, and the ordering rule
forbids that nesting, so acting as owner turned someone else's clear into an
exception.

The test could not have caught it. It asserted the delegated call returned,
which it did whenever nothing else held a lock. It now asserts what the guard
is actually for: no terminal transaction is taken, under a higher-level lock,
and the retired bridge's own state is left alone.
  • Loading branch information
tleonhardt committed Sep 10, 2026
commit 1a6a5561756f28bebc757a483591bf4c182ca47a
7 changes: 7 additions & 0 deletions cmd2/prompt_toolkit_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,14 @@ def _clear_through_bridge(self) -> None:
the remembered origin is forgotten rather than carried across -- recovery would
otherwise place the next frame where the prompt used to be. Cursor reports already in
flight describe the screen before the clear and are discarded with it.

Unbound, this passes straight through, for the reason given on the render wrapper: a
retired bridge is not the terminal's owner, and taking its lock or invalidating its
state on someone else's behalf would be acting as one.
"""
if not self._bound:
self._originals["clear"]()
return
self._last_emission_committed = False
with self._lock.transaction("clear"):
try:
Expand Down
34 changes: 31 additions & 3 deletions tests/test_prompt_toolkit_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
ReservedModeFailureError,
)
from cmd2.terminal_display import TerminalDisplay
from cmd2.terminal_transaction import TerminalLock, current_transaction
from cmd2.terminal_transaction import HigherLevelLock, TerminalLock, current_transaction


class TtyStringIO(io.StringIO):
Expand Down Expand Up @@ -1514,7 +1514,13 @@ def tracing_fire() -> None:
assert fired == [1]
assert handled == [1]

def test_delegated_erase_and_clear_still_work_after_unbinding(self) -> None:
def test_delegated_erase_and_clear_take_no_transaction_after_unbinding(self) -> None:
"""A retired bridge is not the terminal's owner and must not act as one.

Taking its lock is not harmless: the caller may hold a higher-level lock, and the
ordering rule forbids that nesting -- so acting as owner turns someone else's clear
into an exception.
"""
harness = self.bound()
harness.renderer.request_absolute_cursor_position = lambda: None # type: ignore[method-assign]
erase, clear = harness.renderer.erase, harness.renderer.clear
Expand All @@ -1528,11 +1534,33 @@ def passing_clear() -> None:
harness.renderer.erase = passing_erase # type: ignore[method-assign]
harness.renderer.clear = passing_clear # type: ignore[method-assign]
harness.bridge.unbind()
harness.bridge.require_resynchronization("before the delegated calls")

with set_app(harness.app):
harness.stream_recorder.transactions.clear()
with set_app(harness.app), HigherLevelLock("routing"):
harness.renderer.erase()
harness.renderer.clear()

assert harness.stream_recorder.transactions
assert all(state is None for state in harness.stream_recorder.transactions)

def test_a_delegated_clear_does_not_invalidate_the_retired_bridge(self) -> None:
"""Its state describes a terminal it no longer owns; changing it means nothing."""
harness = self.bound()
harness.renderer.request_absolute_cursor_position = lambda: None # type: ignore[method-assign]
harness.bridge.set_prompt_anchor(7)
clear = harness.renderer.clear

def passing_clear() -> None:
clear()

harness.renderer.clear = passing_clear # type: ignore[method-assign]
harness.bridge.unbind()

with set_app(harness.app):
harness.renderer.clear()
assert harness.bridge.prompt_anchor == 7

def test_delegated_cursor_reports_still_work_after_unbinding(self) -> None:
harness = self.bound()
request = harness.renderer.request_absolute_cursor_position
Expand Down