130130from .rl_utils import (
131131 RlType ,
132132 rl_escape_prompt ,
133+ rl_get_display_prompt ,
133134 rl_get_point ,
134135 rl_get_prompt ,
136+ rl_in_search_mode ,
135137 rl_set_prompt ,
136138 rl_type ,
137139 rl_warning ,
@@ -3295,6 +3297,12 @@ def _set_up_cmd2_readline(self) -> _SavedReadlineSettings:
32953297 """
32963298 readline_settings = _SavedReadlineSettings ()
32973299
3300+ if rl_type == RlType .GNU :
3301+ # To calculate line count when printing async_alerts, we rely on commands wider than
3302+ # the terminal to wrap across multiple lines. The default for horizontal-scroll-mode
3303+ # is "off" but a user may have overridden it in their readline initialization file.
3304+ readline .parse_and_bind ("set horizontal-scroll-mode off" )
3305+
32983306 if self ._completion_supported ():
32993307 # Set up readline for our tab completion needs
33003308 if rl_type == RlType .GNU :
@@ -5273,16 +5281,16 @@ class TestMyAppCase(Cmd2TestCase):
52735281 def async_alert (self , alert_msg : str , new_prompt : Optional [str ] = None ) -> None : # pragma: no cover
52745282 """
52755283 Display an important message to the user while they are at a command line prompt.
5276- To the user it appears as if an alert message is printed above the prompt and their current input
5277- text and cursor location is left alone.
5284+ To the user it appears as if an alert message is printed above the prompt and their
5285+ current input text and cursor location is left alone.
52785286
5279- IMPORTANT: This function will not print an alert unless it can acquire self.terminal_lock to ensure
5280- a prompt is onscreen. Therefore, it is best to acquire the lock before calling this function
5281- to guarantee the alert prints and to avoid raising a RuntimeError.
5287+ This function needs to acquire self.terminal_lock to ensure a prompt is on screen.
5288+ Therefore, it is best to acquire the lock before calling this function to avoid
5289+ raising a RuntimeError.
52825290
5283- This function is only needed when you need to print an alert while the main thread is blocking
5284- at the prompt. Therefore, this should never be called from the main thread. Doing so will
5285- raise a RuntimeError.
5291+ This function is only needed when you need to print an alert or update the prompt while the
5292+ main thread is blocking at the prompt. Therefore, this should never be called from the main
5293+ thread. Doing so will raise a RuntimeError.
52865294
52875295 :param alert_msg: the message to display to the user
52885296 :param new_prompt: If you also want to change the prompt that is displayed, then include it here.
@@ -5309,20 +5317,18 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None:
53095317 if new_prompt is not None :
53105318 self .prompt = new_prompt
53115319
5312- # Check if the prompt to display has changed from what's currently displayed
5313- cur_onscreen_prompt = rl_get_prompt ()
5314- new_onscreen_prompt = self .continuation_prompt if self ._at_continuation_prompt else self .prompt
5315-
5316- if new_onscreen_prompt != cur_onscreen_prompt :
5320+ # Check if the onscreen prompt needs to be refreshed to match self.prompt.
5321+ if self .need_prompt_refresh ():
53175322 update_terminal = True
5323+ rl_set_prompt (self .prompt )
53185324
53195325 if update_terminal :
53205326 import shutil
53215327
5322- # Generate the string which will replace the current prompt and input lines with the alert
5328+ # Print a string which replaces the onscreen prompt and input lines with the alert.
53235329 terminal_str = ansi .async_alert_str (
53245330 terminal_columns = shutil .get_terminal_size ().columns ,
5325- prompt = cur_onscreen_prompt ,
5331+ prompt = rl_get_display_prompt () ,
53265332 line = readline .get_line_buffer (),
53275333 cursor_offset = rl_get_point (),
53285334 alert_msg = alert_msg ,
@@ -5333,9 +5339,6 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None:
53335339 elif rl_type == RlType .PYREADLINE :
53345340 readline .rl .mode .console .write (terminal_str )
53355341
5336- # Update Readline's prompt before we redraw it
5337- rl_set_prompt (new_onscreen_prompt )
5338-
53395342 # Redraw the prompt and input lines below the alert
53405343 rl_force_redisplay ()
53415344
@@ -5346,30 +5349,50 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None:
53465349
53475350 def async_update_prompt (self , new_prompt : str ) -> None : # pragma: no cover
53485351 """
5349- Update the command line prompt while the user is still typing at it. This is good for alerting the user to
5350- system changes dynamically in between commands. For instance you could alter the color of the prompt to
5351- indicate a system status or increase a counter to report an event. If you do alter the actual text of the
5352- prompt, it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will
5353- be shifted and the update will not be seamless.
5352+ Update the command line prompt while the user is still typing at it.
53545353
5355- IMPORTANT: This function will not update the prompt unless it can acquire self.terminal_lock to ensure
5356- a prompt is onscreen. Therefore, it is best to acquire the lock before calling this function
5357- to guarantee the prompt changes and to avoid raising a RuntimeError.
5354+ This is good for alerting the user to system changes dynamically in between commands.
5355+ For instance you could alter the color of the prompt to indicate a system status or increase a
5356+ counter to report an event. If you do alter the actual text of the prompt, it is best to keep
5357+ the prompt the same width as what's on screen. Otherwise the user's input text will be shifted
5358+ and the update will not be seamless.
53585359
5359- This function is only needed when you need to update the prompt while the main thread is blocking
5360- at the prompt. Therefore, this should never be called from the main thread. Doing so will
5361- raise a RuntimeError.
5362-
5363- If user is at a continuation prompt while entering a multiline command, the onscreen prompt will
5364- not change. However, self.prompt will still be updated and display immediately after the multiline
5365- line command completes.
5360+ If user is at a continuation prompt while entering a multiline command, the onscreen prompt will
5361+ not change. However, self.prompt will still be updated and display immediately after the multiline
5362+ line command completes.
53665363
53675364 :param new_prompt: what to change the prompt to
53685365 :raises RuntimeError: if called from the main thread.
53695366 :raises RuntimeError: if called while another thread holds `terminal_lock`
53705367 """
53715368 self .async_alert ('' , new_prompt )
53725369
5370+ def async_refresh_prompt (self ) -> None : # pragma: no cover
5371+ """
5372+ Refresh the oncreen prompt to match self.prompt.
5373+
5374+ One case where the onscreen prompt and self.prompt can get out of sync is
5375+ when async_alert() is called while a user is in search mode (e.g. Ctrl-r).
5376+ To prevent overwriting readline's onscreen search prompt, self.prompt is updated
5377+ but readline's saved prompt isn't.
5378+
5379+ Therefore when a user aborts a search, the old prompt is still on screen until they
5380+ press Enter or this method is called. Call need_prompt_refresh() in an async print
5381+ thread to know when a refresh is needed.
5382+
5383+ :raises RuntimeError: if called from the main thread.
5384+ :raises RuntimeError: if called while another thread holds `terminal_lock`
5385+ """
5386+ self .async_alert ('' )
5387+
5388+ def need_prompt_refresh (self ) -> bool : # pragma: no cover
5389+ """Check whether the onscreen prompt needs to be asynchronously refreshed to match self.prompt."""
5390+ if not (vt100_support and self .use_rawinput ):
5391+ return False
5392+
5393+ # Don't overwrite a readline search prompt or a continuation prompt.
5394+ return not rl_in_search_mode () and not self ._at_continuation_prompt and self .prompt != rl_get_prompt ()
5395+
53735396 @staticmethod
53745397 def set_window_title (title : str ) -> None : # pragma: no cover
53755398 """
0 commit comments