Skip to content

Commit

Permalink
Fixed bug where async_alert() was overwriting readline incremental an…
Browse files Browse the repository at this point in the history
…d non-incremental search prompts.
  • Loading branch information
kmvanbrunt committed Oct 11, 2024
1 parent 16c6d30 commit 875428f
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 14 deletions.
6 changes: 3 additions & 3 deletions cmd2/ansi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
26 changes: 17 additions & 9 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,10 @@
from .rl_utils import (
RlType,
rl_escape_prompt,
rl_get_display_prompt,
rl_get_point,
rl_get_prompt,
rl_in_search_mode,
rl_set_prompt,
rl_type,
rl_warning,
Expand Down Expand Up @@ -3295,6 +3297,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:
Expand Down Expand Up @@ -5309,17 +5317,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
cur_onscreen_prompt = rl_get_display_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,
Expand All @@ -5333,9 +5344,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()

Expand Down
52 changes: 50 additions & 2 deletions cmd2/rl_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def rl_get_point() -> int: # pragma: no cover


def rl_get_prompt() -> str: # pragma: no cover
"""Gets Readline's current prompt"""
"""Get Readline's prompt"""
if rl_type == RlType.GNU:
encoded_prompt = ctypes.c_char_p.in_dll(readline_lib, "rl_prompt").value
if encoded_prompt is None:
Expand All @@ -221,6 +221,24 @@ def rl_get_prompt() -> str: # pragma: no cover
return rl_unescape_prompt(prompt)


def rl_get_display_prompt() -> str: # pragma: no cover
"""
Get Readline's currently displayed prompt.
In GNU Readline, the displayed prompt sometimes differs from the prompt.
This occurs in functions that use the prompt string as a message area, such as incremental search.
"""
if rl_type == RlType.GNU:
encoded_prompt = ctypes.c_char_p.in_dll(readline_lib, "rl_display_prompt").value
if encoded_prompt is None:
prompt = ''
else:
prompt = encoded_prompt.decode(encoding='utf-8')
return rl_unescape_prompt(prompt)
else:
return rl_get_prompt()


def rl_set_prompt(prompt: str) -> None: # pragma: no cover
"""
Sets Readline's prompt
Expand All @@ -237,7 +255,8 @@ def rl_set_prompt(prompt: str) -> None: # pragma: no cover


def rl_escape_prompt(prompt: str) -> str:
"""Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes
"""
Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes
:param prompt: original prompt
:return: prompt safe to pass to GNU Readline
Expand Down Expand Up @@ -276,3 +295,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

0 comments on commit 875428f

Please sign in to comment.