Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
- **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. 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
Expand Down Expand Up @@ -53,6 +56,11 @@
- A command can share an argument block with its subcommands via `cmd2_base_args` /
`cmd2_parent_args` parameters, passing parent-level options down without redeclaring them.

- 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)

### Summary
Expand Down
104 changes: 53 additions & 51 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import contextlib
import copy
import dataclasses
import datetime
import functools
import glob
import inspect
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -367,16 +368,17 @@ 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,
enable_bottom_toolbar: bool = False,
enable_rprompt: bool = False,
include_ipy: bool = False,
include_py: bool = False,
intro: RenderableType = "",
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 = "",
Expand Down Expand Up @@ -405,20 +407,23 @@ 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
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
:param multiline_commands: Iterable of commands allowed to accept multi-line input
: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
Expand Down Expand Up @@ -535,10 +540,14 @@ def __init__(
self._initialize_history(persistent_history_file)

# Create the main PromptSession
self.bottom_toolbar = bottom_toolbar
self.complete_in_thread = complete_in_thread

@tleonhardt tleonhardt Jun 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since complete_in_thread, refresh_interval, and bottom_toolbar were previously public instance attributes on Cmd (and documented as such), removing them from the object instance will break downstream code that attempts to access or modify them. This should be explicitly noted in the breaking changes section of CHANGELOG.md, e.g.:

- Breaking Changes
    - ...
    - Removed `bottom_toolbar`, `complete_in_thread`, and `refresh_interval` as instance attributes of `Cmd`. They are now strictly `__init__` parameters.

Alternatively, we could restore them to being instance attributes.

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
Expand Down Expand Up @@ -729,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.
Expand Down Expand Up @@ -759,19 +777,19 @@ 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 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),
"key_bindings": key_bindings,
"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),
}

Expand Down Expand Up @@ -1983,49 +2001,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).

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
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 at scheduled refresh intervals), keeping
this function highly optimized is critical to ensuring the CLI remains responsive.

# 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]
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 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.
"""
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 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 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.
"""
return None

Expand Down Expand Up @@ -2932,8 +2936,6 @@ def onecmd_plus_hooks(
command's stdout.
:return: True if running of commands should stop
"""
import datetime

stop = False
statement = None

Expand Down
4 changes: 3 additions & 1 deletion docs/features/initialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly added enable_bottom_toolbar and enable_rprompt (as well as refresh_interval below) are documented here as instance attributes, meaning they are instance members of the cmd2.Cmd instance that developers can override at runtime. However, in cmd2.py, they are strictly initializer arguments passed directly to PromptSession and are not stored on the Cmd instance. Attempting to override them at runtime will have no effect.

I think we should create a new section documenting __init__ parameters which are not also instance attributes and move them there.

This also applies to complete_in_thread.

Alternatively, we could restore them to instance attributes stored in Cmd.

- **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
Expand All @@ -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
Expand Down
20 changes: 14 additions & 6 deletions docs/features/prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,23 @@ 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, set `enable_bottom_toolbar=True` in the [cmd2.Cmd.__init__][] constructor:

```py
class App(cmd2.Cmd):
def __init__(self):
super().__init__(bottom_toolbar=True)
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. This method should return either a string or a list of `(style, text)` tuples for formatted
text.
method.

```py
def get_bottom_toolbar(self) -> list[str | tuple[str, str]] | None:
from prompt_toolkit.formatted_text import AnyFormattedText

def get_bottom_toolbar(self) -> AnyFormattedText:
Comment thread
kmvanbrunt marked this conversation as resolved.
return [
('ansigreen', 'My Application Name'),
('', ' - '),
Expand All @@ -92,7 +93,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)
Expand Down
5 changes: 2 additions & 3 deletions docs/upgrades.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +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**: Set `bottom_toolbar=True` in the [cmd2.Cmd.__init__][] constructor.
- **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. The content can be a simple string or a list of `(style, text)` tuples for
formatted text with colors.
wish to display.

See the
[getting_started.py](https://github.com/python-cmd2/cmd2/blob/main/examples/getting_started.py)
Expand Down
37 changes: 31 additions & 6 deletions examples/getting_started.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
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.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
Expand All @@ -44,7 +48,8 @@ def __init__(self) -> None:

super().__init__(
auto_suggest=True,
bottom_toolbar=True,
enable_bottom_toolbar=True,
enable_rprompt=True,
include_ipy=True,
multiline_commands=["echo"],
persistent_history_file="cmd2_history.dat",
Expand Down Expand Up @@ -87,11 +92,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]
Comment thread
kmvanbrunt marked this conversation as resolved.

# 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."""
Expand All @@ -108,7 +135,5 @@ def do_echo(self, arg: cmd2.Statement) -> None:


if __name__ == "__main__":
import sys

app = BasicApp()
sys.exit(app.cmdloop())
Loading
Loading