Skip to content

Commit 3f07590

Browse files
authored
Merge pull request #876 from python-cmd2/color_autocomplete
Added convenience enums of fg and bg colors
2 parents 3c627af + 138c28d commit 3f07590

File tree

9 files changed

+142
-79
lines changed

9 files changed

+142
-79
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
* Enhancements
33
* Changed the default help text to make `help -v` more discoverable
44
* Added `add_settable()` and `remove_settable()` convenience methods to update `self.settable` dictionary
5+
* Added convenience `ansi.fg` and `ansi.bg` enums of foreground and background colors
6+
* `ansi.style()` `fg` argument can now either be of type `str` or `ansi.fg`
7+
* `ansi.style()` `bg` argument can now either be of type `str` or `ansi.bg`
8+
* This supports IDE auto-completion of color names
59
* Breaking changes
610
* Renamed `locals_in_py` attribute of `cmd2.Cmd` to `self_in_py`
711
* The following public attributes of `cmd2.Cmd` are no longer settable at runtime by default:

cmd2/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# package is not installed
1111
pass
1212

13-
from .ansi import style
13+
from .ansi import style, fg, bg
1414
from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem, set_default_argument_parser
1515

1616
# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER

cmd2/ansi.py

Lines changed: 102 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
Support for ANSI escape sequences which are used for things like applying style to text,
44
setting the window title, and asynchronous alerts.
55
"""
6+
from enum import Enum, unique
67
import functools
78
import re
8-
from typing import Any, IO
9+
from typing import Any, IO, List, Union
910

1011
import colorama
1112
from colorama import Fore, Back, Style
@@ -25,57 +26,91 @@
2526
# Regular expression to match ANSI style sequences (including 8-bit and 24-bit colors)
2627
ANSI_STYLE_RE = re.compile(r'\x1b\[[^m]*m')
2728

28-
# Foreground color presets
29-
FG_COLORS = {
30-
'black': Fore.BLACK,
31-
'red': Fore.RED,
32-
'green': Fore.GREEN,
33-
'yellow': Fore.YELLOW,
34-
'blue': Fore.BLUE,
35-
'magenta': Fore.MAGENTA,
36-
'cyan': Fore.CYAN,
37-
'white': Fore.WHITE,
38-
'bright_black': Fore.LIGHTBLACK_EX,
39-
'bright_red': Fore.LIGHTRED_EX,
40-
'bright_green': Fore.LIGHTGREEN_EX,
41-
'bright_yellow': Fore.LIGHTYELLOW_EX,
42-
'bright_blue': Fore.LIGHTBLUE_EX,
43-
'bright_magenta': Fore.LIGHTMAGENTA_EX,
44-
'bright_cyan': Fore.LIGHTCYAN_EX,
45-
'bright_white': Fore.LIGHTWHITE_EX,
46-
'reset': Fore.RESET,
47-
}
48-
49-
# Background color presets
50-
BG_COLORS = {
51-
'black': Back.BLACK,
52-
'red': Back.RED,
53-
'green': Back.GREEN,
54-
'yellow': Back.YELLOW,
55-
'blue': Back.BLUE,
56-
'magenta': Back.MAGENTA,
57-
'cyan': Back.CYAN,
58-
'white': Back.WHITE,
59-
'bright_black': Back.LIGHTBLACK_EX,
60-
'bright_red': Back.LIGHTRED_EX,
61-
'bright_green': Back.LIGHTGREEN_EX,
62-
'bright_yellow': Back.LIGHTYELLOW_EX,
63-
'bright_blue': Back.LIGHTBLUE_EX,
64-
'bright_magenta': Back.LIGHTMAGENTA_EX,
65-
'bright_cyan': Back.LIGHTCYAN_EX,
66-
'bright_white': Back.LIGHTWHITE_EX,
67-
'reset': Back.RESET,
68-
}
69-
70-
FG_RESET = FG_COLORS['reset']
71-
BG_RESET = BG_COLORS['reset']
29+
30+
# Foreground colors
31+
# noinspection PyPep8Naming,DuplicatedCode
32+
@unique
33+
class fg(Enum):
34+
"""Enum class for foreground colors (to support IDE autocompletion)."""
35+
black = Fore.BLACK
36+
red = Fore.RED
37+
green = Fore.GREEN
38+
yellow = Fore.YELLOW
39+
blue = Fore.BLUE
40+
magenta = Fore.MAGENTA
41+
cyan = Fore.CYAN
42+
white = Fore.WHITE
43+
bright_black = Fore.LIGHTBLACK_EX
44+
bright_red = Fore.LIGHTRED_EX
45+
bright_green = Fore.LIGHTGREEN_EX
46+
bright_yellow = Fore.LIGHTYELLOW_EX
47+
bright_blue = Fore.LIGHTBLUE_EX
48+
bright_magenta = Fore.LIGHTMAGENTA_EX
49+
bright_cyan = Fore.LIGHTCYAN_EX
50+
bright_white = Fore.LIGHTWHITE_EX
51+
reset = Fore.RESET
52+
53+
def __str__(self) -> str:
54+
"""Make the value the string representation instead of the enum name."""
55+
return self.value
56+
57+
@staticmethod
58+
def colors() -> List[str]:
59+
"""Return a list of color names."""
60+
return [color.name for color in fg]
61+
62+
@staticmethod
63+
def get_value(name: str) -> str:
64+
"""Retrieve color code by name string."""
65+
return fg.__members__[name].value
66+
67+
68+
# Background colors
69+
# noinspection PyPep8Naming,DuplicatedCode
70+
@unique
71+
class bg(Enum):
72+
"""Enum class for background colors (to support IDE autocompletion)."""
73+
black = Back.BLACK
74+
red = Back.RED
75+
green = Back.GREEN
76+
yellow = Back.YELLOW
77+
blue = Back.BLUE
78+
magenta = Back.MAGENTA
79+
cyan = Back.CYAN
80+
white = Back.WHITE
81+
bright_black = Back.LIGHTBLACK_EX
82+
bright_red = Back.LIGHTRED_EX
83+
bright_green = Back.LIGHTGREEN_EX
84+
bright_yellow = Back.LIGHTYELLOW_EX
85+
bright_blue = Back.LIGHTBLUE_EX
86+
bright_magenta = Back.LIGHTMAGENTA_EX
87+
bright_cyan = Back.LIGHTCYAN_EX
88+
bright_white = Back.LIGHTWHITE_EX
89+
reset = Back.RESET
90+
91+
def __str__(self) -> str:
92+
"""Make the value the string representation instead of the enum name."""
93+
return self.value
94+
95+
@staticmethod
96+
def colors() -> List[str]:
97+
"""Return a list of color names."""
98+
return [color.name for color in bg]
99+
100+
@staticmethod
101+
def get_value(name: str) -> str:
102+
"""Retrieve color code by name string."""
103+
return bg.__members__[name].value
104+
105+
106+
FG_RESET = fg.reset.value
107+
BG_RESET = bg.reset.value
72108
RESET_ALL = Style.RESET_ALL
73109

74110
# Text intensities
75111
INTENSITY_BRIGHT = Style.BRIGHT
76112
INTENSITY_DIM = Style.DIM
77113
INTENSITY_NORMAL = Style.NORMAL
78-
79114
# ANSI style sequences not provided by colorama
80115
UNDERLINE_ENABLE = colorama.ansi.code_to_chars(4)
81116
UNDERLINE_DISABLE = colorama.ansi.code_to_chars(24)
@@ -115,46 +150,53 @@ def style_aware_write(fileobj: IO, msg: str) -> None:
115150
fileobj.write(msg)
116151

117152

118-
def fg_lookup(fg_name: str) -> str:
153+
def fg_lookup(fg_name: Union[str, fg]) -> str:
119154
"""
120155
Look up ANSI escape codes based on foreground color name.
121156
122-
:param fg_name: foreground color name to look up ANSI escape code(s) for
157+
:param fg_name: foreground color name or enum to look up ANSI escape code(s) for
123158
:return: ANSI escape code(s) associated with this color
124159
:raises ValueError: if the color cannot be found
125160
"""
161+
if isinstance(fg_name, fg):
162+
return fg_name.value
163+
126164
try:
127-
ansi_escape = FG_COLORS[fg_name.lower()]
165+
ansi_escape = fg.get_value(fg_name.lower())
128166
except KeyError:
129-
raise ValueError('Foreground color {!r} does not exist.'.format(fg_name))
167+
raise ValueError('Foreground color {!r} does not exist; must be one of: {}'.format(fg_name, fg.colors()))
130168
return ansi_escape
131169

132170

133-
def bg_lookup(bg_name: str) -> str:
171+
def bg_lookup(bg_name: Union[str, bg]) -> str:
134172
"""
135173
Look up ANSI escape codes based on background color name.
136174
137-
:param bg_name: background color name to look up ANSI escape code(s) for
175+
:param bg_name: background color name or enum to look up ANSI escape code(s) for
138176
:return: ANSI escape code(s) associated with this color
139177
:raises ValueError: if the color cannot be found
140178
"""
179+
if isinstance(bg_name, bg):
180+
return bg_name.value
181+
141182
try:
142-
ansi_escape = BG_COLORS[bg_name.lower()]
183+
ansi_escape = bg.get_value(bg_name.lower())
143184
except KeyError:
144-
raise ValueError('Background color {!r} does not exist.'.format(bg_name))
185+
raise ValueError('Background color {!r} does not exist; must be one of: {}'.format(bg_name, bg.colors()))
145186
return ansi_escape
146187

147188

148-
def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False,
189+
# noinspection PyShadowingNames
190+
def style(text: Any, *, fg: Union[str, fg] = '', bg: Union[str, bg] = '', bold: bool = False,
149191
dim: bool = False, underline: bool = False) -> str:
150192
"""
151193
Apply ANSI colors and/or styles to a string and return it.
152194
The styling is self contained which means that at the end of the string reset code(s) are issued
153195
to undo whatever styling was done at the beginning.
154196
155197
:param text: Any object compatible with str.format()
156-
:param fg: foreground color. Relies on `fg_lookup()` to retrieve ANSI escape based on name. Defaults to no color.
157-
:param bg: background color. Relies on `bg_lookup()` to retrieve ANSI escape based on name. Defaults to no color.
198+
:param fg: foreground color. Relies on `fg_lookup()` to retrieve ANSI escape based on name or enum. Defaults to no color.
199+
:param bg: background color. Relies on `bg_lookup()` to retrieve ANSI escape based on name or enum. Defaults to no color.
158200
:param bold: apply the bold style if True. Can be combined with dim. Defaults to False.
159201
:param dim: apply the dim style if True. Can be combined with bold. Defaults to False.
160202
:param underline: apply the underline style if True. Defaults to False.
@@ -197,13 +239,13 @@ def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False,
197239
# Default styles for printing strings of various types.
198240
# These can be altered to suit an application's needs and only need to be a
199241
# function with the following structure: func(str) -> str
200-
style_success = functools.partial(style, fg='green')
242+
style_success = functools.partial(style, fg=fg.green)
201243
"""Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text to signify success"""
202244

