Skip to content

Commit db093b6

Browse files
authored
Merge pull request #843 from python-cmd2/ansi_to_style
ansi to style
2 parents 97dd6f3 + 383853e commit db093b6

22 files changed

+192
-164
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
* Bug Fixes
33
* Fixed bug where startup script containing a single quote in its file name was incorrectly quoted
44
* Added missing implicit dependency on `setuptools` due to build with `setuptools_scm`
5+
* Enhancements
6+
* Added dim text style support via `style()` function and `ansi.INTENSITY_DIM` setting.
7+
* Breaking changes
8+
* Renamed the following `ansi` members for accuracy in what types of ANSI escape sequences are handled
9+
* `ansi.allow_ansi` -> `ansi.allow_style`
10+
* `ansi.ansi_safe_wcswidth()` -> `ansi.style_aware_wcswidth()`
11+
* `ansi.ansi_aware_write()` -> `ansi.style_aware_write()`
12+
* Renamed the following `ansi` members for clarification
13+
* `ansi.BRIGHT` -> `ansi.INTENSITY_BRIGHT`
14+
* `ansi.NORMAL` -> `ansi.INTENSITY_NORMAL`
515

616
## 0.9.22 (December 9, 2019)
717
* Bug Fixes

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ example/transcript_regex.txt:
317317
# The regex for editor will match whatever program you use.
318318
# regexes on prompts just make the trailing space obvious
319319
(Cmd) set
320-
allow_ansi: Terminal
320+
allow_style: Terminal
321321
continuation_prompt: >/ /
322322
debug: False
323323
echo: False

cmd2/ansi.py

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# coding=utf-8
2-
"""Support for ANSI escape sequences which are used for things like applying style to text"""
2+
"""
3+
Support for ANSI escape sequences which are used for things like applying style to text,
4+
setting the window title, and asynchronous alerts.
5+
"""
36
import functools
47
import re
58
from typing import Any, IO
@@ -11,16 +14,16 @@
1114
# On Windows, filter ANSI escape codes out of text sent to stdout/stderr, and replace them with equivalent Win32 calls
1215
colorama.init(strip=False)
1316

14-
# Values for allow_ansi setting
15-
ANSI_NEVER = 'Never'
16-
ANSI_TERMINAL = 'Terminal'
17-
ANSI_ALWAYS = 'Always'
17+
# Values for allow_style setting
18+
STYLE_NEVER = 'Never'
19+
STYLE_TERMINAL = 'Terminal'
20+
STYLE_ALWAYS = 'Always'
1821

19-
# Controls when ANSI escape sequences are allowed in output
20-
allow_ansi = ANSI_TERMINAL
22+
# Controls when ANSI style style sequences are allowed in output
23+
allow_style = STYLE_TERMINAL
2124

22-
# Regular expression to match ANSI escape sequences
23-
ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m')
25+
# Regular expression to match ANSI style sequences (including 8-bit and 24-bit colors)
26+
ANSI_STYLE_RE = re.compile(r'\x1b\[[^m]*m')
2427

