32
32
import argparse
33
33
import cmd
34
34
import collections
35
- import colorama
36
- from colorama import Fore
37
35
import glob
38
36
import inspect
39
37
import os
43
41
import threading
44
42
from typing import Any , Callable , Dict , List , Mapping , Optional , Tuple , Type , Union , IO
45
43
44
+ import colorama
45
+ from colorama import Fore
46
+ from wcwidth import wcswidth
47
+
46
48
from . import constants
47
- from . import utils
48
49
from . import plugin
50
+ from . import utils
49
51
from .argparse_completer import AutoCompleter , ACArgumentParser , ACTION_ARG_CHOICES
50
52
from .clipboard import can_clip , get_paste_buffer , write_to_paste_buffer
51
53
from .parsing import StatementParser , Statement , Macro , MacroArg
52
54
53
55
# Set up readline
54
56
from .rl_utils import rl_type , RlType , rl_get_point , rl_set_prompt , vt100_support , rl_make_safe_prompt
57
+
55
58
if rl_type == RlType .NONE : # pragma: no cover
56
59
rl_warning = "Readline features including tab completion have been disabled since no \n " \
57
60
"supported version of readline was found. To resolve this, install \n " \
71
74
72
75
elif rl_type == RlType .GNU :
73
76
74
- # We need wcswidth to calculate display width of tab completions
75
- from wcwidth import wcswidth
76
-
77
77
# Get the readline lib so we can make changes to it
78
78
import ctypes
79
79
from .rl_utils import readline_lib
@@ -457,6 +457,9 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
457
457
# Used to keep track of whether we are redirecting or piping output
458
458
self .redirecting = False
459
459
460
+ # Used to keep track of whether a continuation prompt is being displayed
461
+ self .at_continuation_prompt = False
462
+
460
463
# If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing
461
464
self .broken_pipe_warning = ''
462
465
@@ -1845,6 +1848,7 @@ def _complete_statement(self, line: str) -> Statement:
1845
1848
# - a multiline command with unclosed quotation marks
1846
1849
if not self .quit_on_sigint :
1847
1850
try :
1851
+ self .at_continuation_prompt = True
1848
1852
newline = self .pseudo_raw_input (self .continuation_prompt )
1849
1853
if newline == 'eof' :
1850
1854
# they entered either a blank line, or we hit an EOF
@@ -1858,8 +1862,13 @@ def _complete_statement(self, line: str) -> Statement:
1858
1862
self .poutput ('^C' )
1859
1863
statement = self .statement_parser .parse ('' )
1860
1864
break
1865
+ finally :
1866
+ self .at_continuation_prompt = False
1861
1867
else :
1868
+ self .at_continuation_prompt = True
1862
1869
newline = self .pseudo_raw_input (self .continuation_prompt )
1870
+ self .at_continuation_prompt = False
1871
+
1863
1872
if newline == 'eof' :
1864
1873
# they entered either a blank line, or we hit an EOF
1865
1874
# for some other reason. Turn the literal 'eof'
@@ -2074,11 +2083,6 @@ def pseudo_raw_input(self, prompt: str) -> str:
2074
2083
- if input is a pipe (instead of a tty), look at self.echo
2075
2084
to decide whether to print the prompt and the input
2076
2085
"""
2077
-
2078
- # Temporarily save over self.prompt to reflect what will be on screen
2079
- orig_prompt = self .prompt
2080
- self .prompt = prompt
2081
-
2082
2086
if self .use_rawinput :
2083
2087
try :
2084
2088
if sys .stdin .isatty ():
@@ -2122,9 +2126,6 @@ def pseudo_raw_input(self, prompt: str) -> str:
2122
2126
else :
2123
2127
line = 'eof'
2124
2128
2125
- # Restore prompt
2126
- self .prompt = orig_prompt
2127
-
2128
2129
return line .strip ()
2129
2130
2130
2131
def _cmdloop (self ) -> bool :
@@ -3435,50 +3436,6 @@ class TestMyAppCase(Cmd2TestCase):
3435
3436
runner = unittest .TextTestRunner ()
3436
3437
runner .run (testcase )
3437
3438
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
-
3482
3439
def async_alert (self , alert_msg : str , new_prompt : Optional [str ] = None ) -> None : # pragma: no cover
3483
3440
"""
3484
3441
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:
3497
3454
if not (vt100_support and self .use_rawinput ):
3498
3455
return
3499
3456
3457
+ import shutil
3458
+ import colorama .ansi as ansi
3459
+ from colorama import Cursor
3460
+
3500
3461
# Sanity check that can't fail if self.terminal_lock was acquired before calling this function
3501
3462
if self .terminal_lock .acquire (blocking = False ):
3502
3463
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
+
3505
3470
if alert_msg :
3506
- terminal_str += alert_msg + '\n '
3471
+ alert_msg += '\n '
3472
+ update_terminal = True
3507
3473
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 :
3510
3476
self .prompt = new_prompt
3511
- rl_set_prompt (self .prompt )
3512
3477
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
3518
3482
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 ()
3521
3521
3522
3522
self .terminal_lock .release ()
3523
3523
@@ -3536,6 +3536,10 @@ def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
3536
3536
a prompt is onscreen. Therefore it is best to acquire the lock before calling this function
3537
3537
to guarantee the prompt changes.
3538
3538
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
+
3539
3543
:param new_prompt: what to change the prompt to
3540
3544
:raises RuntimeError if called while another thread holds terminal_lock
3541
3545
"""
0 commit comments