6767
6868import rich .box
6969from prompt_toolkit import print_formatted_text
70+ from prompt_toolkit .application import get_app
7071from rich .console import (
7172 Group ,
7273 RenderableType ,
@@ -608,11 +609,12 @@ def __init__(
608609 self ._command_parsers : _CommandParsers = _CommandParsers (self )
609610
610611 # Members related to printing asychronous alerts
611- self .alert_queue : queue .Queue [AsyncAlert ] = queue .Queue ()
612- self ._alerter_gate = threading .Event ()
613- self ._alerter_shutdown = threading .Event ()
614- self ._process_alerts_thread : threading .Thread | None = None
615- self ._prompt_drawn_at : float = 0.0 # Uses time.monotonic()
612+ self ._alert_queue : queue .Queue [AsyncAlert ] = queue .Queue ()
613+ self ._alert_condition = threading .Condition ()
614+ self ._alert_allowed = False
615+ self ._alert_shutdown_event = threading .Event ()
616+ self ._alert_thread : threading .Thread | None = None
617+ self ._alert_prompt_timestamp : float = 0.0 # Uses time.monotonic()
616618
617619 # Add functions decorated to be subcommands
618620 self ._register_subcommands (self )
@@ -3351,40 +3353,34 @@ def read_input(
33513353 return self ._read_raw_input (prompt , temp_session , completer_to_use )
33523354
33533355 def _process_alerts (self ) -> None :
3354- """Background worker that processes queued alerts and prompt updates.
3356+ """Background worker that processes queued alerts and dynamic prompt updates."""
3357+ while not self ._alert_shutdown_event .is_set ():
3358+ with self ._alert_condition :
3359+ # Wait until alerts are allowed and available, or shutdown is signaled.
3360+ self ._alert_condition .wait_for (
3361+ lambda : (not self ._alert_queue .empty () and self ._alert_allowed ) or self ._alert_shutdown_event .is_set ()
3362+ )
33553363
3356- This loop waits for the prompt gate to open, ensuring that background
3357- messages and UI refreshes only occur while the user is at an
3358- interactive prompt, avoiding interference with active commands.
3359- """
3360- while not self ._alerter_shutdown .is_set ():
3361- try :
3362- # Wait for an alert
3363- alert = self .alert_queue .get (timeout = 0.1 )
3364+ if self ._alert_shutdown_event .is_set ():
3365+ break
33643366
3365- # Block if not at a prompt
3366- while not self ._alerter_gate .is_set ():
3367- if self ._alerter_shutdown .is_set ():
3368- return
3369- self ._alerter_gate .wait (timeout = 0.1 )
3367+ # Get the next alert while still holding the condition lock.
3368+ alert = self ._alert_queue .get ()
33703369
3371- # Print and update
3370+ if alert .msg :
3371+ # Print the message above the current prompt.
33723372 with patch_stdout ():
3373- if alert .msg :
3374- print_formatted_text (pt_filter_style (alert .msg ))
3375-
3376- # Only update if the alert was generated after the current prompt was drawn on the screen.
3377- if (alert .prompt is not None and
3378- alert .prompt != self .prompt and
3379- alert .timestamp > self ._prompt_drawn_at ): # fmt: skip
3380- self .prompt = alert .prompt
3373+ print_formatted_text (pt_filter_style (alert .msg ))
33813374
3382- # Don't update the UI if we are at a continuation prompt.
3383- if not self ._at_continuation_prompt :
3384- self .session .app .invalidate ()
3375+ # Only apply prompt changes generated after the current prompt started.
3376+ if (alert .prompt is not None and
3377+ alert .prompt != self .prompt and
3378+ alert .timestamp > self ._alert_prompt_timestamp ): # fmt: skip
3379+ self .prompt = alert .prompt
33853380
3386- except queue .Empty : # noqa: PERF203
3387- continue
3381+ # Refresh UI immediately unless at a continuation prompt.
3382+ if not self ._at_continuation_prompt :
3383+ get_app ().invalidate ()
33883384
33893385 def _read_command_line (self , prompt : str ) -> str :
33903386 """Read the next command line from the input stream.
@@ -3396,7 +3392,7 @@ def _read_command_line(self, prompt: str) -> str:
33963392 """
33973393
33983394 # Use dynamic prompt if the prompt matches self.prompt
3399- def get_prompt () -> ANSI :
3395+ def get_prompt () -> str | ANSI :
34003396 return pt_filter_style (self .prompt )
34013397
34023398 prompt_to_use : Callable [[], ANSI | str ] | ANSI | str = ANSI (prompt )
@@ -3407,17 +3403,19 @@ def _pre_prompt() -> None:
34073403 """Run standard pre-prompt processing and activate the background alerter."""
34083404 self .pre_prompt ()
34093405
3410- # Record exactly when the user was presented with this prompt
3411- self ._prompt_drawn_at = time .monotonic ()
3406+ # Record when this prompt was started.
3407+ self ._alert_prompt_timestamp = time .monotonic ()
34123408
3413- # Start alerter thread if it's not already running
3414- if self ._process_alerts_thread is None or not self ._process_alerts_thread .is_alive ():
3415- self ._alerter_shutdown .clear ()
3416- self ._process_alerts_thread = threading .Thread (target = self ._process_alerts , daemon = True )
3417- self ._process_alerts_thread .start ()
3409+ # Start alerter thread if it's not already running.
3410+ if self ._alert_thread is None or not self ._alert_thread .is_alive ():
3411+ self ._alert_shutdown_event .clear ()
3412+ self ._alert_thread = threading .Thread (target = self ._process_alerts , daemon = True )
3413+ self ._alert_thread .start ()
34183414
3419- # Allow alerts to be printed
3420- self ._alerter_gate .set ()
3415+ # Allow alerts to be printed now that we are at a prompt.
3416+ with self ._alert_condition :
3417+ self ._alert_allowed = True
3418+ self ._alert_condition .notify_all ()
34213419
34223420 try :
34233421 return self ._read_raw_input (
@@ -3427,8 +3425,9 @@ def _pre_prompt() -> None:
34273425 pre_run = _pre_prompt ,
34283426 )
34293427 finally :
3430- # Ensure no alerts print while the command is processing
3431- self ._alerter_gate .clear ()
3428+ # Ensure no alerts print while not at a prompt.
3429+ with self ._alert_condition :
3430+ self ._alert_allowed = False
34323431
34333432 def _cmdloop (self ) -> None :
34343433 """Repeatedly issue a prompt, accept input, parse it, and dispatch to apporpriate commands.
@@ -3457,16 +3456,17 @@ def _cmdloop(self) -> None:
34573456 stop = self .onecmd_plus_hooks (line )
34583457 finally :
34593458 with self .sigint_protection :
3460- # Shut down the _process_alerts_thread
3461- if self ._process_alerts_thread is not None and self ._process_alerts_thread .is_alive ():
3462- self ._alerter_shutdown .set ()
3463-
3464- # Worker is a daemon polling every 0.1s. We join with a 1.0s
3465- # safety timeout that is highly unlikely to be reached.
3466- # If it is, the daemon status ensures the OS reaps the
3467- # thread when the process exits rather than hanging.
3468- self ._process_alerts_thread .join (timeout = 1.0 )
3469- self ._process_alerts_thread = None
3459+ # Shut down the alert thread.
3460+ if self ._alert_thread is not None :
3461+ with self ._alert_condition :
3462+ self ._alert_shutdown_event .set ()
3463+ self ._alert_condition .notify_all ()
3464+
3465+ # The thread is event-driven and stays suspended until notified.
3466+ # We join with a 1 second timeout as a safety measure. If it hangs,
3467+ # the daemon status allows the OS to reap it on exit.
3468+ self ._alert_thread .join (timeout = 1.0 )
3469+ self ._alert_thread = None
34703470
34713471 #############################################################
34723472 # Parsers and functions for alias command and subcommands
@@ -5303,18 +5303,24 @@ def do__relative_run_script(self, args: argparse.Namespace) -> bool | None:
53035303 return self .do_run_script (su .quote (relative_path ))
53045304
53055305 def add_alert (self , * , msg : str | None = None , prompt : str | None = None ) -> None :
5306- """Thread-safe method to request UI updates.
5306+ """Queue an asynchronous alert to be displayed when the prompt is active.
5307+
5308+ Examples:
5309+ add_alert(msg="System error!") # Print message only
5310+ add_alert(prompt="user@host> ") # Update prompt only
5311+ add_alert(msg="Done", prompt="> ") # Update both
53075312
53085313 :param msg: an optional message to be printed above the prompt.
53095314 :param prompt: an optional string to dynamically replace the current prompt.
53105315
5311- 1. print an alert: add_alert(msg="System error!")
5312- 2. print and update prompt: add_alert(msg="Logged in", prompt="user@host> ")
5313- 3. update prompt only: add_alert(prompt="waiting> ")
53145316 """
5315- if msg is not None or prompt is not None :
5317+ if msg is None and prompt is None :
5318+ return
5319+
5320+ with self ._alert_condition :
53165321 alert = AsyncAlert (msg = msg , prompt = prompt )
5317- self .alert_queue .put (alert )
5322+ self ._alert_queue .put (alert )
5323+ self ._alert_condition .notify_all ()
53185324
53195325 @staticmethod
53205326 def set_window_title (title : str ) -> None : # pragma: no cover
0 commit comments