From fada72f46ee0334f3bf46ef4906f5f412fa156d7 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 27 Jun 2026 11:26:50 -0400 Subject: [PATCH 1/6] 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. --- cmd2/cmd2.py | 70 ++++++++++++++----------------------- docs/features/prompt.md | 26 ++++++-------- docs/upgrades.md | 6 ++-- examples/getting_started.py | 31 +++++++++++++--- tests/test_cmd2.py | 38 +++----------------- 5 files changed, 71 insertions(+), 100 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 66d01c1b8..fa91fc30e 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -32,6 +32,7 @@ import contextlib import copy import dataclasses +import datetime import functools import glob import inspect @@ -73,7 +74,7 @@ from prompt_toolkit.application import create_app_session, get_app from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.completion import Completer, DummyCompleter -from prompt_toolkit.formatted_text import ANSI, FormattedText +from prompt_toolkit.formatted_text import ANSI, AnyFormattedText from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.input import DummyInput, create_input from prompt_toolkit.key_binding import KeyBindings @@ -367,7 +368,6 @@ def __init__( allow_redirection: bool = True, auto_load_commands: bool = False, auto_suggest: bool = True, - bottom_toolbar: bool = False, complete_in_thread: bool = True, command_sets: Iterable[CommandSet[Any]] | None = None, include_ipy: bool = False, @@ -376,7 +376,7 @@ def __init__( multiline_commands: Iterable[str] | None = None, persistent_history_file: str = "", persistent_history_length: int = 1000, - refresh_interval: float = 0, + refresh_interval: float = 0.0, shortcuts: Mapping[str, str] | None = None, silence_startup_script: bool = False, startup_script: str = "", @@ -405,7 +405,6 @@ def __init__( :param auto_suggest: If True, cmd2 will provide fish shell style auto-suggestions based on history. User can press right-arrow key to accept the provided suggestion. - :param bottom_toolbar: if ``True``, then a bottom toolbar will be displayed. :param complete_in_thread: if ``True``, then completion will run in a separate thread. :param command_sets: Provide CommandSet instances to load during cmd2 initialization. This allows CommandSets with custom constructor parameters to be @@ -418,7 +417,7 @@ def __init__( :param persistent_history_file: file path to load a persistent cmd2 command history from :param persistent_history_length: max number of history items to write to the persistent history file - :param refresh_interval: How often, in seconds, to refresh the UI. Defaults to 0. + :param refresh_interval: How often, in seconds, to refresh the UI. Defaults to 0.0. prompt-toolkit already refreshes the UI every time a key is pressed. Set this value if you need the UI to update automatically without user input (e.g., for displaying a clock or background status @@ -535,7 +534,6 @@ def __init__( self._initialize_history(persistent_history_file) # Create the main PromptSession - self.bottom_toolbar = bottom_toolbar self.complete_in_thread = complete_in_thread self.refresh_interval = refresh_interval self.main_session = self._create_main_session(auto_suggest, completekey) @@ -759,7 +757,7 @@ def _(event: Any) -> None: # pragma: no cover # Base configuration kwargs: dict[str, Any] = { "auto_suggest": AutoSuggestFromHistory() if auto_suggest else None, - "bottom_toolbar": self.get_bottom_toolbar if self.bottom_toolbar else None, + "bottom_toolbar": self.get_bottom_toolbar, "color_depth": ColorDepth.TRUE_COLOR, "complete_style": CompleteStyle.MULTI_COLUMN, "complete_in_thread": self.complete_in_thread, @@ -1983,49 +1981,35 @@ def ppretty( end=end, ) - def get_bottom_toolbar(self) -> list[str | tuple[str, str]] | None: + def get_bottom_toolbar(self) -> AnyFormattedText: """Get the bottom toolbar content. - Returns None if `self.bottom_toolbar` is False. Otherwise, returns a - list of tokens to populate the toolbar (which can span multiple lines). + This method is intended to be called by prompt-toolkit as a UI lifecycle callback. + Because prompt-toolkit triggers this callback on every UI refresh (e.g., on every + keypress and at scheduled refresh intervals), keeping this function highly optimized + is critical to ensuring the CLI remains responsive. - NOTE: prompt-toolkit calls this method on every UI refresh (e.g., on every keypress - and at scheduled refresh intervals). To ensure the CLI remains responsive, keep - this function highly optimized. - """ - if not self.bottom_toolbar: - return None - - import datetime - import shutil + Override this if you want a bottom toolbar displaying contextual information useful for + your application. This could be information like the application name, current state, + or even a real-time clock. - # Get the current time in ISO format with 0.01s precision - dt = datetime.datetime.now(datetime.timezone.utc).astimezone() - now = dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-4] + dt.strftime("%z") - left_text = sys.argv[0] - - # Get terminal width to calculate padding for right-alignment - cols, _ = shutil.get_terminal_size() - padding_size = cols - len(left_text) - len(now) - 1 - if padding_size < 1: - padding_size = 1 - padding = " " * padding_size + :return: Content to populate the bottom toolbar, or None to hide it. + """ + return None - # Return formatted text for prompt-toolkit - return [ - ("ansigreen", left_text), - ("", padding), - ("ansicyan", now), - ] + def get_rprompt(self) -> AnyFormattedText: + """Provide text to populate the prompt-toolkit right prompt. - def get_rprompt(self) -> str | FormattedText | None: - """Provide text to populate prompt-toolkit right prompt with. + This method is intended to be called by prompt-toolkit as a UI lifecycle callback. + Because prompt-toolkit triggers this callback on every UI refresh (e.g., on every + keypress and at scheduled refresh intervals), keeping this function highly optimized + is critical to ensuring the CLI remains responsive. - Override this if you want a right-prompt displaying contetual information useful for your application. - This could be information like current Git branch, time, current working directory, etc that is displayed - without cluttering the main input area. + Override this if you want a right-prompt displaying contextual information useful for + your application. This could be information like the current Git branch, time, or current + working directory that is displayed without cluttering the main input area. - :return: any type of formatted text to display as the right prompt + :return: Content to populate the right prompt, or None to hide it. """ return None @@ -2932,8 +2916,6 @@ def onecmd_plus_hooks( command's stdout. :return: True if running of commands should stop """ - import datetime - stop = False statement = None diff --git a/docs/features/prompt.md b/docs/features/prompt.md index fdb4e2391..479f01cd7 100644 --- a/docs/features/prompt.md +++ b/docs/features/prompt.md @@ -65,22 +65,11 @@ terminal window while the application is idle and waiting for input. ### Enabling the Toolbar -To enable the toolbar, set `bottom_toolbar=True` in the [cmd2.Cmd.__init__][] constructor: +To enable the toolbar, override the [cmd2.Cmd.get_bottom_toolbar][] method to return the content you +wish to display. ```py -class App(cmd2.Cmd): - def __init__(self): - super().__init__(bottom_toolbar=True) -``` - -### Customizing Toolbar Content - -You can customize the content of the toolbar by overriding the [cmd2.Cmd.get_bottom_toolbar][] -method. This method should return either a string or a list of `(style, text)` tuples for formatted -text. - -```py - def get_bottom_toolbar(self) -> list[str | tuple[str, str]] | None: + def get_bottom_toolbar(self) -> AnyFormattedText: return [ ('ansigreen', 'My Application Name'), ('', ' - '), @@ -92,7 +81,14 @@ text. Since the toolbar is rendered by `prompt-toolkit` as part of the prompt, it is naturally redrawn whenever the prompt is refreshed. If you want the toolbar to update automatically (for example, to -display a clock), you can use a background thread to call `app.invalidate()` periodically. +display a clock), you can set `refresh_interval` in the [cmd2.Cmd.__init__][] constructor to a value +greater than 0.0. + +```py +class App(cmd2.Cmd): + def __init__(self): + super().__init__(refresh_interval=0.5) +``` See the [getting_started.py](https://github.com/python-cmd2/cmd2/blob/main/examples/getting_started.py) diff --git a/docs/upgrades.md b/docs/upgrades.md index f6a247760..bd154eca8 100644 --- a/docs/upgrades.md +++ b/docs/upgrades.md @@ -36,10 +36,8 @@ While we have strived to maintain compatibility, there are some differences: `cmd2` now supports an optional, persistent bottom toolbar. This can be used to display information such as the application name, current state, or even a real-time clock. -- **Enablement**: Set `bottom_toolbar=True` in the [cmd2.Cmd.__init__][] constructor. -- **Customization**: Override the [cmd2.Cmd.get_bottom_toolbar][] method to return the content you - wish to display. The content can be a simple string or a list of `(style, text)` tuples for - formatted text with colors. +- **Enablement**: Override the [cmd2.Cmd.get_bottom_toolbar][] method to return the content you wish + to display. See the [getting_started.py](https://github.com/python-cmd2/cmd2/blob/main/examples/getting_started.py) diff --git a/examples/getting_started.py b/examples/getting_started.py index 98713e173..f911d2b26 100755 --- a/examples/getting_started.py +++ b/examples/getting_started.py @@ -16,9 +16,11 @@ 12) Persistent bottom toolbar with realtime status updates """ +import datetime import pathlib -from prompt_toolkit.formatted_text import FormattedText +from prompt_toolkit.application import get_app +from prompt_toolkit.formatted_text import AnyFormattedText from rich.style import Style import cmd2 @@ -44,7 +46,6 @@ def __init__(self) -> None: super().__init__( auto_suggest=True, - bottom_toolbar=True, include_ipy=True, multiline_commands=["echo"], persistent_history_file="cmd2_history.dat", @@ -87,11 +88,33 @@ def __init__(self) -> None: ) ) - def get_rprompt(self) -> str | FormattedText | None: + def get_bottom_toolbar(self) -> AnyFormattedText: + # Get the current time in ISO format with 0.01s precision + dt = datetime.datetime.now(datetime.timezone.utc).astimezone() + now = dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-4] + dt.strftime("%z") + left_text = sys.argv[0] + + # Fetch the terminal width to calculate padding for right-alignment. + # If called outside a running app loop (e.g., in unit tests), get_app() + # safely returns a dummy app with an 80-column fallback. + cols = get_app().output.get_size().columns + padding_size = cols - len(left_text) - len(now) - 1 + if padding_size < 1: + padding_size = 1 + padding = " " * padding_size + + # Return formatted text for prompt-toolkit + return [ + ("ansigreen", left_text), + ("", padding), + ("ansicyan", now), + ] + + def get_rprompt(self) -> AnyFormattedText: current_working_directory = pathlib.Path.cwd() style = "bg:ansired fg:ansiwhite" text = f"cwd={current_working_directory}" - return FormattedText([(style, text)]) + return [(style, text)] def do_intro(self, _: cmd2.Statement) -> None: """Display the intro banner.""" diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index df6200898..78f367afa 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -4289,16 +4289,13 @@ def test_path_complete_users_windows(monkeypatch, base_app): def test_get_bottom_toolbar(base_app, monkeypatch): - # Test default (disabled) + # Test default assert base_app.get_bottom_toolbar() is None - # Test enabled - base_app.bottom_toolbar = True - monkeypatch.setattr(sys, "argv", ["myapp.py"]) - toolbar = base_app.get_bottom_toolbar() - assert isinstance(toolbar, list) - assert toolbar[0] == ("ansigreen", "myapp.py") - assert toolbar[2][0] == "ansicyan" + # Test overridden + expected_text = "bottom toolbar text" + base_app.get_bottom_toolbar = lambda: expected_text + assert base_app.get_bottom_toolbar() == expected_text def test_get_rprompt(base_app): @@ -4306,16 +4303,10 @@ def test_get_rprompt(base_app): assert base_app.get_rprompt() is None # Test overridden - from prompt_toolkit.formatted_text import FormattedText - expected_text = "rprompt text" base_app.get_rprompt = lambda: expected_text assert base_app.get_rprompt() == expected_text - expected_formatted = FormattedText([("class:status", "OK")]) - base_app.get_rprompt = lambda: expected_formatted - assert base_app.get_rprompt() == expected_formatted - def test_multiline_complete_statement_keyboard_interrupt(multiline_app, monkeypatch): # Mock _read_command_line to raise KeyboardInterrupt @@ -4499,25 +4490,6 @@ def my_pre_prompt(): assert loop_check["running"] -def test_get_bottom_toolbar_narrow_terminal(base_app, monkeypatch): - """Test get_bottom_toolbar when terminal is too narrow for calculated padding""" - import shutil - - base_app.bottom_toolbar = True - monkeypatch.setattr(sys, "argv", ["myapp.py"]) - - # Mock shutil.get_terminal_size to return a very small width (e.g. 5) - # Calculated padding_size = 5 - len('myapp.py') - len(now) - 1 - # Since len(now) is ~29, this will definitely be < 1 - monkeypatch.setattr(shutil, "get_terminal_size", lambda: os.terminal_size((5, 20))) - - toolbar = base_app.get_bottom_toolbar() - assert isinstance(toolbar, list) - - # The padding (index 1) should be exactly 1 space - assert toolbar[1] == ("", " ") - - def test_auto_suggest_true(): """Test that auto_suggest=True initializes AutoSuggestFromHistory.""" app = cmd2.Cmd(auto_suggest=True) From a9d52e19791f4d1a2bf863c9b4c82206bb7726b0 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 27 Jun 2026 11:33:20 -0400 Subject: [PATCH 2/6] Updated change log. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1331b3dcb..5a05c125b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - **complete_in_thread**: (boolean) if `True`, then completion will run in a separate thread. If `False` then completion runs in the main thread and causes it to block if slow. Defaults to `True`. + - **refresh_interval**: (float) How often, in seconds, to automatically refresh the UI. + Defaults to 0.0. - Bug Fixes - Fixed type hinting so that methods decorated with `with_annotated` no longer trigger spurious @@ -46,6 +48,10 @@ `preprocess=str.lower` on an `Enum`). The two are mutually exclusive on one parameter and neither may be combined with a value-less action. + - Breaking Changes + - Removed `Cmd.bottom_toolbar` boolean. Just return `None` from `Cmd.get_bottom_toolbar()` + when you don't want to display one. This is exactly how `Cmd.get_rprompt()` works. + ## 4.0.0 (June 5, 2026) ### Summary From 5dbf06845803c5feb7ad629ccb7ec2c353750744 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 27 Jun 2026 11:37:57 -0400 Subject: [PATCH 3/6] Added test for refresh_interval. --- tests/test_cmd2.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 78f367afa..c05450d13 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -4288,6 +4288,16 @@ def test_path_complete_users_windows(monkeypatch, base_app): assert expected in matches +def test_refresh_interval() -> None: + # Test default value + default_refresh_app = cmd2.Cmd() + assert default_refresh_app.main_session.refresh_interval == 0.0 + + # Test custom value + custom_refresh_app = cmd2.Cmd(refresh_interval=5.0) + assert custom_refresh_app.main_session.refresh_interval == 5.0 + + def test_get_bottom_toolbar(base_app, monkeypatch): # Test default assert base_app.get_bottom_toolbar() is None From 1486c0936a430096bfb75b0974a6ec9e42e34ffb Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 27 Jun 2026 16:43:11 -0400 Subject: [PATCH 4/6] Renamed bottom_toolbar flag to enable_bottom_toolbar. Added enable_rprompt flag. --- CHANGELOG.md | 10 +++--- cmd2/cmd2.py | 58 ++++++++++++++++++++++----------- docs/features/initialization.md | 4 ++- docs/features/prompt.md | 16 +++++++-- docs/upgrades.md | 5 +-- examples/getting_started.py | 2 ++ tests/test_cmd2.py | 57 +++++++++++++++++++++++++------- 7 files changed, 112 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a05c125b..c78dad22b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ thread. If `False` then completion runs in the main thread and causes it to block if slow. Defaults to `True`. - **refresh_interval**: (float) How often, in seconds, to automatically refresh the UI. - Defaults to 0.0. + Defaults to 0.0. This is used for bottom toolbars and right prompts which have dynamic + content needing to be refreshed at regular intervals and not just when a key is pressed. - Bug Fixes - Fixed type hinting so that methods decorated with `with_annotated` no longer trigger spurious @@ -48,9 +49,10 @@ `preprocess=str.lower` on an `Enum`). The two are mutually exclusive on one parameter and neither may be combined with a value-less action. - - Breaking Changes - - Removed `Cmd.bottom_toolbar` boolean. Just return `None` from `Cmd.get_bottom_toolbar()` - when you don't want to display one. This is exactly how `Cmd.get_rprompt()` works. +- Breaking Changes + - Renamed the `bottom_toolbar` argument in `Cmd.__init__()` to `enable_bottom_toolbar`. + - `get_rprompt()` is now only called if the `enable_rprompt` argument in `Cmd.__init__()` is set + to `True`. ## 4.0.0 (June 5, 2026) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index fa91fc30e..6d26d2f7a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -370,6 +370,8 @@ def __init__( auto_suggest: bool = True, complete_in_thread: bool = True, command_sets: Iterable[CommandSet[Any]] | None = None, + enable_bottom_toolbar: bool = False, + enable_rprompt: bool = False, include_ipy: bool = False, include_py: bool = False, intro: RenderableType = "", @@ -410,6 +412,10 @@ def __init__( This allows CommandSets with custom constructor parameters to be loaded. This also allows the a set of CommandSets to be provided when `auto_load_commands` is set to False + :param enable_bottom_toolbar: if ``True``, enables a bottom toolbar while at the main prompt. + Override ``get_bottom_toolbar()`` to define its content. + :param enable_rprompt: if ``True``, enables a right prompt while at the main prompt. + Override ``get_rprompt()`` to define its content. :param include_ipy: should the "ipy" command be included for an embedded IPython shell :param include_py: should the "py" command be included for an embedded Python shell :param intro: introduction to display at startup @@ -534,9 +540,14 @@ def __init__( self._initialize_history(persistent_history_file) # Create the main PromptSession - self.complete_in_thread = complete_in_thread - self.refresh_interval = refresh_interval - self.main_session = self._create_main_session(auto_suggest, completekey) + self.main_session = self._create_main_session( + auto_suggest=auto_suggest, + complete_in_thread=complete_in_thread, + completekey=completekey, + enable_bottom_toolbar=enable_bottom_toolbar, + enable_rprompt=enable_rprompt, + refresh_interval=refresh_interval, + ) # The session currently holding focus (either the main REPL or a command's # custom prompt). Completion and UI logic should reference this variable @@ -727,7 +738,16 @@ def _should_continue_multiline(self) -> bool: # No macro found or already processed. The statement is complete. return False - def _create_main_session(self, auto_suggest: bool, completekey: str) -> PromptSession[str]: + def _create_main_session( + self, + *, + auto_suggest: bool, + complete_in_thread: bool, + completekey: str, + enable_bottom_toolbar: bool, + enable_rprompt: bool, + refresh_interval: float, + ) -> PromptSession[str]: """Create and return the main PromptSession for the application. Builds an interactive session if self.stdin and self.stdout are TTYs. @@ -757,10 +777,10 @@ def _(event: Any) -> None: # pragma: no cover # Base configuration kwargs: dict[str, Any] = { "auto_suggest": AutoSuggestFromHistory() if auto_suggest else None, - "bottom_toolbar": self.get_bottom_toolbar, + "bottom_toolbar": self.get_bottom_toolbar if enable_bottom_toolbar else None, "color_depth": ColorDepth.TRUE_COLOR, "complete_style": CompleteStyle.MULTI_COLUMN, - "complete_in_thread": self.complete_in_thread, + "complete_in_thread": complete_in_thread, "complete_while_typing": False, "completer": Cmd2Completer(self), "history": Cmd2History(item.raw for item in self.history), @@ -768,8 +788,8 @@ def _(event: Any) -> None: # pragma: no cover "lexer": Cmd2Lexer(self), "multiline": filters.Condition(self._should_continue_multiline), "prompt_continuation": self.continuation_prompt, - "refresh_interval": self.refresh_interval, - "rprompt": self.get_rprompt, + "refresh_interval": refresh_interval, + "rprompt": self.get_rprompt if enable_rprompt else None, "style": DynamicStyle(get_pt_theme), } @@ -1984,32 +2004,32 @@ def ppretty( def get_bottom_toolbar(self) -> AnyFormattedText: """Get the bottom toolbar content. - This method is intended to be called by prompt-toolkit as a UI lifecycle callback. - Because prompt-toolkit triggers this callback on every UI refresh (e.g., on every - keypress and at scheduled refresh intervals), keeping this function highly optimized - is critical to ensuring the CLI remains responsive. + This method is called by prompt-toolkit while at the main prompt if ``enable_bottom_toolbar`` + was set to ``True`` during initialization. Because prompt-toolkit executes this callback + on every UI refresh (such as on every keypress or timed interval), keeping this function + highly optimized is critical to ensuring the CLI remains responsive. Override this if you want a bottom toolbar displaying contextual information useful for your application. This could be information like the application name, current state, or even a real-time clock. - :return: Content to populate the bottom toolbar, or None to hide it. + :return: Content to populate the bottom toolbar. """ return None def get_rprompt(self) -> AnyFormattedText: """Provide text to populate the prompt-toolkit right prompt. - This method is intended to be called by prompt-toolkit as a UI lifecycle callback. - Because prompt-toolkit triggers this callback on every UI refresh (e.g., on every - keypress and at scheduled refresh intervals), keeping this function highly optimized - is critical to ensuring the CLI remains responsive. + This method is called by prompt-toolkit while at the main prompt if ``enable_rprompt`` + was set to ``True`` during initialization. Because prompt-toolkit executes this callback + on every UI refresh (such as on every keypress or timed interval), keeping this function + highly optimized is critical to ensuring the CLI remains responsive. - Override this if you want a right-prompt displaying contextual information useful for + Override this if you want a right prompt displaying contextual information useful for your application. This could be information like the current Git branch, time, or current working directory that is displayed without cluttering the main input area. - :return: Content to populate the right prompt, or None to hide it. + :return: Content to populate the right prompt. """ return None diff --git a/docs/features/initialization.md b/docs/features/initialization.md index 491e71025..e354d5271 100644 --- a/docs/features/initialization.md +++ b/docs/features/initialization.md @@ -33,7 +33,6 @@ The `cmd2.Cmd` class provides a large number of public instance attributes which Here are instance attributes of `cmd2.Cmd` which developers might wish to override: -- **bottom_toolbar**: if `True`, then a bottom toolbar will be displayed (Default: `False`) - **broken_pipe_warning**: if non-empty, this string will be displayed if a broken pipe error occurs - **complete_in_thread**: if `True`, then completion will run in a separate thread (Default: `True`) - **continuation_prompt**: used for multiline commands on 2nd+ line of input @@ -42,6 +41,8 @@ Here are instance attributes of `cmd2.Cmd` which developers might wish to overri - **disabled_commands**: commands that have been disabled from use. This is to support commands that are only available during specific states of the application. This dictionary's keys are the command names and its values are DisabledCommand objects. - **echo**: if `True`, each command the user issues will be repeated to the screen before it is executed. This is particularly useful when running scripts. This behavior does not occur when running a command at the prompt. (Default: `False`) - **editor**: text editor program to use with _edit_ command (e.g. `vim`) +- **enable_bottom_toolbar**: if `True`, enables a bottom toolbar while at the main prompt. (Default: `False`) +- **enable_rprompt**: if `True`, enables a right prompt while at the main prompt. (Default: `False`) - **exclude_from_history**: commands to exclude from the _history_ command - **exit_code**: this determines the value returned by `cmdloop()` when exiting the application - **help_error**: the error that prints when no help information can be found @@ -55,6 +56,7 @@ Here are instance attributes of `cmd2.Cmd` which developers might wish to overri - **py_bridge_name**: name by which embedded Python environments and scripts refer to the `cmd2` application by in order to call commands (Default: `app`) - **py_locals**: dictionary that defines specific variables/functions available in Python shells and scripts (provides more fine-grained control than making everything available with **self_in_py**) - **quiet**: if `True`, then completely suppress nonessential output (Default: `False`) +- **refresh_interval**: how often, in seconds, to automatically refresh the UI. (Default: 0.0) - **scripts_add_to_history**: if `True`, scripts and pyscripts add commands to history (Default: `True`) - **self_in_py**: if `True`, allow access to your application in _py_ command via `self` (Default: `False`) - **settable**: dictionary that controls which of these instance attributes are settable at runtime using the _set_ command diff --git a/docs/features/prompt.md b/docs/features/prompt.md index 479f01cd7..93fb6b495 100644 --- a/docs/features/prompt.md +++ b/docs/features/prompt.md @@ -65,10 +65,22 @@ terminal window while the application is idle and waiting for input. ### Enabling the Toolbar -To enable the toolbar, override the [cmd2.Cmd.get_bottom_toolbar][] method to return the content you -wish to display. +To enable the toolbar, set `enable_bottom_toolbar=True` in the [cmd2.Cmd.__init__][] constructor: ```py +class App(cmd2.Cmd): + def __init__(self): + super().__init__(enable_bottom_toolbar=True) +``` + +### Customizing Toolbar Content + +You can customize the content of the toolbar by overriding the [cmd2.Cmd.get_bottom_toolbar][] +method. + +```py + from prompt_toolkit.formatted_text import AnyFormattedText + def get_bottom_toolbar(self) -> AnyFormattedText: return [ ('ansigreen', 'My Application Name'), diff --git a/docs/upgrades.md b/docs/upgrades.md index bd154eca8..a316819e9 100644 --- a/docs/upgrades.md +++ b/docs/upgrades.md @@ -36,8 +36,9 @@ While we have strived to maintain compatibility, there are some differences: `cmd2` now supports an optional, persistent bottom toolbar. This can be used to display information such as the application name, current state, or even a real-time clock. -- **Enablement**: Override the [cmd2.Cmd.get_bottom_toolbar][] method to return the content you wish - to display. +- **Enablement**: Set `enable_bottom_toolbar=True` in the [cmd2.Cmd.__init__][] constructor. +- **Customization**: Override the [cmd2.Cmd.get_bottom_toolbar][] method to return the content you + wish to display. See the [getting_started.py](https://github.com/python-cmd2/cmd2/blob/main/examples/getting_started.py) diff --git a/examples/getting_started.py b/examples/getting_started.py index f911d2b26..3d6a7b061 100755 --- a/examples/getting_started.py +++ b/examples/getting_started.py @@ -46,6 +46,8 @@ def __init__(self) -> None: super().__init__( auto_suggest=True, + enable_bottom_toolbar=True, + enable_rprompt=True, include_ipy=True, multiline_commands=["echo"], persistent_history_file="cmd2_history.dat", diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index c05450d13..bec336830 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -67,17 +67,14 @@ def test_version(base_app) -> None: def test_complete_in_thread() -> None: # Test default app_default = cmd2.Cmd() - assert app_default.complete_in_thread is True assert app_default.main_session.complete_in_thread is True # Test True app_true = cmd2.Cmd(complete_in_thread=True) - assert app_true.complete_in_thread is True assert app_true.main_session.complete_in_thread is True # Test False app_false = cmd2.Cmd(complete_in_thread=False) - assert app_false.complete_in_thread is False assert app_false.main_session.complete_in_thread is False @@ -4229,13 +4226,14 @@ def test_custom_completekey_ctrl_k(): def test_completekey_empty_string() -> None: # Test that an empty string for completekey defaults to DEFAULT_COMPLETEKEY + with mock.patch("cmd2.Cmd._create_main_session", autospec=True) as create_session_mock: create_session_mock.return_value = mock.MagicMock(spec=PromptSession) - app = cmd2.Cmd(completekey="") - # Verify it was called with DEFAULT_COMPLETEKEY - # auto_suggest is the second arg and it defaults to True - create_session_mock.assert_called_once_with(app, True, app.DEFAULT_COMPLETEKEY) + app = cmd2.Cmd(completekey="") + create_session_mock.assert_called_once() + _, kwargs = create_session_mock.call_args + assert kwargs["completekey"] == app.DEFAULT_COMPLETEKEY def test_create_main_session_exception(monkeypatch): @@ -4290,12 +4288,40 @@ def test_path_complete_users_windows(monkeypatch, base_app): def test_refresh_interval() -> None: # Test default value - default_refresh_app = cmd2.Cmd() - assert default_refresh_app.main_session.refresh_interval == 0.0 + default_app = cmd2.Cmd() + assert default_app.main_session.refresh_interval == 0.0 # Test custom value - custom_refresh_app = cmd2.Cmd(refresh_interval=5.0) - assert custom_refresh_app.main_session.refresh_interval == 5.0 + custom_app = cmd2.Cmd(refresh_interval=5.0) + assert custom_app.main_session.refresh_interval == 5.0 + + +def test_enable_bottom_toolbar() -> None: + # Test default + default_app = cmd2.Cmd() + assert default_app.main_session.bottom_toolbar is None + + # Test True + custom_app = cmd2.Cmd(enable_bottom_toolbar=True) + assert custom_app.main_session.bottom_toolbar == custom_app.get_bottom_toolbar + + # Test False + custom_app = cmd2.Cmd(enable_bottom_toolbar=False) + assert custom_app.main_session.bottom_toolbar is None + + +def test_enable_rprompt() -> None: + # Test default + default_app = cmd2.Cmd() + assert default_app.main_session.rprompt is None + + # Test True + custom_app = cmd2.Cmd(enable_rprompt=True) + assert custom_app.main_session.rprompt == custom_app.get_rprompt + + # Test False + custom_app = cmd2.Cmd(enable_rprompt=False) + assert custom_app.main_session.rprompt is None def test_get_bottom_toolbar(base_app, monkeypatch): @@ -4377,7 +4403,14 @@ def test_create_main_session_with_custom_tty() -> None: app = cmd2.Cmd() app.stdin = custom_stdin app.stdout = custom_stdout - app._create_main_session(auto_suggest=True, completekey=app.DEFAULT_COMPLETEKEY) + app._create_main_session( + auto_suggest=True, + completekey=app.DEFAULT_COMPLETEKEY, + enable_bottom_toolbar=False, + enable_rprompt=False, + complete_in_thread=False, + refresh_interval=0.0, + ) mock_create_input.assert_called_once_with(stdin=custom_stdin) mock_create_output.assert_called_once_with(stdout=custom_stdout) From b7172ca245e014defeba673665f807c2440f055a Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 27 Jun 2026 16:47:30 -0400 Subject: [PATCH 5/6] Moved import. --- examples/getting_started.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/getting_started.py b/examples/getting_started.py index 3d6a7b061..ad1a08d9f 100755 --- a/examples/getting_started.py +++ b/examples/getting_started.py @@ -14,10 +14,12 @@ 10) How to make custom attributes settable at runtime. 11) Shortcuts for commands 12) Persistent bottom toolbar with realtime status updates +13) Right prompt which displays contextual information """ import datetime import pathlib +import sys from prompt_toolkit.application import get_app from prompt_toolkit.formatted_text import AnyFormattedText @@ -133,7 +135,5 @@ def do_echo(self, arg: cmd2.Statement) -> None: if __name__ == "__main__": - import sys - app = BasicApp() sys.exit(app.cmdloop()) From 4706ed00ebdcc06c011a895641688a4cf04ce68c Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 27 Jun 2026 17:04:17 -0400 Subject: [PATCH 6/6] Updated docstrings. --- cmd2/cmd2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 6d26d2f7a..8ac7dcfea 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2006,8 +2006,8 @@ def get_bottom_toolbar(self) -> AnyFormattedText: This method is called by prompt-toolkit while at the main prompt if ``enable_bottom_toolbar`` was set to ``True`` during initialization. Because prompt-toolkit executes this callback - on every UI refresh (such as on every keypress or timed interval), keeping this function - highly optimized is critical to ensuring the CLI remains responsive. + on every UI refresh (such as on every keypress or at scheduled refresh intervals), keeping + this function highly optimized is critical to ensuring the CLI remains responsive. Override this if you want a bottom toolbar displaying contextual information useful for your application. This could be information like the application name, current state, @@ -2022,8 +2022,8 @@ def get_rprompt(self) -> AnyFormattedText: This method is called by prompt-toolkit while at the main prompt if ``enable_rprompt`` was set to ``True`` during initialization. Because prompt-toolkit executes this callback - on every UI refresh (such as on every keypress or timed interval), keeping this function - highly optimized is critical to ensuring the CLI remains responsive. + on every UI refresh (such as on every keypress or at scheduled refresh intervals), keeping + this function highly optimized is critical to ensuring the CLI remains responsive. Override this if you want a right prompt displaying contextual information useful for your application. This could be information like the current Git branch, time, or current