3232import argparse
3333import cmd
3434import collections
35- import colorama
36- from colorama import Fore
3735import glob
3836import inspect
3937import os
4341import threading
4442from typing import Any , Callable , Dict , List , Mapping , Optional , Tuple , Type , Union , IO
4543
44+ import colorama
45+ from colorama import Fore
46+ from wcwidth import wcswidth
47+
4648from . import constants
47- from . import utils
4849from . import plugin
50+ from . import utils
4951from .argparse_completer import AutoCompleter , ACArgumentParser , ACTION_ARG_CHOICES
5052from .clipboard import can_clip , get_paste_buffer , write_to_paste_buffer
5153from .parsing import StatementParser , Statement , Macro , MacroArg
5254
5355# Set up readline
5456from .rl_utils import rl_type , RlType , rl_get_point , rl_set_prompt , vt100_support , rl_make_safe_prompt
57+
5558if rl_type == RlType .NONE : # pragma: no cover
5659 rl_warning = "Readline features including tab completion have been disabled since no \n " \
5760 "supported version of readline was found. To resolve this, install \n " \
7174
7275 elif rl_type == RlType .GNU :
7376
74- # We need wcswidth to calculate display width of tab completions
75- from wcwidth import wcswidth
76-
7777 # Get the readline lib so we can make changes to it
7878 import ctypes
7979 from .rl_utils import readline_lib
@@ -457,6 +457,9 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
457457 # Used to keep track of whether we are redirecting or piping output
458458 self .redirecting = False
459459
460+ # Used to keep track of whether a continuation prompt is being displayed
461+ self .at_continuation_prompt = False
462+
460463 # If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing
461464 self .broken_pipe_warning = ''
462465
@@ -1845,6 +1848,7 @@ def _complete_statement(self, line: str) -> Statement:
18451848 # - a multiline command with unclosed quotation marks
18461849 if not self .quit_on_sigint :
18471850 try :
1851+ self .at_continuation_prompt = True
18481852 newline = self .pseudo_raw_input (self .continuation_prompt )
18491853 if newline == 'eof' :
18501854 # they entered either a blank line, or we hit an EOF
@@ -1858,8 +1862,13 @@ def _complete_statement(self, line: str) -> Statement:
18581862 self .poutput ('^C' )
18591863 statement = self .statement_parser .parse ('' )
18601864 break
1865+ finally :
1866+ self .at_continuation_prompt = False
18611867 else :
1868+ self .at_continuation_prompt = True
18621869 newline = self .pseudo_raw_input (self .continuation_prompt )
1870+ self .at_continuation_prompt = False
1871+
18631872 if newline == 'eof' :
18641873 # they entered either a blank line, or we hit an EOF
18651874 # for some other reason. Turn the literal 'eof'
@@ -2074,11 +2083,6 @@ def pseudo_raw_input(self, prompt: str) -> str:
20742083 - if input is a pipe (instead of a tty), look at self.echo
20752084 to decide whether to print the prompt and the input
20762085 """
2077-
2078- # Temporarily save over self.prompt to reflect what will be on screen
2079- orig_prompt = self .prompt
2080- self .prompt = prompt
2081-
20822086 if self .use_rawinput :
20832087 try :
20842088 if sys .stdin .isatty ():
@@ -2122,9 +2126,6 @@ def pseudo_raw_input(self, prompt: str) -> str:
21222126 else :
21232127 line = 'eof'
21242128
2125- # Restore prompt
2126- self .prompt = orig_prompt
2127-
21282129 return line .strip ()
21292130
21302131 def _cmdloop (self ) -> bool :
@@ -3435,50 +3436,6 @@ class TestMyAppCase(Cmd2TestCase):
34353436 runner = unittest .TextTestRunner ()
34363437 runner .run (testcase )
34373438
3438- def _clear_input_lines_str (self ) -> str : # pragma: no cover
3439- """
3440- Returns a string that if printed will clear the prompt and input lines in the terminal,
3441- leaving the cursor at the beginning of the first input line
3442- :return: the string to print
3443- """
3444- if not (vt100_support and self .use_rawinput ):
3445- return ''
3446-
3447- import shutil
3448- import colorama .ansi as ansi
3449- from colorama import Cursor
3450-
3451- visible_prompt = self .visible_prompt
3452-
3453- # Get the size of the terminal
3454- terminal_size = shutil .get_terminal_size ()
3455-
3456- # Figure out how many lines the prompt and user input take up
3457- total_str_size = len (visible_prompt ) + len (readline .get_line_buffer ())
3458- num_input_lines = int (total_str_size / terminal_size .columns ) + 1
3459-
3460- # Get the cursor's offset from the beginning of the first input line
3461- cursor_input_offset = len (visible_prompt ) + rl_get_point ()
3462-
3463- # Calculate what input line the cursor is on
3464- cursor_input_line = int (cursor_input_offset / terminal_size .columns ) + 1
3465-
3466- # Create a string that will clear all input lines and print the alert
3467- terminal_str = ''
3468-
3469- # Move the cursor down to the last input line
3470- if cursor_input_line != num_input_lines :
3471- terminal_str += Cursor .DOWN (num_input_lines - cursor_input_line )
3472-
3473- # Clear each input line from the bottom up so that the cursor ends up on the original first input line
3474- terminal_str += (ansi .clear_line () + Cursor .UP (1 )) * (num_input_lines - 1 )
3475- terminal_str += ansi .clear_line ()
3476-
3477- # Move the cursor to the beginning of the first input line
3478- terminal_str += '\r '
3479-
3480- return terminal_str
3481-
34823439 def async_alert (self , alert_msg : str , new_prompt : Optional [str ] = None ) -> None : # pragma: no cover
34833440 """
34843441 Display an important message to the user while they are at the prompt in between commands.
@@ -3497,27 +3454,70 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None:
34973454 if not (vt100_support and self .use_rawinput ):
34983455 return
34993456
3457+ import shutil
3458+ import colorama .ansi as ansi
3459+ from colorama import Cursor
3460+
35003461 # Sanity check that can't fail if self.terminal_lock was acquired before calling this function
35013462 if self .terminal_lock .acquire (blocking = False ):
35023463
3503- # Generate a string to clear the prompt and input lines and replace with the alert
3504- terminal_str = self ._clear_input_lines_str ()
3464+ # Figure out what prompt is displaying
3465+ current_prompt = self .continuation_prompt if self .at_continuation_prompt else self .prompt
3466+
3467+ # Only update terminal if there are changes
3468+ update_terminal = False
3469+
35053470 if alert_msg :
3506- terminal_str += alert_msg + '\n '
3471+ alert_msg += '\n '
3472+ update_terminal = True
35073473
3508- # Set the new prompt now that _clear_input_lines_str is done using the old prompt
3509- if new_prompt is not None :
3474+ # Set the prompt if its changed
3475+ if new_prompt is not None and new_prompt != self . prompt :
35103476 self .prompt = new_prompt
3511- rl_set_prompt (self .prompt )
35123477
3513- # Print terminal_str to erase the lines
3514- if rl_type == RlType .GNU :
3515- sys .stderr .write (terminal_str )
3516- elif rl_type == RlType .PYREADLINE :
3517- readline .rl .mode .console .write (terminal_str )
3478+ # If we aren't at a continuation prompt, then redraw the prompt now
3479+ if not self .at_continuation_prompt :
3480+ rl_set_prompt (self .prompt )
3481+ update_terminal = True
35183482
3519- # Redraw the prompt and input lines
3520- rl_force_redisplay ()
3483+ if update_terminal :
3484+ # Remove ansi characters to get the visible width of the prompt
3485+ prompt_width = wcswidth (utils .strip_ansi (current_prompt ))
3486+
3487+ # Get the size of the terminal
3488+ terminal_size = shutil .get_terminal_size ()
3489+
3490+ # Figure out how many lines the prompt and user input take up
3491+ total_str_size = prompt_width + wcswidth (readline .get_line_buffer ())
3492+ num_input_lines = int (total_str_size / terminal_size .columns ) + 1
3493+
3494+ # Get the cursor's offset from the beginning of the first input line
3495+ cursor_input_offset = prompt_width + rl_get_point ()
3496+
3497+ # Calculate what input line the cursor is on
3498+ cursor_input_line = int (cursor_input_offset / terminal_size .columns ) + 1
3499+
3500+ # Create a string that when printed will clear all input lines and display the alert
3501+ terminal_str = ''
3502+
3503+ # Move the cursor down to the last input line
3504+ if cursor_input_line != num_input_lines :
3505+ terminal_str += Cursor .DOWN (num_input_lines - cursor_input_line )
3506+
3507+ # Clear each input line from the bottom up so that the cursor ends up on the original first input line
3508+ terminal_str += (ansi .clear_line () + Cursor .UP (1 )) * (num_input_lines - 1 )
3509+ terminal_str += ansi .clear_line ()
3510+
3511+ # Move the cursor to the beginning of the first input line and print the alert
3512+ terminal_str += '\r ' + alert_msg
3513+
3514+ if rl_type == RlType .GNU :
3515+ sys .stderr .write (terminal_str )
3516+ elif rl_type == RlType .PYREADLINE :
3517+ readline .rl .mode .console .write (terminal_str )
3518+
3519+ # Redraw the prompt and input lines
3520+ rl_force_redisplay ()
35213521
35223522 self .terminal_lock .release ()
35233523
@@ -3536,6 +3536,10 @@ def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
35363536 a prompt is onscreen. Therefore it is best to acquire the lock before calling this function
35373537 to guarantee the prompt changes.
35383538
3539+ If a continuation prompt is currently being displayed while entering a multiline
3540+ command, the onscreen prompt will not change. However self.prompt will still be updated
3541+ and display immediately after the multiline line command completes.
3542+
35393543 :param new_prompt: what to change the prompt to
35403544 :raises RuntimeError if called while another thread holds terminal_lock
35413545 """
0 commit comments