2528
# Foreground color presets
2629
FG_COLORS = {
@@ -68,50 +71,50 @@
6871
BG_RESET = BG_COLORS['reset']
6972
RESET_ALL = Style.RESET_ALL
7073

71-
BRIGHT = Style.BRIGHT
72-
NORMAL = Style.NORMAL
74+
# Text intensities
75+
INTENSITY_BRIGHT = Style.BRIGHT
76+
INTENSITY_DIM = Style.DIM
77+
INTENSITY_NORMAL = Style.NORMAL
7378

74-
# ANSI escape sequences not provided by colorama
79+
# ANSI style sequences not provided by colorama
7580
UNDERLINE_ENABLE = colorama.ansi.code_to_chars(4)
7681
UNDERLINE_DISABLE = colorama.ansi.code_to_chars(24)
7782

7883

79-
def strip_ansi(text: str) -> str:
84+
def strip_style(text: str) -> str:
8085
"""
81-
Strip ANSI escape sequences from a string.
86+
Strip ANSI style sequences from a string.
8287
83-
:param text: string which may contain ANSI escape sequences
84-
:return: the same string with any ANSI escape sequences removed
88+
:param text: string which may contain ANSI style sequences
89+
:return: the same string with any ANSI style sequences removed
8590
"""
86-
return ANSI_ESCAPE_RE.sub('', text)
91+
return ANSI_STYLE_RE.sub('', text)
8792

8893

89-
def ansi_safe_wcswidth(text: str) -> int:
94+
def style_aware_wcswidth(text: str) -> int:
9095
"""
91-
Wrap wcswidth to make it compatible with strings that contains ANSI escape sequences
92-
96+
Wrap wcswidth to make it compatible with strings that contains ANSI style sequences
9397
:param text: the string being measured
9498
"""
95-
# Strip ANSI escape sequences since they cause wcswidth to return -1
96-
return wcswidth(strip_ansi(text))
99+
# Strip ANSI style sequences since they cause wcswidth to return -1
100+
return wcswidth(strip_style(text))
97101

98102

99-
def ansi_aware_write(fileobj: IO, msg: str) -> None:
103+
def style_aware_write(fileobj: IO, msg: str) -> None:
100104
"""
101-
Write a string to a fileobject and strip its ANSI escape sequences if required by allow_ansi setting
102-
105+
Write a string to a fileobject and strip its ANSI style sequences if required by allow_style setting
103106
:param fileobj: the file object being written to
104107
:param msg: the string being written
105108
"""
106-
if allow_ansi.lower() == ANSI_NEVER.lower() or \
107-
(allow_ansi.lower() == ANSI_TERMINAL.lower() and not fileobj.isatty()):
108-
msg = strip_ansi(msg)
109+
if allow_style.lower() == STYLE_NEVER.lower() or \
110+
(allow_style.lower() == STYLE_TERMINAL.lower() and not fileobj.isatty()):
111+
msg = strip_style(msg)
109112
fileobj.write(msg)
110113

111114

112115
def fg_lookup(fg_name: str) -> str:
113-
"""Look up ANSI escape codes based on foreground color name.
114-
116+
"""
117+
Look up ANSI escape codes based on foreground color name.
115118
:param fg_name: foreground color name to look up ANSI escape code(s) for
116119
:return: ANSI escape code(s) associated with this color
117120
:raises ValueError if the color cannot be found
@@ -124,8 +127,8 @@ def fg_lookup(fg_name: str) -> str:
124127

125128

126129
def bg_lookup(bg_name: str) -> str:
127-
"""Look up ANSI escape codes based on background color name.
128-
130+
"""
131+
Look up ANSI escape codes based on background color name.
129132
:param bg_name: background color name to look up ANSI escape code(s) for
130133
:return: ANSI escape code(s) associated with this color
131134
:raises ValueError if the color cannot be found
@@ -137,16 +140,18 @@ def bg_lookup(bg_name: str) -> str:
137140
return ansi_escape
138141

139142

140-
def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underline: bool = False) -> str:
141-
"""Styles a string with ANSI colors and/or styles and returns the new string.
142-
143+
def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False,
144+
dim: bool = False, underline: bool = False) -> str:
145+
"""
146+
Apply ANSI colors and/or styles to a string and return it.
143147
The styling is self contained which means that at the end of the string reset code(s) are issued
144148
to undo whatever styling was done at the beginning.
145149
146150
:param text: Any object compatible with str.format()
147151
:param fg: foreground color. Relies on `fg_lookup()` to retrieve ANSI escape based on name. Defaults to no color.
148152
:param bg: background color. Relies on `bg_lookup()` to retrieve ANSI escape based on name. Defaults to no color.
149-
:param bold: apply the bold style if True. Defaults to False.
153+
:param bold: apply the bold style if True. Can be combined with dim. Defaults to False.
154+
:param dim: apply the dim style if True. Can be combined with bold. Defaults to False.
150155
:param underline: apply the underline style if True. Defaults to False.
151156
:return: the stylized string
152157
"""
@@ -169,14 +174,18 @@ def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underlin
169174
removals.append(BG_RESET)
170175

171176
if bold:
172-
additions.append(Style.BRIGHT)
173-
removals.append(Style.NORMAL)
177+
additions.append(INTENSITY_BRIGHT)
178+
removals.append(INTENSITY_NORMAL)
179+
180+
if dim:
181+
additions.append(INTENSITY_DIM)
182+
removals.append(INTENSITY_NORMAL)
174183

175184
if underline:
176185
additions.append(UNDERLINE_ENABLE)
177186
removals.append(UNDERLINE_DISABLE)
178187

179-
# Combine the ANSI escape sequences with the text
188+
# Combine the ANSI style sequences with the text
180189
return "".join(additions) + text + "".join(removals)
181190

182191

@@ -206,14 +215,14 @@ def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_off
206215
# That will be included in the input lines calculations since that is where the cursor is.
207216
num_prompt_terminal_lines = 0
208217
for line in prompt_lines[:-1]:
209-
line_width = ansi_safe_wcswidth(line)
218+
line_width = style_aware_wcswidth(line)
210219
num_prompt_terminal_lines += int(line_width / terminal_columns) + 1
211220

212221
# Now calculate how many terminal lines are take up by the input
213222
last_prompt_line = prompt_lines[-1]
214-
last_prompt_line_width = ansi_safe_wcswidth(last_prompt_line)
223+
last_prompt_line_width = style_aware_wcswidth(last_prompt_line)
215224

216-
input_width = last_prompt_line_width + ansi_safe_wcswidth(line)
225+
input_width = last_prompt_line_width + style_aware_wcswidth(line)
217226

218227
num_input_terminal_lines = int(input_width / terminal_columns) + 1
219228

cmd2/argparse_completer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,11 +444,11 @@ def _format_completions(self, action, completions: List[Union[str, CompletionIte
444444
completions.sort(key=self._cmd2_app.default_sort_key)
445445
self._cmd2_app.matches_sorted = True
446446

447-
token_width = ansi.ansi_safe_wcswidth(action.dest)
447+
token_width = ansi.style_aware_wcswidth(action.dest)
448448
completions_with_desc = []
449449

450450
for item in completions:
451-
item_width = ansi.ansi_safe_wcswidth(item)
451+
item_width = ansi.style_aware_wcswidth(item)
452452
if item_width > token_width:
453453
token_width = item_width
454454

@@ -585,7 +585,7 @@ def _complete_for_arg(self, arg_action: argparse.Action,
585585
def _print_message(msg: str) -> None:
586586
"""Print a message instead of tab completions and redraw the prompt and input line"""
587587
import sys
588-
ansi.ansi_aware_write(sys.stdout, msg + '\n')
588+
ansi.style_aware_write(sys.stdout, msg + '\n')
589589
rl_force_redisplay()
590590

591591
def _print_arg_hint(self, arg_action: argparse.Action) -> None:

cmd2/argparse_custom.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -802,11 +802,11 @@ def format_help(self) -> str:
802802
return formatter.format_help() + '\n'
803803

804804
def _print_message(self, message, file=None):
805-
# Override _print_message to use ansi_aware_write() since we use ANSI escape characters to support color
805+
# Override _print_message to use style_aware_write() since we use ANSI escape characters to support color
806806
if message:
807807
if file is None:
808808
file = sys.stderr
809-
ansi.ansi_aware_write(file, message)
809+
ansi.style_aware_write(file, message)
810810

811811

812812
# The default ArgumentParser class for a cmd2 app

cmd2/cmd2.py

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,11 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
206206
# To make an attribute settable with the "do_set" command, add it to this ...
207207
self.settable = \
208208
{
209-
# allow_ansi is a special case in which it's an application-wide setting defined in ansi.py
210-
'allow_ansi': ('Allow ANSI escape sequences in output '
211-
'(valid values: {}, {}, {})'.format(ansi.ANSI_TERMINAL,
212-
ansi.ANSI_ALWAYS,
213-
ansi.ANSI_NEVER)),
209+
# allow_style is a special case in which it's an application-wide setting defined in ansi.py
210+
'allow_style': ('Allow ANSI text style sequences in output '
211+
'(valid values: {}, {}, {})'.format(ansi.STYLE_TERMINAL,
212+
ansi.STYLE_ALWAYS,
213+
ansi.STYLE_NEVER)),
214214
'continuation_prompt': 'On 2nd+ line of input',
215215
'debug': 'Show full error stack on error',
216216
'echo': 'Echo command issued into output',
@@ -366,7 +366,7 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
366366
else:
367367
# Here is the meaning of the various flags we are using with the less command:
368368
# -S causes lines longer than the screen width to be chopped (truncated) rather than wrapped
369-
# -R causes ANSI "color" escape sequences to be output in raw form (i.e. colors are displayed)
369+
# -R causes ANSI "style" escape sequences to be output in raw form (i.e. colors are displayed)
370370
# -X disables sending the termcap initialization and deinitialization strings to the terminal
371371
# -F causes less to automatically exit if the entire file can be displayed on the first screen
372372
self.pager = 'less -RXF'
@@ -395,38 +395,38 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
395395
# ----- Methods related to presenting output to the user -----
396396

397397
@property
398-
def allow_ansi(self) -> str:
399-
"""Read-only property needed to support do_set when it reads allow_ansi"""
400-
return ansi.allow_ansi
398+
def allow_style(self) -> str:
399+
"""Read-only property needed to support do_set when it reads allow_style"""
400+
return ansi.allow_style
401401

402-
@allow_ansi.setter
403-
def allow_ansi(self, new_val: str) -> None:
404-
"""Setter property needed to support do_set when it updates allow_ansi"""
402+
@allow_style.setter
403+
def allow_style(self, new_val: str) -> None:
404+
"""Setter property needed to support do_set when it updates allow_style"""
405405
new_val = new_val.lower()
406-
if new_val == ansi.ANSI_TERMINAL.lower():
407-
ansi.allow_ansi = ansi.ANSI_TERMINAL
408-
elif new_val == ansi.ANSI_ALWAYS.lower():
409-
ansi.allow_ansi = ansi.ANSI_ALWAYS
410-
elif new_val == ansi.ANSI_NEVER.lower():
411-
ansi.allow_ansi = ansi.ANSI_NEVER
406+
if new_val == ansi.STYLE_TERMINAL.lower():
407+
ansi.allow_style = ansi.STYLE_TERMINAL
408+
elif new_val == ansi.STYLE_ALWAYS.lower():
409+
ansi.allow_style = ansi.STYLE_ALWAYS
410+
elif new_val == ansi.STYLE_NEVER.lower():
411+
ansi.allow_style = ansi.STYLE_NEVER
412412
else:
413-
self.perror('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.ANSI_TERMINAL,
414-
ansi.ANSI_ALWAYS, ansi.ANSI_NEVER))
413+
self.perror('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.STYLE_TERMINAL,
414+
ansi.STYLE_ALWAYS, ansi.STYLE_NEVER))
415415

416416
def _completion_supported(self) -> bool:
417417
"""Return whether tab completion is supported"""
418418
return self.use_rawinput and self.completekey and rl_type != RlType.NONE
419419

420420
@property
421421
def visible_prompt(self) -> str:
422-
"""Read-only property to get the visible prompt with any ANSI escape codes stripped.
422+
"""Read-only property to get the visible prompt with any ANSI style escape codes stripped.
423423
424424
Used by transcript testing to make it easier and more reliable when users are doing things like coloring the
425425
prompt using ANSI color codes.
426426
427427
:return: prompt stripped of any ANSI escape codes
428428
"""
429-
return ansi.strip_ansi(self.prompt)
429+
return ansi.strip_style(self.prompt)
430430

