summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkmvanbrunt <kmvanbrunt@gmail.com>2018-10-12 12:26:04 -0400
committerGitHub <noreply@github.com>2018-10-12 12:26:04 -0400
commitb216987165d7783903e02db006cf5055f2615796 (patch)
tree0851e964a9d715ebf5ad321f0212b83d6b8f7712
parent11bc029a20168312eaa999e62b45989b6b1d11ef (diff)
parentfbf2a224fcf0e47102c4fcd3ce29c51228b7d931 (diff)
downloadcmd2-git-b216987165d7783903e02db006cf5055f2615796.tar.gz
Merge pull request #575 from python-cmd2/prompt_update
Do not asynchronously redraw continuation prompts
-rw-r--r--.gitignore3
-rw-r--r--CHANGELOG.md4
-rw-r--r--CONTRIBUTING.md1
-rwxr-xr-xREADME.md8
-rw-r--r--cmd2/cmd2.py146
-rw-r--r--docs/install.rst4
-rwxr-xr-xsetup.py4
7 files changed, 86 insertions, 84 deletions
diff --git a/.gitignore b/.gitignore
index 8acd2d4b..9954fca6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,6 @@ dmypy.sock
# cmd2 history file used in main.py
cmd2_history.txt
+
+# Virtual environment
+venv
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dee53e86..d51baadd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.6 (TBD)
+* Enhancements
+ * All platforms now depend on [wcwidth](https://pypi.python.org/pypi/wcwidth) to assist with asynchronous alerts.
+
## 0.9.5 (October 11, 2018)
* Bug Fixes
* Fixed bug where ``get_all_commands`` could return non-callable attributes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3efce4d3..d2f71e6b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -50,6 +50,7 @@ The tables below list all prerequisites along with the minimum required version
| [attrs](https://github.com/python-attrs/attrs) | `16.3` |
| [colorama](https://github.com/tartley/colorama) | `0.3.7` |
| [pyperclip](https://github.com/asweigart/pyperclip) | `1.5.27` |
+| [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.1.7` |
#### Additional prerequisites to run cmd2 unit tests
diff --git a/README.md b/README.md
index efb9f122..e54a3be8 100755
--- a/README.md
+++ b/README.md
@@ -58,13 +58,7 @@ On all operating systems, the latest stable version of `cmd2` can be installed u
pip install -U cmd2
```
-cmd2 works with Python 3.4+ on Windows, macOS, and Linux. It is pure Python code with
-the only 3rd-party dependencies being on [attrs](https://github.com/python-attrs/attrs),
-[colorama](https://github.com/tartley/colorama), and [pyperclip](https://github.com/asweigart/pyperclip).
-Windows has an additional dependency on [pyreadline](https://pypi.python.org/pypi/pyreadline). Non-Windows platforms
-have an additional dependency on [wcwidth](https://pypi.python.org/pypi/wcwidth). Finally, Python
-3.4 has additional dependencies on [contextlib2](https://pypi.python.org/pypi/contextlib2) and the
-[typing](https://pypi.org/project/typing/) backport.
+cmd2 works with Python 3.4+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party dependencies.
For information on other installation options, see
[Installation Instructions](https://cmd2.readthedocs.io/en/latest/install.html) in the cmd2
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 02803b06..e09f428d 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -32,8 +32,6 @@ Git repository on GitHub at https://github.com/python-cmd2/cmd2
import argparse
import cmd
import collections
-import colorama
-from colorama import Fore
import glob
import inspect
import os
@@ -43,15 +41,20 @@ import sys
import threading
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union, IO
+import colorama
+from colorama import Fore
+from wcwidth import wcswidth
+
from . import constants
-from . import utils
from . import plugin
+from . import utils
from .argparse_completer import AutoCompleter, ACArgumentParser, ACTION_ARG_CHOICES
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .parsing import StatementParser, Statement, Macro, MacroArg
# Set up readline
from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt
+
if rl_type == RlType.NONE: # pragma: no cover
rl_warning = "Readline features including tab completion have been disabled since no \n" \
"supported version of readline was found. To resolve this, install \n" \
@@ -71,9 +74,6 @@ else:
elif rl_type == RlType.GNU:
- # We need wcswidth to calculate display width of tab completions
- from wcwidth import wcswidth
-
# Get the readline lib so we can make changes to it
import ctypes
from .rl_utils import readline_lib
@@ -457,6 +457,9 @@ class Cmd(cmd.Cmd):
# Used to keep track of whether we are redirecting or piping output
self.redirecting = False
+ # Used to keep track of whether a continuation prompt is being displayed
+ self.at_continuation_prompt = False
+
# If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing
self.broken_pipe_warning = ''
@@ -1845,6 +1848,7 @@ class Cmd(cmd.Cmd):
# - a multiline command with unclosed quotation marks
if not self.quit_on_sigint:
try:
+ self.at_continuation_prompt = True
newline = self.pseudo_raw_input(self.continuation_prompt)
if newline == 'eof':
# they entered either a blank line, or we hit an EOF
@@ -1858,8 +1862,13 @@ class Cmd(cmd.Cmd):
self.poutput('^C')
statement = self.statement_parser.parse('')
break
+ finally:
+ self.at_continuation_prompt = False
else:
+ self.at_continuation_prompt = True
newline = self.pseudo_raw_input(self.continuation_prompt)
+ self.at_continuation_prompt = False
+
if newline == 'eof':
# they entered either a blank line, or we hit an EOF
# for some other reason. Turn the literal 'eof'
@@ -2074,11 +2083,6 @@ class Cmd(cmd.Cmd):
- if input is a pipe (instead of a tty), look at self.echo
to decide whether to print the prompt and the input
"""
-
- # Temporarily save over self.prompt to reflect what will be on screen
- orig_prompt = self.prompt
- self.prompt = prompt
-
if self.use_rawinput:
try:
if sys.stdin.isatty():
@@ -2122,9 +2126,6 @@ class Cmd(cmd.Cmd):
else:
line = 'eof'
- # Restore prompt
- self.prompt = orig_prompt
-
return line.strip()
def _cmdloop(self) -> bool:
@@ -3435,50 +3436,6 @@ a..b, a:b, a:, ..b items by indices (inclusive)
runner = unittest.TextTestRunner()
runner.run(testcase)
- def _clear_input_lines_str(self) -> str: # pragma: no cover
- """
- Returns a string that if printed will clear the prompt and input lines in the terminal,
- leaving the cursor at the beginning of the first input line
- :return: the string to print
- """
- if not (vt100_support and self.use_rawinput):
- return ''
-
- import shutil
- import colorama.ansi as ansi
- from colorama import Cursor
-
- visible_prompt = self.visible_prompt
-
- # Get the size of the terminal
- terminal_size = shutil.get_terminal_size()
-
- # Figure out how many lines the prompt and user input take up
- total_str_size = len(visible_prompt) + len(readline.get_line_buffer())
- num_input_lines = int(total_str_size / terminal_size.columns) + 1
-
- # Get the cursor's offset from the beginning of the first input line
- cursor_input_offset = len(visible_prompt) + rl_get_point()
-
- # Calculate what input line the cursor is on
- cursor_input_line = int(cursor_input_offset / terminal_size.columns) + 1
-
- # Create a string that will clear all input lines and print the alert
- terminal_str = ''
-
- # Move the cursor down to the last input line
- if cursor_input_line != num_input_lines:
- terminal_str += Cursor.DOWN(num_input_lines - cursor_input_line)
-
- # Clear each input line from the bottom up so that the cursor ends up on the original first input line
- terminal_str += (ansi.clear_line() + Cursor.UP(1)) * (num_input_lines - 1)
- terminal_str += ansi.clear_line()
-
- # Move the cursor to the beginning of the first input line
- terminal_str += '\r'
-
- return terminal_str
-
def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover
"""
Display an important message to the user while they are at the prompt in between commands.
@@ -3497,27 +3454,70 @@ a..b, a:b, a:, ..b items by indices (inclusive)
if not (vt100_support and self.use_rawinput):
return
+ import shutil
+ import colorama.ansi as ansi
+ from colorama import Cursor
+
# Sanity check that can't fail if self.terminal_lock was acquired before calling this function
if self.terminal_lock.acquire(blocking=False):
- # Generate a string to clear the prompt and input lines and replace with the alert
- terminal_str = self._clear_input_lines_str()
+ # Figure out what prompt is displaying
+ current_prompt = self.continuation_prompt if self.at_continuation_prompt else self.prompt
+
+ # Only update terminal if there are changes
+ update_terminal = False
+
if alert_msg:
- terminal_str += alert_msg + '\n'
+ alert_msg += '\n'
+ update_terminal = True
- # Set the new prompt now that _clear_input_lines_str is done using the old prompt
- if new_prompt is not None:
+ # Set the prompt if its changed
+ if new_prompt is not None and new_prompt != self.prompt:
self.prompt = new_prompt
- rl_set_prompt(self.prompt)
- # Print terminal_str to erase the lines
- if rl_type == RlType.GNU:
- sys.stderr.write(terminal_str)
- elif rl_type == RlType.PYREADLINE:
- readline.rl.mode.console.write(terminal_str)
+ # If we aren't at a continuation prompt, then redraw the prompt now
+ if not self.at_continuation_prompt:
+ rl_set_prompt(self.prompt)
+ update_terminal = True
- # Redraw the prompt and input lines
- rl_force_redisplay()
+ if update_terminal:
+ # Remove ansi characters to get the visible width of the prompt
+ prompt_width = wcswidth(utils.strip_ansi(current_prompt))
+
+ # Get the size of the terminal
+ terminal_size = shutil.get_terminal_size()
+
+ # Figure out how many lines the prompt and user input take up
+ total_str_size = prompt_width + wcswidth(readline.get_line_buffer())
+ num_input_lines = int(total_str_size / terminal_size.columns) + 1
+
+ # Get the cursor's offset from the beginning of the first input line
+ cursor_input_offset = prompt_width + rl_get_point()
+
+ # Calculate what input line the cursor is on
+ cursor_input_line = int(cursor_input_offset / terminal_size.columns) + 1
+
+ # Create a string that when printed will clear all input lines and display the alert
+ terminal_str = ''
+
+ # Move the cursor down to the last input line
+ if cursor_input_line != num_input_lines:
+ terminal_str += Cursor.DOWN(num_input_lines - cursor_input_line)
+
+ # Clear each input line from the bottom up so that the cursor ends up on the original first input line
+ terminal_str += (ansi.clear_line() + Cursor.UP(1)) * (num_input_lines - 1)
+ terminal_str += ansi.clear_line()
+
+ # Move the cursor to the beginning of the first input line and print the alert
+ terminal_str += '\r' + alert_msg
+
+ if rl_type == RlType.GNU:
+ sys.stderr.write(terminal_str)
+ elif rl_type == RlType.PYREADLINE:
+ readline.rl.mode.console.write(terminal_str)
+
+ # Redraw the prompt and input lines
+ rl_force_redisplay()
self.terminal_lock.release()
@@ -3536,6 +3536,10 @@ a..b, a:b, a:, ..b items by indices (inclusive)
a prompt is onscreen. Therefore it is best to acquire the lock before calling this function
to guarantee the prompt changes.
+ If a continuation prompt is currently being displayed while entering a multiline
+ command, the onscreen prompt will not change. However self.prompt will still be updated
+ and display immediately after the multiline line command completes.
+
:param new_prompt: what to change the prompt to
:raises RuntimeError if called while another thread holds terminal_lock
"""
diff --git a/docs/install.rst b/docs/install.rst
index 3578ce25..b0710916 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -101,14 +101,12 @@ the following Python packages are installed:
* attrs
* colorama
* pyperclip
+ * wcwidth
On Windows, there is an additional dependency:
* pyreadline
-On macOS or Linux, there is an additional dependency:
- * wcwidth
-
Upgrading cmd2
--------------
diff --git a/setup.py b/setup.py
index 861813c7..607bc346 100755
--- a/setup.py
+++ b/setup.py
@@ -66,13 +66,11 @@ Topic :: Software Development :: Libraries :: Python Modules
SETUP_REQUIRES = ['setuptools_scm']
-INSTALL_REQUIRES = ['pyperclip >= 1.5.27', 'colorama', 'attrs >= 16.3.0']
+INSTALL_REQUIRES = ['pyperclip >= 1.5.27', 'colorama', 'attrs >= 16.3.0', 'wcwidth >= 0.1.7']
EXTRAS_REQUIRE = {
# Windows also requires pyreadline to ensure tab completion works
":sys_platform=='win32'": ['pyreadline'],
- # POSIX OSes also require wcwidth for correctly estimating the displayed width of unicode chars
- ":sys_platform!='win32'": ['wcwidth'],
# Python 3.4 and earlier require contextlib2 for temporarily redirecting stderr and stdout
":python_version<'3.5'": ['contextlib2', 'typing'],
# Extra dependencies for running unit tests