203-
style_warning = functools.partial(style, fg='bright_yellow')
245+
style_warning = functools.partial(style, fg=fg.bright_yellow)
204246
"""Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text to signify a warning"""
205247

206-
style_error = functools.partial(style, fg='bright_red')
248+
style_error = functools.partial(style, fg=fg.bright_red)
207249
"""Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text to signify an error"""
208250

209251

docs/features/initialization.rst

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ capabilities which you may wish to utilize while initializing the app::
1919
10) How to make custom attributes settable at runtime
2020
"""
2121
import cmd2
22-
from cmd2 import style
23-
from cmd2.ansi import FG_COLORS
22+
from cmd2 import style, fg, bg
2423

2524
class BasicApp(cmd2.Cmd):
2625
CUSTOM_CATEGORY = 'My Custom Commands'
@@ -30,7 +29,7 @@ capabilities which you may wish to utilize while initializing the app::
3029
startup_script='scripts/startup.txt', use_ipython=True)
3130

3231
# Prints an intro banner once upon application startup
33-
self.intro = style('Welcome to cmd2!', fg='red', bg='white', bold=True)
32+
self.intro = style('Welcome to cmd2!', fg=fg.red, bg=bg.white, bold=True)
3433

3534
# Show this as the prompt when asking for input
3635
self.prompt = 'myapp> '
@@ -51,7 +50,7 @@ capabilities which you may wish to utilize while initializing the app::
5150
self.add_settable(cmd2.Settable('foreground_color',
5251
str,
5352
'Foreground color to use with echo command',
54-
choices=FG_COLORS))
53+
choices=fg.colors()))
5554

5655
@cmd2.with_category(CUSTOM_CATEGORY)
5756
def do_intro(self, _):

examples/basic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
6) Shell-like capabilities
1010
"""
1111
import cmd2
12-
from cmd2 import style
12+
from cmd2 import style, fg, bg
1313

