Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
- Enhancements
- `enable_bottom_toolbar=True` now keeps the toolbar visible and refreshing during command
execution
- `Cmd.read_input()` and `Cmd.read_secret()` now keep the bottom toolbar visible while they wait
for input, refreshing at the same `refresh_interval` as the main prompt, instead of the
toolbar disappearing for the duration of the nested prompt. `Cmd.select()` is unchanged, since
prompt-toolkit's `choice()` offers no way to configure a refresh interval
- Added an embedded pager which `Cmd.ppaged()` uses while the bottom toolbar is running, so the
toolbar stays visible and refreshing instead of the terminal being handed to an external
pager. It supports vertical and horizontal scrolling, incremental search, and chopped lines on
Expand Down
4 changes: 4 additions & 0 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3776,6 +3776,7 @@ def read_input(

temp_session: PromptSession[str] = PromptSession(
auto_suggest=self.main_session.auto_suggest,
bottom_toolbar=self.get_bottom_toolbar if self.main_session.bottom_toolbar is not None else None,
color_depth=self.main_session.color_depth,
complete_style=self.main_session.complete_style,
complete_in_thread=self.main_session.complete_in_thread,
Expand All @@ -3786,6 +3787,7 @@ def read_input(
key_bindings=self.main_session.key_bindings,
input=self.main_session.input,
output=self.main_session.output,
refresh_interval=self.main_session.refresh_interval,
style=self.main_session.style,
)

Expand All @@ -3803,10 +3805,12 @@ def read_secret(
:raises Exception: any other exceptions raised by prompt()
"""
temp_session: PromptSession[str] = PromptSession(
bottom_toolbar=self.get_bottom_toolbar if self.main_session.bottom_toolbar is not None else None,
color_depth=self.main_session.color_depth,
enable_suspend=self.main_session.enable_suspend,
input=self.main_session.input,
output=self.main_session.output,
refresh_interval=self.main_session.refresh_interval,
style=self.main_session.style,
)

Expand Down
13 changes: 9 additions & 4 deletions docs/features/prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,15 @@ directly above the toolbar, and longer output becomes a scrollable, searchable v
`self.use_builtin_pager = False` to opt out and use your configured external `pager`/`pager_chop`
commands. See [Embedded pager](./os.md#embedded-pager) for the key bindings and full details.

cmd2 temporarily hides the toolbar for its input prompts, external pagers, Python environments, and
shell commands. It also hides it while a command's output is piped to another process, since that
process may be interactive, as `less` and `fzf` are. It restores the toolbar when those operations
finish.
cmd2 temporarily hides the toolbar for external pagers, Python environments, and shell commands. It
also hides it while a command's output is piped to another process, since that process may be
interactive, as `less` and `fzf` are. It restores the toolbar when those operations finish.

[cmd2.Cmd.read_input][] and [cmd2.Cmd.read_secret][] keep the toolbar visible, and it continues to
refresh at the same `refresh_interval` as the main prompt, so a clock or status display does not go
stale while your command waits for input. [cmd2.Cmd.select][] does not yet show one, because
prompt-toolkit's `choice()` has no way to configure a refresh interval and its toolbar would go
stale while the selection sits idle.

For custom terminal UIs, calls to `input()`, or subprocesses your own command code starts, use
[cmd2.Cmd.suspend_bottom_toolbar][]:
Expand Down
66 changes: 66 additions & 0 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2391,6 +2391,72 @@ def test_read_secret_eof(base_app, monkeypatch):
base_app.read_secret("Secret: ")


def _capture_nested_session(app, call):
"""Run `call` and return the real PromptSession cmd2 built for the nested prompt."""
captured = []

def fake_read_raw_input(prompt, session, **kwargs):
captured.append(session)
return "typed"

with mock.patch.object(app, "_read_raw_input", side_effect=fake_read_raw_input):
call()
assert len(captured) == 1
return captured[0]


def test_read_input_session_shows_the_bottom_toolbar(base_app) -> None:
"""A nested prompt keeps the persistent toolbar instead of dropping it.

read_input() builds its own PromptSession, so without being told about the
toolbar the bar simply vanishes for as long as the nested prompt is up.
"""
base_app.main_session.bottom_toolbar = base_app.get_bottom_toolbar

session = _capture_nested_session(base_app, lambda: base_app.read_input("Prompt> "))

assert session.bottom_toolbar == base_app.get_bottom_toolbar


def test_read_input_session_inherits_refresh_interval(base_app) -> None:
"""A toolbar clock keeps ticking while the nested prompt waits for input."""
base_app.main_session.bottom_toolbar = base_app.get_bottom_toolbar
base_app.main_session.refresh_interval = 0.25

session = _capture_nested_session(base_app, lambda: base_app.read_input("Prompt> "))

assert session.refresh_interval == 0.25


def test_read_secret_session_shows_the_bottom_toolbar(base_app) -> None:
"""Reading a secret keeps the persistent toolbar; the bar is not the secret."""
base_app.main_session.bottom_toolbar = base_app.get_bottom_toolbar

session = _capture_nested_session(base_app, lambda: base_app.read_secret("Secret: "))

assert session.bottom_toolbar == base_app.get_bottom_toolbar


def test_read_secret_session_inherits_refresh_interval(base_app) -> None:
"""A toolbar clock keeps ticking while a secret is being entered."""
base_app.main_session.bottom_toolbar = base_app.get_bottom_toolbar
base_app.main_session.refresh_interval = 0.25

session = _capture_nested_session(base_app, lambda: base_app.read_secret("Secret: "))

assert session.refresh_interval == 0.25


@pytest.mark.parametrize("method", ["read_input", "read_secret"])
def test_nested_prompt_has_no_toolbar_when_the_app_has_none(base_app, method) -> None:
"""An app without a bottom toolbar must not sprout one at a nested prompt."""
assert base_app.main_session.bottom_toolbar is None

session = _capture_nested_session(base_app, lambda: getattr(base_app, method)("Prompt> "))

assert session.bottom_toolbar is None


def test_read_input_passes_all_arguments_to_resolver(base_app):
mock_choices = ["choice1", "choice2"]
mock_provider = mock.MagicMock(name="provider")
Expand Down