You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bottom bar should persist during command execution #1743
It would be a better user experience if the bottom toolbar which is displayed when enable_bottom_toolbar=True persists during command execution and output.
Currently the bottom toolbar is only present when prompt-toolkit is at a prompt waiting for a user to finish entering a command. While the command is executing, it disappears. This can be a major annoyance to lose the visual status display when long-running commands are executing.
Ideally, the bottom toolbar would be completely separate and persist through everything, but it would be ok if it disappears in certain limited circumstances like when using a pager, piping to a shell command, or running a shell command.
There are differences in approach that impact user experience, and developer experience. I don't think one of them is obviously the right choice. So I would like feedback from other developers on what they like or don't like about each approach.
First, a framing correction
These aren't two independent solutions. persistent-bottom-bar's tip (6cb419d9) is a git ancestor of consolidated_toolbar — the entire PBB implementation is present verbatim in the consolidated branch. The real difference is one commit, aac8a4e7, plus two merge-backs of PBB fixes.
So ~90% of the design is shared, and the comparison is really about the delta.
The shared foundation (both branches)
cmd2/command_toolbar.py — command execution stays on the main thread; a prompt-toolkit UI runs on a background thread (cmd2-toolbar) so the toolbar keeps refreshing. Around that:
ToolbarStream wrappers give cmd.stdout / sys.stdout / sys.stderr stable identities across suspensions and cmd2 redirection, swapping the underlying sink between a StdoutProxy (prints above the bar) and the raw terminal, under an RLock.
_ContextStdoutProxy overrides _start_write_thread so the flush worker inherits the toolbar's create_app_session contextvar.
_ToolbarBuffer incremental-decodes subprocess bytes so a split UTF-8 sequence doesn't corrupt output.
Input coordination: the UI owns the keyboard (needed for CPR), stashes typed keys and re-injects them as typeahead at the next prompt; Ctrl-C is forwarded via killpg/interrupt_main; Ctrl-Z suspends.
Because there's now a single application to host views in, a third layout becomes possible: cmd2/pager.py (209 lines) — a TextArea + SearchToolbar + the same toolbar container. ppaged() measures with Pager.fits(), prints directly if it fits, otherwise swaps in the pager layout via _call_in_ui() (a Future-marshalled call onto the UI event loop) and blocks the command thread on pager.closed. New use_builtin_pager flag (defaults to enable_bottom_toolbar) opts back out to the external pager.
all of those plussession.layout.container shape, app.renderer.erase()/full_screen/request_absolute_cursor_position(), app.is_done, live mutation of layout/key_bindings/editing_mode/full_screen
Advantages / disadvantages
persistent-bottom-bar
✅ Strong isolation — command-time UI can't corrupt PromptSession state (buffer, history, search state, vi state, editing_mode).
✅ No dependency on the internal shape of PromptSession._create_layout().
✅ Smaller: no _call_in_ui, no Future marshalling, no pager.
❌ Duplicated display config drifts. Real, present bug: PBB hardcodes height=1, while PromptSession uses height=Dimension(min=1), dont_extend_height=True. A multiline bottom_toolbar renders fully at the prompt and is truncated to one row during commands. PBB's filter also drops the self.bottom_toolbar is not None clause. Every future upstream toolbar change has to be mirrored by hand.
❌ Paging blanks the toolbar — for a status/progress bar (the feature's whole point), that's the moment users most want it.
❌ No chopped-line paging on Windows.
consolidated_toolbar
✅ Zero display-config duplication. Reusing root.children[-1] means style, visibility filter, and multiline sizing are correct by construction, forever.
✅ Follows prompt-toolkit's own documented model (one Application, interchangeable layouts).
✅ Toolbar survives paging; consistent chop behavior cross-platform; use_builtin_pager=False preserves the old path.
❌ One guarded structural coupling to PromptSession's layout (isinstance(root, HSplit) + children[-1] + style check). It fails loudly, and is verified against 3.0.52/3.0.53 — but it is a coupling.
❌ Mutating a live shared Application is a wider blast radius. More restore paths to get right, and _call_in_ui + _check_running polling exist purely to serialize that. Latent trap: _CombinedRegistry._key_bindings caches merged bindings keyed only on (current_window, frozenset(controls)) and is not invalidated when app.key_bindings is reassigned — safe here only because each layout carries freshly constructed Windows. Worth a comment; it's the kind of thing a later refactor silently breaks.
❌ Reaches into app.renderer internals for the full-screen transition.
❌ +415 lines of new surface; the pager is a new feature riding along with an architecture change, which muddies review and blame.
Which do I think is "better"
This is a weak recommendation and I'm open to having my mind changed or to bring ideas from others: consolidated_toolbar — but for the architecture, not the pager
The decisive argument isn't the pager; it's that PBB's second application must hand-copy the toolbar's construction, and that copy is already wrong (the height=1 multiline truncation). That's not a bug you fix once — it's a structural obligation to re-mirror upstream on every prompt-toolkit bump. Consolidated deletes the obligation by reusing the object. Both branches already pay the hard costs of this feature (thread, stdout proxy, stream identity, typeahead, suspension protocol); consolidated removes one of the two remaining duplications rather than adding a category of complexity.
The counter-argument — isolation — is weaker than it looks. Both branches already run a prompt-toolkit app on the same input/output while the main thread is elsewhere; the coupling is inherent to the feature. Consolidated makes it explicit and reversible via ExitStack instead of implicit.
Options for splitting into multiple PRs
For the consolidated approach, the app-consolidation could be one reviewable change and the embedded pager could be a separate independent one that could go in first. They're independently valuable and independently riskier.
For the consolidated approach, we could also have a follow-on PR to add a regression test pinning the layout-shape assumption against the prompt-toolkit versions in pyproject.toml, and a comment at the key_bindings swap noting the un-invalidated binding cache.
There is now a 3rd branch which is continuing evolution of the overall architecture which is trying to arrive at a cleaner architecture from a prompt-toolkit application perspective where we just have a single Prompt Toolkit application running continuously and we dynamically swap layouts at different stages. This is being done in attempt at eliminating an annoying flicker of the bottom toolbar.
The WIP for this new continuous-toolbar-app branch is in PR #1748
Activity
tleonhardt commentedon Sep 5, 2026
Summary
I have two different architectural approaches for solving this problem currently published as two separate PRs:
There are differences in approach that impact user experience, and developer experience. I don't think one of them is obviously the right choice. So I would like feedback from other developers on what they like or don't like about each approach.
First, a framing correction
These aren't two independent solutions.
persistent-bottom-bar's tip (6cb419d9) is a git ancestor ofconsolidated_toolbar— the entire PBB implementation is present verbatim in the consolidated branch. The real difference is one commit,aac8a4e7, plus two merge-backs of PBB fixes.So ~90% of the design is shared, and the comparison is really about the delta.
The shared foundation (both branches)
cmd2/command_toolbar.py— command execution stays on the main thread; a prompt-toolkit UI runs on a background thread (cmd2-toolbar) so the toolbar keeps refreshing. Around that:ToolbarStreamwrappers givecmd.stdout/sys.stdout/sys.stderrstable identities across suspensions and cmd2 redirection, swapping the underlying sink between aStdoutProxy(prints above the bar) and the raw terminal, under anRLock._ContextStdoutProxyoverrides_start_write_threadso the flush worker inherits the toolbar'screate_app_sessioncontextvar._ToolbarBufferincremental-decodes subprocess bytes so a split UTF-8 sequence doesn't corrupt output.killpg/interrupt_main; Ctrl-Z suspends.suspend_toolbardecorator +suspend_bottom_toolbar()context manager step aside for nested prompts,select(),do_shell,_run_python,do_ipy, and pipe processes (pipe_target()+RedirectionSavedState.toolbar_suspension).Where they diverge
persistent-bottom-bar— two applicationsCommandToolbar.__init__constructs a second, privateApplicationwith its own hand-built layout:Paging stays external:
ppagedis decorated with@suspend_toolbarand shells out toless/more, hiding the bar for the duration.consolidated_toolbar— one application, interchangeable layoutsCommandToolbarborrowssession.appand swaps its attributes for the duration of a command, restoring them via anExitStack:Because there's now a single application to host views in, a third layout becomes possible:
cmd2/pager.py(209 lines) — aTextArea+SearchToolbar+ the same toolbar container.ppaged()measures withPager.fits(), prints directly if it fits, otherwise swaps in the pager layout via_call_in_ui()(aFuture-marshalled call onto the UI event loop) and blocks the command thread onpager.closed. Newuse_builtin_pagerflag (defaults toenable_bottom_toolbar) opts back out to the external pager.Comparison
less/more, toolbar hiddenmorewraps)get/store_typeahead,StdoutProxy._start_write_thread,app.loopsession.layout.containershape,app.renderer.erase()/full_screen/request_absolute_cursor_position(),app.is_done, live mutation oflayout/key_bindings/editing_mode/full_screenAdvantages / disadvantages
persistent-bottom-bar
PromptSessionstate (buffer, history, search state, vi state,editing_mode).PromptSession._create_layout()._call_in_ui, noFuturemarshalling, no pager.height=1, whilePromptSessionusesheight=Dimension(min=1), dont_extend_height=True. A multilinebottom_toolbarrenders fully at the prompt and is truncated to one row during commands. PBB's filter also drops theself.bottom_toolbar is not Noneclause. Every future upstream toolbar change has to be mirrored by hand.consolidated_toolbar
root.children[-1]means style, visibility filter, and multiline sizing are correct by construction, forever.Application, interchangeable layouts).use_builtin_pager=Falsepreserves the old path.PromptSession's layout (isinstance(root, HSplit)+children[-1]+ style check). It fails loudly, and is verified against 3.0.52/3.0.53 — but it is a coupling.Applicationis a wider blast radius. More restore paths to get right, and_call_in_ui+_check_runningpolling exist purely to serialize that. Latent trap:_CombinedRegistry._key_bindingscaches merged bindings keyed only on(current_window, frozenset(controls))and is not invalidated whenapp.key_bindingsis reassigned — safe here only because each layout carries freshly constructedWindows. Worth a comment; it's the kind of thing a later refactor silently breaks.app.rendererinternals for the full-screen transition.Which do I think is "better"
This is a weak recommendation and I'm open to having my mind changed or to bring ideas from others:
consolidated_toolbar— but for the architecture, not the pagerThe decisive argument isn't the pager; it's that PBB's second application must hand-copy the toolbar's construction, and that copy is already wrong (the
height=1multiline truncation). That's not a bug you fix once — it's a structural obligation to re-mirror upstream on every prompt-toolkit bump. Consolidated deletes the obligation by reusing the object. Both branches already pay the hard costs of this feature (thread, stdout proxy, stream identity, typeahead, suspension protocol); consolidated removes one of the two remaining duplications rather than adding a category of complexity.The counter-argument — isolation — is weaker than it looks. Both branches already run a prompt-toolkit app on the same
input/outputwhile the main thread is elsewhere; the coupling is inherent to the feature. Consolidated makes it explicit and reversible viaExitStackinstead of implicit.Options for splitting into multiple PRs
pyproject.toml, and a comment at thekey_bindingsswap noting the un-invalidated binding cache.tleonhardt commentedon Sep 6, 2026
@kmvanbrunt @bambu I would particularly value feedback from both of you on the two architectural approaches present in the two separate PRs for this.
If you have time, please look at the code and do some manual testing using the
getting_started.pyexample app or whatever you prefer.I appreciate any and all feedback. I'm also very open to feedback from anyone else.
tleonhardt commentedon Sep 6, 2026
There is now a 3rd branch which is continuing evolution of the overall architecture which is trying to arrive at a cleaner architecture from a
prompt-toolkitapplication perspective where we just have a single Prompt Toolkit application running continuously and we dynamically swap layouts at different stages. This is being done in attempt at eliminating an annoying flicker of the bottom toolbar.The WIP for this new
continuous-toolbar-appbranch is in PR #1748tleonhardt commentedon Sep 7, 2026
The first attempt at eliminating flicker didn't pan out.
PR #1745 does a good job of solving the persistence problem in most but not absolutely all situations.
I am currently exploring options for eliminating visual flicker as well as truly solving the persistence problem in all situations.