1414

1515
class BasicApp(cmd2.Cmd):
@@ -19,7 +19,7 @@ def __init__(self):
1919
super().__init__(multiline_commands=['echo'], persistent_history_file='cmd2_history.dat',
2020
startup_script='scripts/startup.txt', use_ipython=True)
2121

22-
self.intro = style('Welcome to PyOhio 2019 and cmd2!', fg='red', bg='white', bold=True) + ' 😀'
22+
self.intro = style('Welcome to PyOhio 2019 and cmd2!', fg=fg.red, bg=bg.white, bold=True) + ' 😀'
2323

2424
# Allow access to your application in py and ipy via self
2525
self.self_in_py = True

examples/colors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ def __init__(self):
4848
speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
4949
speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
5050
speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times')
51-
speak_parser.add_argument('-f', '--fg', choices=ansi.FG_COLORS, help='foreground color to apply to output')
52-
speak_parser.add_argument('-b', '--bg', choices=ansi.BG_COLORS, help='background color to apply to output')
51+
speak_parser.add_argument('-f', '--fg', choices=ansi.fg.colors(), help='foreground color to apply to output')
52+
speak_parser.add_argument('-b', '--bg', choices=ansi.bg.colors(), help='background color to apply to output')
5353
speak_parser.add_argument('-l', '--bold', action='store_true', help='bold the output')
5454
speak_parser.add_argument('-u', '--underline', action='store_true', help='underline the output')
5555
speak_parser.add_argument('words', nargs='+', help='words to say')

