Skip to content

Commit b216987

Browse files
authored
Merge pull request #575 from python-cmd2/prompt_update
Do not asynchronously redraw continuation prompts
2 parents 11bc029 + fbf2a22 commit b216987

File tree

7 files changed

+86
-84
lines changed

7 files changed

+86
-84
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ dmypy.sock
2828

2929
# cmd2 history file used in main.py
3030
cmd2_history.txt
31+
32+
# Virtual environment
33+
venv

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.6 (TBD)
2+
* Enhancements
3+
* All platforms now depend on [wcwidth](https://pypi.python.org/pypi/wcwidth) to assist with asynchronous alerts.
4+
15
## 0.9.5 (October 11, 2018)
26
* Bug Fixes
37
* Fixed bug where ``get_all_commands`` could return non-callable attributes

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ The tables below list all prerequisites along with the minimum required version
5050
| [attrs](https://github.com/python-attrs/attrs) | `16.3` |
5151
| [colorama](https://github.com/tartley/colorama) | `0.3.7` |
5252
| [pyperclip](https://github.com/asweigart/pyperclip) | `1.5.27` |
53+
| [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.1.7` |
5354

5455

5556
#### Additional prerequisites to run cmd2 unit tests

README.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,7 @@ On all operating systems, the latest stable version of `cmd2` can be installed u
5858
pip install -U cmd2
5959
```
6060

61-
cmd2 works with Python 3.4+ on Windows, macOS, and Linux. It is pure Python code with
62-
the only 3rd-party dependencies being on [attrs](https://github.com/python-attrs/attrs),
63-
[colorama](https://github.com/tartley/colorama), and [pyperclip](https://github.com/asweigart/pyperclip).
64-
Windows has an additional dependency on [pyreadline](https://pypi.python.org/pypi/pyreadline). Non-Windows platforms
65-
have an additional dependency on [wcwidth](https://pypi.python.org/pypi/wcwidth). Finally, Python
66-
3.4 has additional dependencies on [contextlib2](https://pypi.python.org/pypi/contextlib2) and the
67-
[typing](https://pypi.org/project/typing/) backport.
61+
cmd2 works with Python 3.4+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party dependencies.
6862

6963
For information on other installation options, see
7064
[Installation Instructions](https://cmd2.readthedocs.io/en/latest/install.html) in the cmd2

cmd2/cmd2.py

Lines changed: 75 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@
3232
import argparse
3333
import cmd
3434
import collections
35-
import colorama
36-
from colorama import Fore
3735
import glob
3836
import inspect
3937
import os
@@ -43,15 +41,20 @@
4341
import threading
4442
from 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+
4648
from . import constants
47-
from . import utils
4849
from . import plugin
50+
from . import utils
4951
from .argparse_completer import AutoCompleter, ACArgumentParser, ACTION_ARG_CHOICES
5052
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
5153
from .parsing import StatementParser, Statement, Macro, MacroArg
5254

5355
# Set up readline
5456
from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt
57+
5558
if 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" \
@@ -71,9 +74,6 @@
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
"""

docs/install.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,12 @@ the following Python packages are installed:
101101
* attrs
102102
* colorama
103103
* pyperclip
104+
* wcwidth
104105

105106
On Windows, there is an additional dependency:
106107

107108
* pyreadline
108109

109-
On macOS or Linux, there is an additional dependency:
110-
* wcwidth
111-
112110

113111
Upgrading cmd2
114112
--------------

setup.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,11 @@
6666

6767
SETUP_REQUIRES = ['setuptools_scm']
6868

69-
INSTALL_REQUIRES = ['pyperclip >= 1.5.27', 'colorama', 'attrs >= 16.3.0']
69+
INSTALL_REQUIRES = ['pyperclip >= 1.5.27', 'colorama', 'attrs >= 16.3.0', 'wcwidth >= 0.1.7']
7070

7171
EXTRAS_REQUIRE = {
7272
# Windows also requires pyreadline to ensure tab completion works
7373
":sys_platform=='win32'": ['pyreadline'],
74-
# POSIX OSes also require wcwidth for correctly estimating the displayed width of unicode chars
75-
":sys_platform!='win32'": ['wcwidth'],
7674
# Python 3.4 and earlier require contextlib2 for temporarily redirecting stderr and stdout
7775
":python_version<'3.5'": ['contextlib2', 'typing'],
7876
# Extra dependencies for running unit tests

0 commit comments

Comments
 (0)