summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/ansi.py74
-rw-r--r--cmd2/cmd2.py58
-rw-r--r--tests/test_ansi.py38
3 files changed, 103 insertions, 67 deletions
diff --git a/cmd2/ansi.py b/cmd2/ansi.py
index 035991bb..59dcb43c 100644
--- a/cmd2/ansi.py
+++ b/cmd2/ansi.py
@@ -8,6 +8,9 @@ import colorama
from colorama import Fore, Back, Style
from wcwidth import wcswidth
+# On Windows, filter ANSI escape codes out of text sent to stdout/stderr, and replace them with equivalent Win32 calls
+colorama.init(strip=False)
+
# Values for allow_ansi setting
ANSI_NEVER = 'Never'
ANSI_TERMINAL = 'Terminal'
@@ -65,6 +68,9 @@ FG_RESET = FG_COLORS['reset']
BG_RESET = BG_COLORS['reset']
RESET_ALL = Style.RESET_ALL
+BRIGHT = Style.BRIGHT
+NORMAL = Style.NORMAL
+
# ANSI escape sequences not provided by colorama
UNDERLINE_ENABLE = colorama.ansi.code_to_chars(4)
UNDERLINE_DISABLE = colorama.ansi.code_to_chars(24)
@@ -180,3 +186,71 @@ def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underlin
style_success = functools.partial(style, fg='green', bold=True)
style_warning = functools.partial(style, fg='bright_yellow')
style_error = functools.partial(style, fg='bright_red')
+
+
+def async_alert_str(*, prompt: str, line: str, alert_msg: str) -> str:
+ """Calculate the desired string, including ANSI escape codes, for displaying an asynchronous alert message.
+
+ :param prompt: prompt that is displayed on the current line
+ :param line: current contents of the Readline line buffer
+ :param alert_msg: the message to display to the user
+ :return: the correct string so that the alert message appears to the user to be printed above the current line.
+ """
+ import shutil
+ from colorama import Cursor
+
+ from .rl_utils import rl_get_point
+
+ # Get the size of the terminal
+ terminal_size = shutil.get_terminal_size()
+
+ # Split the prompt lines since it can contain newline characters.
+ prompt_lines = prompt.splitlines()
+
+ # Calculate how many terminal lines are taken up by all prompt lines except for the last one.
+ # That will be included in the input lines calculations since that is where the cursor is.
+ num_prompt_terminal_lines = 0
+ for line in prompt_lines[:-1]:
+ line_width = ansi_safe_wcswidth(line)
+ num_prompt_terminal_lines += int(line_width / terminal_size.columns) + 1
+
+ # Now calculate how many terminal lines are take up by the input
+ last_prompt_line = prompt_lines[-1]
+ last_prompt_line_width = ansi_safe_wcswidth(last_prompt_line)
+
+ input_width = last_prompt_line_width + ansi_safe_wcswidth(line)
+
+ num_input_terminal_lines = int(input_width / terminal_size.columns) + 1
+
+ # Get the cursor's offset from the beginning of the first input line
+ cursor_input_offset = last_prompt_line_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_terminal_lines:
+ terminal_str += Cursor.DOWN(num_input_terminal_lines - cursor_input_line)
+
+ # Clear each line from the bottom up so that the cursor ends up on the first prompt line
+ total_lines = num_prompt_terminal_lines + num_input_terminal_lines
+ terminal_str += (colorama.ansi.clear_line() + Cursor.UP(1)) * (total_lines - 1)
+
+ # Clear the first prompt line
+ terminal_str += colorama.ansi.clear_line()
+
+ # Move the cursor to the beginning of the first prompt line and print the alert
+ terminal_str += '\r' + alert_msg
+ return terminal_str
+
+
+def set_title_str(title: str) -> str:
+ """Get the required string, including ANSI escape codes, for setting window title for the terminal.
+
+ :param title: new title for the window
+ :return string to write to sys.stderr in order to set the window title to the desired test
+ """
+ return colorama.ansi.set_title(title)
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index d41a631d..77af3910 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -42,8 +42,6 @@ from collections import namedtuple
from contextlib import redirect_stdout
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union
-import colorama
-
from . import ansi
from . import constants
from . import plugin
@@ -54,7 +52,7 @@ from .history import History, HistoryItem
from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split
# Set up readline
-from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt
+from .rl_utils import rl_type, RlType, 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" \
@@ -359,9 +357,6 @@ class Cmd(cmd.Cmd):
except AttributeError:
pass
- # Override whether ansi codes should be stripped from the output since cmd2 has its own logic for doing this
- colorama.init(strip=False)
-
# initialize plugin system
# needs to be done before we call __init__(0)
self._initialize_plugin_system()
@@ -3782,9 +3777,6 @@ class Cmd(cmd.Cmd):
if not (vt100_support and self.use_rawinput):
return
- import shutil
- 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):
@@ -3808,50 +3800,8 @@ class Cmd(cmd.Cmd):
update_terminal = True
if update_terminal:
- # Get the size of the terminal
- terminal_size = shutil.get_terminal_size()
-
- # Split the prompt lines since it can contain newline characters.
- prompt_lines = current_prompt.splitlines()
-
- # Calculate how many terminal lines are taken up by all prompt lines except for the last one.
- # That will be included in the input lines calculations since that is where the cursor is.
- num_prompt_terminal_lines = 0
- for line in prompt_lines[:-1]:
- line_width = ansi.ansi_safe_wcswidth(line)
- num_prompt_terminal_lines += int(line_width / terminal_size.columns) + 1
-
- # Now calculate how many terminal lines are take up by the input
- last_prompt_line = prompt_lines[-1]
- last_prompt_line_width = ansi.ansi_safe_wcswidth(last_prompt_line)
-
- input_width = last_prompt_line_width + ansi.ansi_safe_wcswidth(readline.get_line_buffer())
-
- num_input_terminal_lines = int(input_width / terminal_size.columns) + 1
-
- # Get the cursor's offset from the beginning of the first input line
- cursor_input_offset = last_prompt_line_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_terminal_lines:
- terminal_str += Cursor.DOWN(num_input_terminal_lines - cursor_input_line)
-
- # Clear each line from the bottom up so that the cursor ends up on the first prompt line
- total_lines = num_prompt_terminal_lines + num_input_terminal_lines
- terminal_str += (colorama.ansi.clear_line() + Cursor.UP(1)) * (total_lines - 1)
-
- # Clear the first prompt line
- terminal_str += colorama.ansi.clear_line()
-
- # Move the cursor to the beginning of the first prompt line and print the alert
- terminal_str += '\r' + alert_msg
-
+ terminal_str = ansi.async_alert_str(prompt=current_prompt, line=readline.get_line_buffer(),
+ alert_msg=alert_msg)
if rl_type == RlType.GNU:
sys.stderr.write(terminal_str)
elif rl_type == RlType.PYREADLINE:
@@ -3904,7 +3854,7 @@ class Cmd(cmd.Cmd):
# Sanity check that can't fail if self.terminal_lock was acquired before calling this function
if self.terminal_lock.acquire(blocking=False):
try:
- sys.stderr.write(colorama.ansi.set_title(title))
+ sys.stderr.write(ansi.set_title_str(title))
except AttributeError:
# Debugging in Pycharm has issues with setting terminal title
pass
diff --git a/tests/test_ansi.py b/tests/test_ansi.py
index bf628ef0..53c917d5 100644
--- a/tests/test_ansi.py
+++ b/tests/test_ansi.py
@@ -4,7 +4,6 @@
Unit testing for cmd2/ansi.py module
"""
import pytest
-from colorama import Fore, Back, Style
import cmd2.ansi as ansi
@@ -13,14 +12,14 @@ HELLO_WORLD = 'Hello, world!'
def test_strip_ansi():
base_str = HELLO_WORLD
- ansi_str = Fore.GREEN + base_str + Fore.RESET
+ ansi_str = ansi.style(base_str, fg='green')
assert base_str != ansi_str
assert base_str == ansi.strip_ansi(ansi_str)
def test_ansi_safe_wcswidth():
base_str = HELLO_WORLD
- ansi_str = Fore.GREEN + base_str + Fore.RESET
+ ansi_str = ansi.style(base_str, fg='green')
assert ansi.ansi_safe_wcswidth(ansi_str) != len(ansi_str)
@@ -32,19 +31,21 @@ def test_style_none():
def test_style_fg():
base_str = HELLO_WORLD
- ansi_str = Fore.BLUE + base_str + Fore.RESET
- assert ansi.style(base_str, fg='blue') == ansi_str
+ fg_color = 'blue'
+ ansi_str = ansi.FG_COLORS[fg_color] + base_str + ansi.FG_RESET
+ assert ansi.style(base_str, fg=fg_color) == ansi_str
def test_style_bg():
base_str = HELLO_WORLD
- ansi_str = Back.GREEN + base_str + Back.RESET
- assert ansi.style(base_str, bg='green') == ansi_str
+ bg_color = 'green'
+ ansi_str = ansi.BG_COLORS[bg_color] + base_str + ansi.BG_RESET
+ assert ansi.style(base_str, bg=bg_color) == ansi_str
def test_style_bold():
base_str = HELLO_WORLD
- ansi_str = Style.BRIGHT + base_str + Style.NORMAL
+ ansi_str = ansi.BRIGHT + base_str + ansi.NORMAL
assert ansi.style(base_str, bold=True) == ansi_str
@@ -56,9 +57,11 @@ def test_style_underline():
def test_style_multi():
base_str = HELLO_WORLD
- ansi_str = Fore.BLUE + Back.GREEN + Style.BRIGHT + ansi.UNDERLINE_ENABLE + \
- base_str + Fore.RESET + Back.RESET + Style.NORMAL + ansi.UNDERLINE_DISABLE
- assert ansi.style(base_str, fg='blue', bg='green', bold=True, underline=True) == ansi_str
+ fg_color = 'blue'
+ bg_color = 'green'
+ ansi_str = ansi.FG_COLORS[fg_color] + ansi.BG_COLORS[bg_color] + ansi.BRIGHT + ansi.UNDERLINE_ENABLE + \
+ base_str + ansi.FG_RESET + ansi.BG_RESET + ansi.NORMAL + ansi.UNDERLINE_DISABLE
+ assert ansi.style(base_str, fg=fg_color, bg=bg_color, bold=True, underline=True) == ansi_str
def test_style_color_not_exist():
@@ -72,7 +75,8 @@ def test_style_color_not_exist():
def test_fg_lookup_exist():
- assert ansi.fg_lookup('green') == Fore.GREEN
+ fg_color = 'green'
+ assert ansi.fg_lookup(fg_color) == ansi.FG_COLORS[fg_color]
def test_fg_lookup_nonexist():
@@ -81,9 +85,17 @@ def test_fg_lookup_nonexist():
def test_bg_lookup_exist():
- assert ansi.bg_lookup('green') == Back.GREEN
+ bg_color = 'green'
+ assert ansi.bg_lookup(bg_color) == ansi.BG_COLORS[bg_color]
def test_bg_lookup_nonexist():
with pytest.raises(ValueError):
ansi.bg_lookup('bar')
+
+
+def test_set_title_str():
+ OSC = '\033]'
+ BEL = '\007'
+ title = HELLO_WORLD
+ assert ansi.set_title_str(title) == OSC + '2;' + title + BEL