examples/initialization.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
10) How to make custom attributes settable at runtime
1414
"""
1515
import cmd2
16-
from cmd2 import style
17-
from cmd2.ansi import FG_COLORS
16+
from cmd2 import style, fg, bg
1817

1918

2019
class BasicApp(cmd2.Cmd):
@@ -25,7 +24,7 @@ def __init__(self):
2524
startup_script='scripts/startup.txt', use_ipython=True)
2625

2726
# Prints an intro banner once upon application startup
28-
self.intro = style('Welcome to cmd2!', fg='red', bg='white', bold=True)
27+
self.intro = style('Welcome to cmd2!', fg=fg.red, bg=bg.white, bold=True)
2928

3029
# Show this as the prompt when asking for input
3130
self.prompt = 'myapp> '
@@ -44,7 +43,7 @@ def __init__(self):
4443

4544
# Make echo_fg settable at runtime
4645
self.add_settable(cmd2.Settable('foreground_color', str, 'Foreground color to use with echo command',
47-
choices=FG_COLORS))
46+
choices=fg.colors()))
4847

4948
@cmd2.with_category(CUSTOM_CATEGORY)
5049
def do_intro(self, _):

examples/pirate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(self):
2525
self.songcolor = 'blue'
2626

2727
# Make songcolor settable at runtime
28-
self.add_settable(cmd2.Settable('songcolor', str, 'Color to ``sing``', choices=cmd2.ansi.FG_COLORS))
28+
self.add_settable(cmd2.Settable('songcolor', str, 'Color to ``sing``', choices=cmd2.ansi.fg.colors()))
2929

3030
# prompts and defaults
3131
self.gold = 0

0 commit comments

Comments
 (0)