Skip to content

Commit f156b75

Browse files
committed
Updated async printing example to use Rich renderables.
1 parent f4a1962 commit f156b75

3 files changed

Lines changed: 65 additions & 76 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
- Bug Fixes
44
- Fixed a couple type issues that emerged with the upgrade to `mypy` 2.2.0
5+
- Enhancements
6+
- Async alert messages can now be Rich renderables.
57

68
## 4.1.0 (July 7, 2026)
79

cmd2/cmd2.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,8 @@ def remove(self, command_method: BoundCommandFunc) -> None:
312312
class AsyncAlert:
313313
"""Contents of an asynchronous alert which display while user is at prompt.
314314
315-
:param msg: an optional message to be printed above the prompt.
315+
:param msg: an optional printable object (including Rich renderables) to be
316+
printed above the prompt.
316317
:param soft_wrap: Enable soft wrap mode. This only applies with msg is not None.
317318
Defaults to True. See print_to() docstring for more details on
318319
this parameter.
@@ -323,7 +324,7 @@ class AsyncAlert:
323324
to avoid a stale display, but its msg data will still be displayed.
324325
"""
325326

326-
msg: RenderableType | None = None
327+
msg: Any | None = None
327328
soft_wrap: bool = True
328329
prompt: str | None = None
329330
timestamp: float = field(default_factory=time.monotonic, init=False)
@@ -5635,7 +5636,7 @@ def do__relative_run_script(self, args: argparse.Namespace) -> bool | None:
56355636
def add_alert(
56365637
self,
56375638
*,
5638-
msg: RenderableType | None = None,
5639+
msg: Any | None = None,
56395640
soft_wrap: bool = True,
56405641
prompt: str | None = None,
56415642
) -> None:
@@ -5646,7 +5647,8 @@ def add_alert(
56465647
add_alert(prompt="user@host> ") # Update prompt only
56475648
add_alert(msg="Done", prompt="> ") # Update both
56485649
5649-
:param msg: an optional message to be printed above the prompt.
5650+
:param msg: an optional printable object (including Rich renderables) to be
5651+
printed above the prompt.
56505652
:param soft_wrap: Enable soft wrap mode. This only applies with msg is not None.
56515653
Defaults to True. See print_to() docstring for more details on
56525654
this parameter.

examples/async_printing.py

Lines changed: 57 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,42 @@
66
import secrets
77
import threading
88
import time
9+
from typing import Any
10+
11+
from rich.console import Group
12+
from rich.panel import Panel
13+
from rich.table import Table
14+
from rich.text import Text
915

1016
import cmd2
1117
from cmd2 import (
1218
Color,
1319
stylize,
1420
)
1521

16-
ALERTS = [
17-
"Watch as this application prints alerts and updates the prompt",
18-
"This will only happen when the prompt is present",
19-
"Notice how it doesn't interfere with your typing or cursor location",
20-
"Go ahead and type some stuff and move the cursor throughout the line",
21-
"Keep typing...",
22-
"Move that cursor...",
23-
"Pretty seamless, eh?",
24-
"Feedback can also be given in the window title. Notice the alert count up there?",
25-
"You can stop and start the alerts by typing stop_alerts and start_alerts",
26-
"This demo will now continue to print alerts at random intervals",
27-
]
22+
23+
def get_alerts() -> list[tuple[Any, bool]]:
24+
"""Return a list of (alert_msg, soft_wrap) tuples."""
25+
table = Table("Command", "Description", title="Quick Help")
26+
table.add_row("start_alerts", "Start the async alert generator")
27+
table.add_row("stop_alerts", "Stop the async alert generator")
28+
table.add_row("help", "Show help menu")
29+
30+
return [
31+
(Text("Watch as this application prints alerts asynchronously!", style="bold bright_cyan"), True),
32+
("Notice how alerts don't interfere with your typing or cursor location.", True),
33+
(
34+
Panel(
35+
"This message is wrapped in a Rich Panel!",
36+
title="System Alert",
37+
border_style="bright_blue",
38+
expand=False,
39+
),
40+
False,
41+
),
42+
(table, False),
43+
("You can stop and start the alerts by typing stop_alerts and start_alerts.", True),
44+
]
2845

2946

3047
class AlerterApp(cmd2.Cmd):
@@ -40,7 +57,6 @@ def __init__(self) -> None:
4057
self._stop_event = threading.Event()
4158
self._add_alert_thread = threading.Thread()
4259
self._alert_count = 0
43-
self._next_alert_time = 0.0
4460

4561
# Create some hooks to handle the starting and stopping of our thread
4662
self.register_preloop_hook(self._preloop_hook)
@@ -78,56 +94,6 @@ def do_stop_alerts(self, _: cmd2.Statement) -> None:
7894
else:
7995
print("The alert thread is already stopped")
8096

81-
def _get_alerts(self) -> list[str]:
82-
"""Reports alerts
83-
:return: the list of alerts.
84-
"""
85-
cur_time = time.monotonic()
86-
if cur_time < self._next_alert_time:
87-
return []
88-
89-
alerts = []
90-
91-
if self._alert_count < len(ALERTS):
92-
alerts.append(ALERTS[self._alert_count])
93-
self._alert_count += 1
94-
self._next_alert_time = cur_time + 4
95-
96-
else:
97-
rand_num = self._secure_generator.randint(1, 20)
98-
if rand_num > 2:
99-
return []
100-
101-
for _ in range(rand_num):
102-
self._alert_count += 1
103-
alerts.append(f"Alert {self._alert_count}")
104-
105-
self._next_alert_time = 0
106-
107-
return alerts
108-
109-
def _build_alert_str(self) -> str:
110-
"""Combines alerts into one string that can be printed to the terminal
111-
:return: the alert string.
112-
"""
113-
alert_str = ""
114-
alerts = self._get_alerts()
115-
116-
longest_alert = max(ALERTS, key=len)
117-
num_asterisks = len(longest_alert) + 8
118-
119-
for i, cur_alert in enumerate(alerts):
120-
# Use padding to center the alert
121-
padding = " " * int((num_asterisks - len(cur_alert)) / 2)
122-
123-
if i > 0:
124-
alert_str += "\n"
125-
alert_str += "*" * num_asterisks + "\n"
126-
alert_str += padding + cur_alert + padding + "\n"
127-
alert_str += "*" * num_asterisks + "\n"
128-
129-
return alert_str
130-
13197
def _build_colored_prompt(self) -> str:
13298
"""Randomly builds a colored prompt
13399
:return: the new prompt.
@@ -152,26 +118,45 @@ def _build_colored_prompt(self) -> str:
152118
def _add_alerts_func(self) -> None:
153119
"""Prints alerts and updates the prompt any time the prompt is showing."""
154120
self._alert_count = 0
155-
self._next_alert_time = 0
121+
122+
alerts = get_alerts()
123+
alert_index = 0
124+
last_alert_time = 0.0
156125

157126
while not self._stop_event.is_set():
158-
# Get any alerts that need to be printed
159-
alert_str = self._build_alert_str()
127+
cur_time = time.monotonic()
128+
alert_msg = None
129+
soft_wrap = True
130+
131+
# Trigger the next alert every 4 seconds
132+
if cur_time - last_alert_time >= 4.0:
133+
alert_msg, soft_wrap = alerts[alert_index]
134+
alert_index = (alert_index + 1) % len(alerts)
135+
self._alert_count += 1
136+
last_alert_time = cur_time
160137

161-
# Build a new prompt
138+
# Build a new prompt (color changes randomly)
162139
new_prompt = self._build_colored_prompt()
163140

164-
# Check if we have alerts to print
165-
if alert_str:
166-
self.add_alert(msg=alert_str, prompt=new_prompt)
141+
# Check if we have an alert to print
142+
if alert_msg is not None:
143+
# Wrap the alert message and an empty string in a Rich Group.
144+
# This is a clean way to append a blank line after any RenderableType
145+
# (strings, panels, tables, etc.) for visual separation.
146+
self.add_alert(
147+
msg=Group(alert_msg, ""),
148+
soft_wrap=soft_wrap,
149+
prompt=new_prompt,
150+
)
151+
167152
new_title = f"Alerts Printed: {self._alert_count}"
168153
self.set_window_title(new_title)
169154

170-
# Otherwise check if the prompt needs to be updated or refreshed
155+
# Otherwise, check if the prompt needs to be updated or refreshed
171156
elif self.prompt != new_prompt:
172157
self.add_alert(prompt=new_prompt)
173158

174-
self._stop_event.wait(0.5)
159+
self._stop_event.wait(1.0)
175160

176161

177162
if __name__ == "__main__":

0 commit comments

Comments
 (0)