Skip to content

Commit d8a46dd

Browse files
Refactored bottom toolbar and rprompt functionality. (#1700)
* Removed bottom_toolbar boolean from Cmd to make get_bottom_toolbar() work the same way as get_rprompt(). Removed default implementation of get_bottom_toolbar() from Cmd class and moved it to the getting_started example. * Updated change log. * Added test for refresh_interval. * Renamed bottom_toolbar flag to enable_bottom_toolbar. Added enable_rprompt flag. * Moved import. * Updated docstrings. * Simplified tests. * Updated documentation. --------- Co-authored-by: Todd Leonhardt <todd.leonhardt@gmail.com>
1 parent df08bb7 commit d8a46dd

7 files changed

Lines changed: 161 additions & 115 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
- **complete_in_thread**: (boolean) if `True`, then completion will run in a separate
66
thread. If `False` then completion runs in the main thread and causes it to block if slow.
77
Defaults to `True`.
8+
- **refresh_interval**: (float) How often, in seconds, to automatically refresh the UI.
9+
Defaults to 0.0. This is used for bottom toolbars and right prompts which have dynamic
10+
content needing to be refreshed at regular intervals and not just when a key is pressed.
811

912
- Bug Fixes
1013
- Fixed type hinting so that methods decorated with `with_annotated` no longer trigger spurious
@@ -53,6 +56,14 @@
5356
- A command can share an argument block with its subcommands via `cmd2_base_args` /
5457
`cmd2_parent_args` parameters, passing parent-level options down without redeclaring them.
5558

59+
- Breaking Changes
60+
- Renamed the `bottom_toolbar` argument in `Cmd.__init__()` to `enable_bottom_toolbar`. It is
61+
also now strictly an `__init__` parameter and not an instance attribute.
62+
- `complete_in_thread` is now strictly an `__init__` parameter and not an instance attribute of
63+
`Cmd`.
64+
- `get_rprompt()` is now only called if the `enable_rprompt` argument in `Cmd.__init__()` is set
65+
to `True`.
66+
5667
## 4.0.0 (June 5, 2026)
5768

5869
### Summary

cmd2/cmd2.py

Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import contextlib
3333
import copy
3434
import dataclasses
35+
import datetime
3536
import functools
3637
import glob
3738
import inspect
@@ -73,7 +74,7 @@
7374
from prompt_toolkit.application import create_app_session, get_app
7475
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
7576
from prompt_toolkit.completion import Completer, DummyCompleter
76-
from prompt_toolkit.formatted_text import ANSI, FormattedText
77+
from prompt_toolkit.formatted_text import ANSI, AnyFormattedText
7778
from prompt_toolkit.history import InMemoryHistory
7879
from prompt_toolkit.input import DummyInput, create_input
7980
from prompt_toolkit.key_binding import KeyBindings
@@ -367,16 +368,17 @@ def __init__(
367368
allow_redirection: bool = True,
368369
auto_load_commands: bool = False,
369370
auto_suggest: bool = True,
370-
bottom_toolbar: bool = False,
371371
complete_in_thread: bool = True,
372372
command_sets: Iterable[CommandSet[Any]] | None = None,
373+
enable_bottom_toolbar: bool = False,
374+
enable_rprompt: bool = False,
373375
include_ipy: bool = False,
374376
include_py: bool = False,
375377
intro: RenderableType = "",
376378
multiline_commands: Iterable[str] | None = None,
377379
persistent_history_file: str = "",
378380
persistent_history_length: int = 1000,
379-
refresh_interval: float = 0,
381+
refresh_interval: float = 0.0,
380382
shortcuts: Mapping[str, str] | None = None,
381383
silence_startup_script: bool = False,
382384
startup_script: str = "",
@@ -405,20 +407,23 @@ def __init__(
405407
:param auto_suggest: If True, cmd2 will provide fish shell style auto-suggestions
406408
based on history. User can press right-arrow key to accept the
407409
provided suggestion.
408-
:param bottom_toolbar: if ``True``, then a bottom toolbar will be displayed.
409410
:param complete_in_thread: if ``True``, then completion will run in a separate thread.
410411
:param command_sets: Provide CommandSet instances to load during cmd2 initialization.
411412
This allows CommandSets with custom constructor parameters to be
412413
loaded. This also allows the a set of CommandSets to be provided
413414
when `auto_load_commands` is set to False
415+
:param enable_bottom_toolbar: if ``True``, enables a bottom toolbar while at the main prompt.
416+
Override ``get_bottom_toolbar()`` to define its content.
417+
:param enable_rprompt: if ``True``, enables a right prompt while at the main prompt.
418+
Override ``get_rprompt()`` to define its content.
414419
:param include_ipy: should the "ipy" command be included for an embedded IPython shell
415420
:param include_py: should the "py" command be included for an embedded Python shell
416421
:param intro: introduction to display at startup
417422
:param multiline_commands: Iterable of commands allowed to accept multi-line input
418423
:param persistent_history_file: file path to load a persistent cmd2 command history from
419424
:param persistent_history_length: max number of history items to write
420425
to the persistent history file
421-
:param refresh_interval: How often, in seconds, to refresh the UI. Defaults to 0.
426+
:param refresh_interval: How often, in seconds, to refresh the UI. Defaults to 0.0.
422427
prompt-toolkit already refreshes the UI every time a key is pressed.
423428
Set this value if you need the UI to update automatically without
424429
user input (e.g., for displaying a clock or background status
@@ -535,10 +540,14 @@ def __init__(
535540
self._initialize_history(persistent_history_file)
536541

537542
# Create the main PromptSession
538-
self.bottom_toolbar = bottom_toolbar
539-
self.complete_in_thread = complete_in_thread
540-
self.refresh_interval = refresh_interval
541-
self.main_session = self._create_main_session(auto_suggest, completekey)
543+
self.main_session = self._create_main_session(
544+
auto_suggest=auto_suggest,
545+
complete_in_thread=complete_in_thread,
546+
completekey=completekey,
547+
enable_bottom_toolbar=enable_bottom_toolbar,
548+
enable_rprompt=enable_rprompt,
549+
refresh_interval=refresh_interval,
550+
)
542551

543552
# The session currently holding focus (either the main REPL or a command's
544553
# custom prompt). Completion and UI logic should reference this variable
@@ -729,7 +738,16 @@ def _should_continue_multiline(self) -> bool:
729738
# No macro found or already processed. The statement is complete.
730739
return False
731740

732-
def _create_main_session(self, auto_suggest: bool, completekey: str) -> PromptSession[str]:
741+
def _create_main_session(
742+
self,
743+
*,
744+
auto_suggest: bool,
745+
complete_in_thread: bool,
746+
completekey: str,
747+
enable_bottom_toolbar: bool,
748+
enable_rprompt: bool,
749+
refresh_interval: float,
750+
) -> PromptSession[str]:
733751
"""Create and return the main PromptSession for the application.
734752
735753
Builds an interactive session if self.stdin and self.stdout are TTYs.
@@ -759,19 +777,19 @@ def _(event: Any) -> None: # pragma: no cover
759777
# Base configuration
760778
kwargs: dict[str, Any] = {
761779
"auto_suggest": AutoSuggestFromHistory() if auto_suggest else None,
762-
"bottom_toolbar": self.get_bottom_toolbar if self.bottom_toolbar else None,
780+
"bottom_toolbar": self.get_bottom_toolbar if enable_bottom_toolbar else None,
763781
"color_depth": ColorDepth.TRUE_COLOR,
764782
"complete_style": CompleteStyle.MULTI_COLUMN,
765-
"complete_in_thread": self.complete_in_thread,
783+
"complete_in_thread": complete_in_thread,
766784
"complete_while_typing": False,
767785
"completer": Cmd2Completer(self),
768786
"history": Cmd2History(item.raw for item in self.history),
769787
"key_bindings": key_bindings,
770788
"lexer": Cmd2Lexer(self),
771789
"multiline": filters.Condition(self._should_continue_multiline),
772790
"prompt_continuation": self.continuation_prompt,
773-
"refresh_interval": self.refresh_interval,
774-
"rprompt": self.get_rprompt,
791+
"refresh_interval": refresh_interval,
792+
"rprompt": self.get_rprompt if enable_rprompt else None,
775793
"style": DynamicStyle(get_pt_theme),
776794
}
777795

@@ -1983,49 +2001,35 @@ def ppretty(
19832001
end=end,
19842002
)
19852003

1986-
def get_bottom_toolbar(self) -> list[str | tuple[str, str]] | None:
2004+
def get_bottom_toolbar(self) -> AnyFormattedText:
19872005
"""Get the bottom toolbar content.
19882006
1989-
Returns None if `self.bottom_toolbar` is False. Otherwise, returns a
1990-
list of tokens to populate the toolbar (which can span multiple lines).
1991-
1992-
NOTE: prompt-toolkit calls this method on every UI refresh (e.g., on every keypress
1993-
and at scheduled refresh intervals). To ensure the CLI remains responsive, keep
1994-
this function highly optimized.
1995-
"""
1996-
if not self.bottom_toolbar:
1997-
return None
1998-
1999-
import datetime
2000-
import shutil
2007+
This method is called by prompt-toolkit while at the main prompt if ``enable_bottom_toolbar``
2008+
was set to ``True`` during initialization. Because prompt-toolkit executes this callback
2009+
on every UI refresh (such as on every keypress or at scheduled refresh intervals), keeping
2010+
this function highly optimized is critical to ensuring the CLI remains responsive.
20012011
2002-
# Get the current time in ISO format with 0.01s precision
2003-
dt = datetime.datetime.now(datetime.timezone.utc).astimezone()
2004-
now = dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-4] + dt.strftime("%z")
2005-
left_text = sys.argv[0]
2012+
Override this if you want a bottom toolbar displaying contextual information useful for
2013+
your application. This could be information like the application name, current state,
2014+
or even a real-time clock.
20062015
2007-
# Get terminal width to calculate padding for right-alignment
2008-
cols, _ = shutil.get_terminal_size()
2009-
padding_size = cols - len(left_text) - len(now) - 1
2010-
if padding_size < 1:
2011-
padding_size = 1
2012-
padding = " " * padding_size
2016+
:return: Content to populate the bottom toolbar.
2017+
"""
2018+
return None
20132019

2014-
# Return formatted text for prompt-toolkit
2015-
return [
2016-
("ansigreen", left_text),
2017-
("", padding),
2018-
("ansicyan", now),
2019-
]
2020+
def get_rprompt(self) -> AnyFormattedText:
2021+
"""Provide text to populate the prompt-toolkit right prompt.
20202022
2021-
def get_rprompt(self) -> str | FormattedText | None:
2022-
"""Provide text to populate prompt-toolkit right prompt with.
2023+
This method is called by prompt-toolkit while at the main prompt if ``enable_rprompt``
2024+
was set to ``True`` during initialization. Because prompt-toolkit executes this callback
2025+
on every UI refresh (such as on every keypress or at scheduled refresh intervals), keeping
2026+
this function highly optimized is critical to ensuring the CLI remains responsive.
20232027
2024-
Override this if you want a right-prompt displaying contetual information useful for your application.
2025-
This could be information like current Git branch, time, current working directory, etc that is displayed
2026-
without cluttering the main input area.
2028+
Override this if you want a right prompt displaying contextual information useful for
2029+
your application. This could be information like the current Git branch, time, or current
2030+
working directory that is displayed without cluttering the main input area.
20272031
2028-
:return: any type of formatted text to display as the right prompt
2032+
:return: Content to populate the right prompt.
20292033
"""
20302034
return None
20312035

@@ -2932,8 +2936,6 @@ def onecmd_plus_hooks(
29322936
command's stdout.
29332937
:return: True if running of commands should stop
29342938
"""
2935-
import datetime
2936-
29372939
stop = False
29382940
statement = None
29392941

docs/features/initialization.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ The `cmd2.Cmd` class provides a large number of public instance attributes which
3333

3434
Here are instance attributes of `cmd2.Cmd` which developers might wish to override:
3535

36-
- **bottom_toolbar**: if `True`, then a bottom toolbar will be displayed (Default: `False`)
3736
- **broken_pipe_warning**: if non-empty, this string will be displayed if a broken pipe error occurs
38-
- **complete_in_thread**: if `True`, then completion will run in a separate thread (Default: `True`)
3937
- **continuation_prompt**: used for multiline commands on 2nd+ line of input
4038
- **debug**: if `True`, show full stack trace on error (Default: `False`)
4139
- **default_error**: the error that prints when a non-existent command is run

docs/features/prompt.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,23 @@ terminal window while the application is idle and waiting for input.
6565

6666
### Enabling the Toolbar
6767

68-
To enable the toolbar, set `bottom_toolbar=True` in the [cmd2.Cmd.__init__][] constructor:
68+
To enable the toolbar, set `enable_bottom_toolbar=True` in the [cmd2.Cmd.__init__][] constructor:
6969

7070
```py
7171
class App(cmd2.Cmd):
7272
def __init__(self):
73-
super().__init__(bottom_toolbar=True)
73+
super().__init__(enable_bottom_toolbar=True)
7474
```
7575

7676
### Customizing Toolbar Content
7777

7878
You can customize the content of the toolbar by overriding the [cmd2.Cmd.get_bottom_toolbar][]
79-
method. This method should return either a string or a list of `(style, text)` tuples for formatted
80-
text.
79+
method.
8180

8281
```py
83-
def get_bottom_toolbar(self) -> list[str | tuple[str, str]] | None:
82+
from prompt_toolkit.formatted_text import AnyFormattedText
83+
84+
def get_bottom_toolbar(self) -> AnyFormattedText:
8485
return [
8586
('ansigreen', 'My Application Name'),
8687
('', ' - '),
@@ -92,7 +93,14 @@ text.
9293

9394
Since the toolbar is rendered by `prompt-toolkit` as part of the prompt, it is naturally redrawn
9495
whenever the prompt is refreshed. If you want the toolbar to update automatically (for example, to
95-
display a clock), you can use a background thread to call `app.invalidate()` periodically.
96+
display a clock), you can set `refresh_interval` in the [cmd2.Cmd.__init__][] constructor to a value
97+
greater than 0.0.
98+
99+
```py
100+
class App(cmd2.Cmd):
101+
def __init__(self):
102+
super().__init__(refresh_interval=0.5)
103+
```
96104

97105
See the
98106
[getting_started.py](https://github.com/python-cmd2/cmd2/blob/main/examples/getting_started.py)

docs/upgrades.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,9 @@ While we have strived to maintain compatibility, there are some differences:
3636
`cmd2` now supports an optional, persistent bottom toolbar. This can be used to display information
3737
such as the application name, current state, or even a real-time clock.
3838

39-
- **Enablement**: Set `bottom_toolbar=True` in the [cmd2.Cmd.__init__][] constructor.
39+
- **Enablement**: Set `enable_bottom_toolbar=True` in the [cmd2.Cmd.__init__][] constructor.
4040
- **Customization**: Override the [cmd2.Cmd.get_bottom_toolbar][] method to return the content you
41-
wish to display. The content can be a simple string or a list of `(style, text)` tuples for
42-
formatted text with colors.
41+
wish to display.
4342

4443
See the
4544
[getting_started.py](https://github.com/python-cmd2/cmd2/blob/main/examples/getting_started.py)

examples/getting_started.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414
10) How to make custom attributes settable at runtime.
1515
11) Shortcuts for commands
1616
12) Persistent bottom toolbar with realtime status updates
17+
13) Right prompt which displays contextual information
1718
"""
1819

20+
import datetime
1921
import pathlib
22+
import sys
2023

21-
from prompt_toolkit.formatted_text import FormattedText
24+
from prompt_toolkit.application import get_app
25+
from prompt_toolkit.formatted_text import AnyFormattedText
2226
from rich.style import Style
2327

2428
import cmd2
@@ -44,7 +48,8 @@ def __init__(self) -> None:
4448

4549
super().__init__(
4650
auto_suggest=True,
47-
bottom_toolbar=True,
51+
enable_bottom_toolbar=True,
52+
enable_rprompt=True,
4853
include_ipy=True,
4954
multiline_commands=["echo"],
5055
persistent_history_file="cmd2_history.dat",
@@ -87,11 +92,33 @@ def __init__(self) -> None:
8792
)
8893
)
8994

90-
def get_rprompt(self) -> str | FormattedText | None:
95+
def get_bottom_toolbar(self) -> AnyFormattedText:
96+
# Get the current time in ISO format with 0.01s precision
97+
dt = datetime.datetime.now(datetime.timezone.utc).astimezone()
98+
now = dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-4] + dt.strftime("%z")
99+
left_text = sys.argv[0]
100+
101+
# Fetch the terminal width to calculate padding for right-alignment.
102+
# If called outside a running app loop (e.g., in unit tests), get_app()
103+
# safely returns a dummy app with an 80-column fallback.
104+
cols = get_app().output.get_size().columns
105+
padding_size = cols - len(left_text) - len(now) - 1
106+
if padding_size < 1:
107+
padding_size = 1
108+
padding = " " * padding_size
109+
110+
# Return formatted text for prompt-toolkit
111+
return [
112+
("ansigreen", left_text),
113+
("", padding),
114+
("ansicyan", now),
115+
]
116+
117+
def get_rprompt(self) -> AnyFormattedText:
91118
current_working_directory = pathlib.Path.cwd()
92119
style = "bg:ansired fg:ansiwhite"
93120
text = f"cwd={current_working_directory}"
94-
return FormattedText([(style, text)])
121+
return [(style, text)]
95122

96123
def do_intro(self, _: cmd2.Statement) -> None:
97124
"""Display the intro banner."""
@@ -108,7 +135,5 @@ def do_echo(self, arg: cmd2.Statement) -> None:
108135

109136

110137
if __name__ == "__main__":
111-
import sys
112-
113138
app = BasicApp()
114139
sys.exit(app.cmdloop())

0 commit comments

Comments
 (0)