diff --git a/cmd2/ansi.py b/cmd2/ansi.py index 62b85384..93c2f019 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -1071,9 +1071,9 @@ def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_off # Calculate how many terminal lines are taken up by all prompt lines except for the last one. # That will be included in the input lines calculations since that is where the cursor is. num_prompt_terminal_lines = 0 - for line in prompt_lines[:-1]: - line_width = style_aware_wcswidth(line) - num_prompt_terminal_lines += int(line_width / terminal_columns) + 1 + for prompt_line in prompt_lines[:-1]: + prompt_line_width = style_aware_wcswidth(prompt_line) + num_prompt_terminal_lines += int(prompt_line_width / terminal_columns) + 1 # Now calculate how many terminal lines are take up by the input last_prompt_line = prompt_lines[-1] diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 79fd2bf2..50733e2c 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -132,6 +132,7 @@ rl_escape_prompt, rl_get_point, rl_get_prompt, + rl_in_search_mode, rl_set_prompt, rl_type, rl_warning, @@ -3295,6 +3296,12 @@ def _set_up_cmd2_readline(self) -> _SavedReadlineSettings: """ readline_settings = _SavedReadlineSettings() + if rl_type == RlType.GNU: + # To calculate line count when printing async_alerts, we rely on commands wider than + # the terminal to wrap across multiple lines. The default for horizontal-scroll-mode + # is "off" but a user may have overridden it in their readline initialization file. + readline.parse_and_bind("set horizontal-scroll-mode off") + if self._completion_supported(): # Set up readline for our tab completion needs if rl_type == RlType.GNU: @@ -5309,17 +5316,20 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: if new_prompt is not None: self.prompt = new_prompt - # Check if the prompt to display has changed from what's currently displayed cur_onscreen_prompt = rl_get_prompt() - new_onscreen_prompt = self.continuation_prompt if self._at_continuation_prompt else self.prompt - if new_onscreen_prompt != cur_onscreen_prompt: - update_terminal = True + # We won't change the onscreen prompt while readline is in search mode (e.g. Ctrl-r) + if not rl_in_search_mode(): + # Check if the prompt to display has changed from what's currently displayed + new_onscreen_prompt = self.continuation_prompt if self._at_continuation_prompt else self.prompt + if new_onscreen_prompt != cur_onscreen_prompt: + update_terminal = True + rl_set_prompt(new_onscreen_prompt) if update_terminal: import shutil - # Generate the string which will replace the current prompt and input lines with the alert + # Print a string which replaces the current prompt and input lines with the alert terminal_str = ansi.async_alert_str( terminal_columns=shutil.get_terminal_size().columns, prompt=cur_onscreen_prompt, @@ -5333,9 +5343,6 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: elif rl_type == RlType.PYREADLINE: readline.rl.mode.console.write(terminal_str) - # Update Readline's prompt before we redraw it - rl_set_prompt(new_onscreen_prompt) - # Redraw the prompt and input lines below the alert rl_force_redisplay() diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index 28d9d2d6..3104be99 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -276,3 +276,32 @@ def rl_unescape_prompt(prompt: str) -> str: prompt = prompt.replace(escape_start, "").replace(escape_end, "") return prompt + + +def rl_in_search_mode() -> bool: + """Check if readline is doing either an incremental (e.g. Ctrl-r) or non-incremental (e.g. Esc-p) search""" + if rl_type == RlType.GNU: + # GNU Readline defines constants that we can use to determine if in search mode. + # RL_STATE_ISEARCH 0x0000080 + # RL_STATE_NSEARCH 0x0000100 + IN_SEARCH_MODE = 0x0000180 + + readline_state = ctypes.c_int.in_dll(readline_lib, "rl_readline_state").value + return bool(IN_SEARCH_MODE & readline_state) + elif rl_type == RlType.PYREADLINE: + from pyreadline3.modes.emacs import ( # type: ignore[import] + EmacsMode, + ) + + # These search modes only apply to Emacs mode, which is the default. + if not isinstance(readline.rl.mode, EmacsMode): + return False + + # While in search mode, the current keyevent function is set one of the following. + search_funcs = ( + readline.rl.mode._process_incremental_search_keyevent, + readline.rl.mode._process_non_incremental_search_keyevent, + ) + return readline.rl.mode.process_keyevent_queue[-1] in search_funcs + else: + return False