Skip to content

Commit fada72f

Browse files
committed
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.
1 parent b17b1bd commit fada72f

5 files changed

Lines changed: 71 additions & 100 deletions

File tree

cmd2/cmd2.py

Lines changed: 26 additions & 44 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,7 +368,6 @@ 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,
373373
include_ipy: bool = False,
@@ -376,7 +376,7 @@ def __init__(
376376
multiline_commands: Iterable[str] | None = None,
377377
persistent_history_file: str = "",
378378
persistent_history_length: int = 1000,
379-
refresh_interval: float = 0,
379+
refresh_interval: float = 0.0,
380380
shortcuts: Mapping[str, str] | None = None,
381381
silence_startup_script: bool = False,
382382
startup_script: str = "",
@@ -405,7 +405,6 @@ def __init__(
405405
:param auto_suggest: If True, cmd2 will provide fish shell style auto-suggestions
406406
based on history. User can press right-arrow key to accept the
407407
provided suggestion.
408-
:param bottom_toolbar: if ``True``, then a bottom toolbar will be displayed.
409408
:param complete_in_thread: if ``True``, then completion will run in a separate thread.
410409
:param command_sets: Provide CommandSet instances to load during cmd2 initialization.
411410
This allows CommandSets with custom constructor parameters to be
@@ -418,7 +417,7 @@ def __init__(
418417
:param persistent_history_file: file path to load a persistent cmd2 command history from
419418
:param persistent_history_length: max number of history items to write
420419
to the persistent history file
421-
:param refresh_interval: How often, in seconds, to refresh the UI. Defaults to 0.
420+
:param refresh_interval: How often, in seconds, to refresh the UI. Defaults to 0.0.
422421
prompt-toolkit already refreshes the UI every time a key is pressed.
423422
Set this value if you need the UI to update automatically without
424423
user input (e.g., for displaying a clock or background status
@@ -535,7 +534,6 @@ def __init__(
535534
self._initialize_history(persistent_history_file)
536535

537536
# Create the main PromptSession
538-
self.bottom_toolbar = bottom_toolbar
539537
self.complete_in_thread = complete_in_thread
540538
self.refresh_interval = refresh_interval
541539
self.main_session = self._create_main_session(auto_suggest, completekey)
@@ -759,7 +757,7 @@ def _(event: Any) -> None: # pragma: no cover
759757
# Base configuration
760758
kwargs: dict[str, Any] = {
761759
"auto_suggest": AutoSuggestFromHistory() if auto_suggest else None,
762-
"bottom_toolbar": self.get_bottom_toolbar if self.bottom_toolbar else None,
760+
"bottom_toolbar": self.get_bottom_toolbar,
763761
"color_depth": ColorDepth.TRUE_COLOR,
764762
"complete_style": CompleteStyle.MULTI_COLUMN,
765763
"complete_in_thread": self.complete_in_thread,
@@ -1983,49 +1981,35 @@ def ppretty(
19831981
end=end,
19841982
)
19851983

1986-
def get_bottom_toolbar(self) -> list[str | tuple[str, str]] | None:
1984+
def get_bottom_toolbar(self) -> AnyFormattedText:
19871985
"""Get the bottom toolbar content.
19881986
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).
1987+
This method is intended to be called by prompt-toolkit as a UI lifecycle callback.
1988+
Because prompt-toolkit triggers this callback on every UI refresh (e.g., on every
1989+
keypress and at scheduled refresh intervals), keeping this function highly optimized
1990+
is critical to ensuring the CLI remains responsive.
19911991
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
1992+
Override this if you want a bottom toolbar displaying contextual information useful for
1993+
your application. This could be information like the application name, current state,
1994+
or even a real-time clock.
20011995
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]
2006-
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
1996+
:return: Content to populate the bottom toolbar, or None to hide it.
1997+
"""
1998+
return None
20131999

2014-
# Return formatted text for prompt-toolkit
2015-
return [
2016-
("ansigreen", left_text),
2017-
("", padding),
2018-
("ansicyan", now),
2019-
]
2000+
def get_rprompt(self) -> AnyFormattedText:
2001+
"""Provide text to populate the prompt-toolkit right prompt.
20202002
2021-
def get_rprompt(self) -> str | FormattedText | None:
2022-
"""Provide text to populate prompt-toolkit right prompt with.
2003+
This method is intended to be called by prompt-toolkit as a UI lifecycle callback.
2004+
Because prompt-toolkit triggers this callback on every UI refresh (e.g., on every
2005+
keypress and at scheduled refresh intervals), keeping this function highly optimized
2006+
is critical to ensuring the CLI remains responsive.
20232007
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.
2008+
Override this if you want a right-prompt displaying contextual information useful for
2009+
your application. This could be information like the current Git branch, time, or current
2010+
working directory that is displayed without cluttering the main input area.
20272011
2028-
:return: any type of formatted text to display as the right prompt
2012+
:return: Content to populate the right prompt, or None to hide it.
20292013
"""
20302014
return None
20312015

@@ -2932,8 +2916,6 @@ def onecmd_plus_hooks(
29322916
command's stdout.
29332917
:return: True if running of commands should stop
29342918
"""
2935-
import datetime
2936-
29372919
stop = False
29382920
statement = None
29392921

docs/features/prompt.md

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,11 @@ 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, override the [cmd2.Cmd.get_bottom_toolbar][] method to return the content you
69+
wish to display.
6970

7071
```py
71-
class App(cmd2.Cmd):
72-
def __init__(self):
73-
super().__init__(bottom_toolbar=True)
74-
```
75-
76-
### Customizing Toolbar Content
77-
78-
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.
81-
82-
```py
83-
def get_bottom_toolbar(self) -> list[str | tuple[str, str]] | None:
72+
def get_bottom_toolbar(self) -> AnyFormattedText:
8473
return [
8574
('ansigreen', 'My Application Name'),
8675
('', ' - '),
@@ -92,7 +81,14 @@ text.
9281

9382
Since the toolbar is rendered by `prompt-toolkit` as part of the prompt, it is naturally redrawn
9483
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.
84+
display a clock), you can set `refresh_interval` in the [cmd2.Cmd.__init__][] constructor to a value
85+
greater than 0.0.
86+
87+
```py
88+
class App(cmd2.Cmd):
89+
def __init__(self):
90+
super().__init__(refresh_interval=0.5)
91+
```
9692

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

docs/upgrades.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ 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.
40-
- **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.
39+
- **Enablement**: Override the [cmd2.Cmd.get_bottom_toolbar][] method to return the content you wish
40+
to display.
4341

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

examples/getting_started.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
12) Persistent bottom toolbar with realtime status updates
1717
"""
1818

19+
import datetime
1920
import pathlib
2021

21-
from prompt_toolkit.formatted_text import FormattedText
22+
from prompt_toolkit.application import get_app
23+
from prompt_toolkit.formatted_text import AnyFormattedText
2224
from rich.style import Style
2325

2426
import cmd2
@@ -44,7 +46,6 @@ def __init__(self) -> None:
4446

4547
super().__init__(
4648
auto_suggest=True,
47-
bottom_toolbar=True,
4849
include_ipy=True,
4950
multiline_commands=["echo"],
5051
persistent_history_file="cmd2_history.dat",
@@ -87,11 +88,33 @@ def __init__(self) -> None:
8788
)
8889
)
8990

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

96119
def do_intro(self, _: cmd2.Statement) -> None:
97120
"""Display the intro banner."""

tests/test_cmd2.py

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4289,33 +4289,24 @@ def test_path_complete_users_windows(monkeypatch, base_app):
42894289

42904290

42914291
def test_get_bottom_toolbar(base_app, monkeypatch):
4292-
# Test default (disabled)
4292+
# Test default
42934293
assert base_app.get_bottom_toolbar() is None
42944294

4295-
# Test enabled
4296-
base_app.bottom_toolbar = True
4297-
monkeypatch.setattr(sys, "argv", ["myapp.py"])
4298-
toolbar = base_app.get_bottom_toolbar()
4299-
assert isinstance(toolbar, list)
4300-
assert toolbar[0] == ("ansigreen", "myapp.py")
4301-
assert toolbar[2][0] == "ansicyan"
4295+
# Test overridden
4296+
expected_text = "bottom toolbar text"
4297+
base_app.get_bottom_toolbar = lambda: expected_text
4298+
assert base_app.get_bottom_toolbar() == expected_text
43024299

43034300

43044301
def test_get_rprompt(base_app):
43054302
# Test default
43064303
assert base_app.get_rprompt() is None
43074304

43084305
# Test overridden
4309-
from prompt_toolkit.formatted_text import FormattedText
4310-
43114306
expected_text = "rprompt text"
43124307
base_app.get_rprompt = lambda: expected_text
43134308
assert base_app.get_rprompt() == expected_text
43144309

4315-
expected_formatted = FormattedText([("class:status", "OK")])
4316-
base_app.get_rprompt = lambda: expected_formatted
4317-
assert base_app.get_rprompt() == expected_formatted
4318-
43194310

43204311
def test_multiline_complete_statement_keyboard_interrupt(multiline_app, monkeypatch):
43214312
# Mock _read_command_line to raise KeyboardInterrupt
@@ -4499,25 +4490,6 @@ def my_pre_prompt():
44994490
assert loop_check["running"]
45004491

45014492

4502-
def test_get_bottom_toolbar_narrow_terminal(base_app, monkeypatch):
4503-
"""Test get_bottom_toolbar when terminal is too narrow for calculated padding"""
4504-
import shutil
4505-
4506-
base_app.bottom_toolbar = True
4507-
monkeypatch.setattr(sys, "argv", ["myapp.py"])
4508-
4509-
# Mock shutil.get_terminal_size to return a very small width (e.g. 5)
4510-
# Calculated padding_size = 5 - len('myapp.py') - len(now) - 1
4511-
# Since len(now) is ~29, this will definitely be < 1
4512-
monkeypatch.setattr(shutil, "get_terminal_size", lambda: os.terminal_size((5, 20)))
4513-
4514-
toolbar = base_app.get_bottom_toolbar()
4515-
assert isinstance(toolbar, list)
4516-
4517-
# The padding (index 1) should be exactly 1 space
4518-
assert toolbar[1] == ("", " ")
4519-
4520-
45214493
def test_auto_suggest_true():
45224494
"""Test that auto_suggest=True initializes AutoSuggestFromHistory."""
45234495
app = cmd2.Cmd(auto_suggest=True)

0 commit comments

Comments
 (0)