431431
def poutput(self, msg: Any = '', *, end: str = '\n') -> None:
432432
"""Print message to self.stdout and appends a newline by default
@@ -439,7 +439,7 @@ def poutput(self, msg: Any = '', *, end: str = '\n') -> None:
439439
:param end: string appended after the end of the message, default a newline
440440
"""
441441
try:
442-
ansi.ansi_aware_write(self.stdout, "{}{}".format(msg, end))
442+
ansi.style_aware_write(self.stdout, "{}{}".format(msg, end))
443443
except BrokenPipeError:
444444
# This occurs if a command's output is being piped to another
445445
# process and that process closes before the command is
@@ -462,7 +462,7 @@ def perror(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) ->
462462
final_msg = ansi.style_error(msg)
463463
else:
464464
final_msg = "{}".format(msg)
465-
ansi.ansi_aware_write(sys.stderr, final_msg + end)
465+
ansi.style_aware_write(sys.stderr, final_msg + end)
466466

467467
def pwarning(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None:
468468
"""Wraps perror, but applies ansi.style_warning by default
@@ -551,8 +551,8 @@ def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None:
551551
# Don't attempt to use a pager that can block if redirecting or running a script (either text or Python)
552552
# Also only attempt to use a pager if actually running in a real fully functional terminal
553553
if functional_terminal and not self._redirecting and not self.in_pyscript() and not self.in_script():
554-
if ansi.allow_ansi.lower() == ansi.ANSI_NEVER.lower():
555-
msg_str = ansi.strip_ansi(msg_str)
554+
if ansi.allow_style.lower() == ansi.STYLE_NEVER.lower():
555+
msg_str = ansi.strip_style(msg_str)
556556
msg_str += end
557557

558558
pager = self.pager
@@ -1096,7 +1096,7 @@ def _display_matches_gnu_readline(self, substitution: str, matches: List[str],
10961096
longest_match_length = 0
10971097

10981098
for cur_match in matches_to_display:
1099-
cur_length = ansi.ansi_safe_wcswidth(cur_match)
1099+
cur_length = ansi.style_aware_wcswidth(cur_match)
11001100
if cur_length > longest_match_length:
11011101
longest_match_length = cur_length
11021102
else:
@@ -2653,7 +2653,7 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
26532653
widest = 0
26542654
# measure the commands
26552655
for command in cmds:
2656-
width = ansi.ansi_safe_wcswidth(command)
2656+
width = ansi.style_aware_wcswidth(command)
26572657
if width > widest:
26582658
widest = width
26592659
# add a 4-space pad
@@ -3737,7 +3737,7 @@ class TestMyAppCase(Cmd2TestCase):
37373737
test_results = runner.run(testcase)
37383738
execution_time = time.time() - start_time
37393739
if test_results.wasSuccessful():
3740-
ansi.ansi_aware_write(sys.stderr, stream.read())
3740+
ansi.style_aware_write(sys.stderr, stream.read())
37413741
finish_msg = ' {0} transcript{1} passed in {2:.3f} seconds '.format(num_transcripts, plural, execution_time)
37423742
finish_msg = ansi.style_success(utils.align_center(finish_msg, fill_char='='))
37433743
self.poutput(finish_msg)

cmd2/rl_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def rl_set_prompt(prompt: str) -> None: # pragma: no cover
193193

194194

195195
def rl_make_safe_prompt(prompt: str) -> str: # pragma: no cover
196-
"""Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes.
196+
"""Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes
197197
198198
:param prompt: original prompt
199199
:return: prompt safe to pass to GNU Readline

0 commit comments

Comments
 (0)