Skip to content

Commit d80aeb3

Browse files
committed
Using wcwidth to calculate display width of prompt
1 parent 7ef96d3 commit d80aeb3

File tree

4 files changed

+34
-26
lines changed

4 files changed

+34
-26
lines changed

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

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,10 @@ pip install -U cmd2
6060

6161
cmd2 works with Python 3.4+ on Windows, macOS, and Linux. It is pure Python code with
6262
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.
63+
[colorama](https://github.com/tartley/colorama), [pyperclip](https://github.com/asweigart/pyperclip), and
64+
[wcwidth](https://pypi.python.org/pypi/wcwidth). Windows has an additional dependency on
65+
[pyreadline](https://pypi.python.org/pypi/pyreadline). Finally, Python 3.4 has additional dependencies
66+
on [contextlib2](https://pypi.python.org/pypi/contextlib2) and the [typing](https://pypi.org/project/typing/) backport.
6867

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

cmd2/cmd2.py

Lines changed: 25 additions & 18 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
@@ -3436,11 +3436,12 @@ class TestMyAppCase(Cmd2TestCase):
34363436
runner = unittest.TextTestRunner()
34373437
runner.run(testcase)
34383438

3439-
def _clear_input_lines_str(self) -> str: # pragma: no cover
3439+
def _clear_input_lines_str(self, current_prompt: str) -> str: # pragma: no cover
34403440
"""
34413441
Returns a string that if printed will clear the prompt and input lines in the terminal,
34423442
leaving the cursor at the beginning of the first input line
3443-
:return: the string to print
3443+
3444+
:param current_prompt: the currently displayed prompt
34443445
"""
34453446
if not (vt100_support and self.use_rawinput):
34463447
return ''
@@ -3449,17 +3450,18 @@ def _clear_input_lines_str(self) -> str: # pragma: no cover
34493450
import colorama.ansi as ansi
34503451
from colorama import Cursor
34513452

3452-
visible_prompt = self.visible_prompt
3453+
# Remove ansi characters to get the visible width of the prompt
3454+
prompt_width = wcswidth(utils.strip_ansi(current_prompt))
34533455

34543456
# Get the size of the terminal
34553457
terminal_size = shutil.get_terminal_size()
34563458

34573459
# Figure out how many lines the prompt and user input take up
3458-
total_str_size = len(visible_prompt) + len(readline.get_line_buffer())
3460+
total_str_size = prompt_width + wcswidth(readline.get_line_buffer())
34593461
num_input_lines = int(total_str_size / terminal_size.columns) + 1
34603462

34613463
# Get the cursor's offset from the beginning of the first input line
3462-
cursor_input_offset = len(visible_prompt) + rl_get_point()
3464+
cursor_input_offset = prompt_width + rl_get_point()
34633465

34643466
# Calculate what input line the cursor is on
34653467
cursor_input_line = int(cursor_input_offset / terminal_size.columns) + 1
@@ -3505,16 +3507,20 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None:
35053507
do_update = False
35063508

35073509
# Generate a string to clear the prompt and input lines and replace with the alert
3508-
terminal_str = self._clear_input_lines_str()
3510+
current_prompt = self.continuation_prompt if self.at_continuation_prompt else self.prompt
3511+
terminal_str = self._clear_input_lines_str(current_prompt)
3512+
35093513
if alert_msg:
35103514
terminal_str += alert_msg + '\n'
35113515
do_update = True
35123516

3513-
# Set the new prompt now that _clear_input_lines_str is done using the old prompt
3514-
if new_prompt is not None and not self.at_continuation_prompt:
3517+
# Set the prompt if its changed
3518+
if new_prompt is not None and new_prompt != self.prompt:
35153519
self.prompt = new_prompt
3516-
rl_set_prompt(self.prompt)
3517-
do_update = True
3520+
3521+
if not self.at_continuation_prompt:
3522+
rl_set_prompt(self.prompt)
3523+
do_update = True
35183524

35193525
if do_update:
35203526
# Print terminal_str to erase the lines
@@ -3543,8 +3549,9 @@ def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
35433549
a prompt is onscreen. Therefore it is best to acquire the lock before calling this function
35443550
to guarantee the prompt changes.
35453551
3546-
The prompt will not update if a continuation prompt is being displayed
3547-
while entering a multiline command.
3552+
If a continuation prompt is currently being displayed while entering a multiline
3553+
command, the onscreen prompt will not change. However self.prompt will still be updated
3554+
and display immediately after the multiline line command completes.
35483555
35493556
:param new_prompt: what to change the prompt to
35503557
:raises RuntimeError if called while another thread holds terminal_lock

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']
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)