33and changes the window title.
44"""
55
6+ import asyncio
7+ import contextlib
68import random
7- import threading
89import time
910
1011import cmd2
@@ -36,45 +37,57 @@ def __init__(self, *args, **kwargs) -> None:
3637
3738 self .prompt = "(APR)> "
3839
39- # The thread that will asynchronously alert the user of events
40- self ._stop_event = threading . Event ()
41- self ._alerter_thread = threading . Thread ()
40+ # The task that will asynchronously alert the user of events
41+ self ._alerter_task : asyncio . Task | None = None
42+ self ._alerts_enabled = True
4243 self ._alert_count = 0
4344 self ._next_alert_time = 0
4445
45- # Create some hooks to handle the starting and stopping of our thread
46- self .register_preloop_hook (self ._preloop_hook )
46+ # Register hook to stop alerts when the command loop finishes
4747 self .register_postloop_hook (self ._postloop_hook )
4848
49- def _preloop_hook (self ) -> None :
50- """Start the alerter thread."""
51- self . _stop_event . clear ()
52-
53- self . _alerter_thread = threading . Thread ( name = 'alerter' , target = self ._alerter_thread_func )
54- self ._alerter_thread . start ()
49+ def pre_prompt (self ) -> None :
50+ """Start the alerter task if enabled.
51+ This is called after the prompt event loop has started, so create_background_task works.
52+ """
53+ if self ._alerts_enabled :
54+ self ._start_alerter_task ()
5555
5656 def _postloop_hook (self ) -> None :
57- """Stops the alerter thread."""
58- self ._stop_event .set ()
59- if self ._alerter_thread .is_alive ():
60- self ._alerter_thread .join ()
57+ """Stops the alerter task."""
58+ self ._cancel_alerter_task ()
6159
6260 def do_start_alerts (self , _ ) -> None :
63- """Starts the alerter thread ."""
64- if self ._alerter_thread . is_alive () :
65- print ("The alert thread is already started" )
61+ """Starts the alerter task ."""
62+ if self ._alerts_enabled :
63+ print ("The alert task is already started" )
6664 else :
67- self ._stop_event .clear ()
68- self ._alerter_thread = threading .Thread (name = 'alerter' , target = self ._alerter_thread_func )
69- self ._alerter_thread .start ()
65+ self ._alerts_enabled = True
66+ # Task will be started in pre_prompt at next prompt
7067
7168 def do_stop_alerts (self , _ ) -> None :
72- """Stops the alerter thread."""
73- self ._stop_event .set ()
74- if self ._alerter_thread .is_alive ():
75- self ._alerter_thread .join ()
69+ """Stops the alerter task."""
70+ if not self ._alerts_enabled :
71+ print ("The alert task is already stopped" )
7672 else :
77- print ("The alert thread is already stopped" )
73+ self ._alerts_enabled = False
74+ self ._cancel_alerter_task ()
75+
76+ def _start_alerter_task (self ) -> None :
77+ """Start the alerter task if it's not running."""
78+ if self ._alerter_task is not None and not self ._alerter_task .done ():
79+ return
80+
81+ # self.session.app is the prompt_toolkit Application.
82+ # create_background_task creates a task that runs on the same loop as the app.
83+ with contextlib .suppress (RuntimeError ):
84+ self ._alerter_task = self .session .app .create_background_task (self ._alerter ())
85+
86+ def _cancel_alerter_task (self ) -> None :
87+ """Cancel the alerter task."""
88+ if self ._alerter_task is not None :
89+ self ._alerter_task .cancel ()
90+ self ._alerter_task = None
7891
7992 def _get_alerts (self ) -> list [str ]:
8093 """Reports alerts
@@ -147,13 +160,13 @@ def _generate_colored_prompt(self) -> str:
147160
148161 return stylize (self .visible_prompt , style = status_color )
149162
150- def _alerter_thread_func (self ) -> None :
163+ async def _alerter (self ) -> None :
151164 """Prints alerts and updates the prompt any time the prompt is showing."""
152165 self ._alert_count = 0
153166 self ._next_alert_time = 0
154167
155- while not self . _stop_event . is_set () :
156- try :
168+ try :
169+ while True :
157170 # Get any alerts that need to be printed
158171 alert_str = self ._generate_alert_str ()
159172
@@ -162,22 +175,26 @@ def _alerter_thread_func(self) -> None:
162175
163176 # Check if we have alerts to print
164177 if alert_str :
165- # new_prompt is an optional parameter to async_alert()
166- self .async_alert (alert_str , new_prompt )
178+ # We are running on the main loop, so we can print directly.
179+ # patch_stdout (active during read_input) handles the output.
180+ print (alert_str )
181+
182+ self .prompt = new_prompt
167183 new_title = f"Alerts Printed: { self ._alert_count } "
168184 self .set_window_title (new_title )
185+ self .session .app .invalidate ()
169186
170187 # Otherwise check if the prompt needs to be updated or refreshed
171188 elif self .prompt != new_prompt :
172- self .async_update_prompt (new_prompt )
189+ self .prompt = new_prompt
190+ self .session .app .invalidate ()
173191
174192 elif self .need_prompt_refresh ():
175- self .async_refresh_prompt ()
176-
177- except RuntimeError :
178- pass
193+ self .session .app .invalidate ()
179194
180- self ._stop_event .wait (0.5 )
195+ await asyncio .sleep (0.5 )
196+ except asyncio .CancelledError :
197+ pass
181198
182199
183200if __name__ == '__main__' :
0 commit comments