diff options
Diffstat (limited to 'cmd2/ansi.py')
-rw-r--r-- | cmd2/ansi.py | 1154 |
1 files changed, 950 insertions, 204 deletions
diff --git a/cmd2/ansi.py b/cmd2/ansi.py index 32b50697..91188a9c 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -12,165 +12,56 @@ from typing import ( IO, Any, List, - Union, + Optional, cast, ) -import colorama # type: ignore [import] -from colorama import ( - Back, - Fore, - Style, -) -from wcwidth import ( # type: ignore [import] +from wcwidth import ( # type: ignore[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) +####################################################### +# Common ANSI escape sequence constants +####################################################### +CSI = '\033[' +OSC = '\033]' +BEL = '\a' -# Values for allow_style setting -STYLE_NEVER = 'Never' -""" -Constant for ``cmd2.ansi.allow_style`` to indicate ANSI style sequences should -be removed from all output. -""" -STYLE_TERMINAL = 'Terminal' -""" -Constant for ``cmd2.ansi.allow_style`` to indicate ANSI style sequences -should be removed if the output is not going to the terminal. -""" -STYLE_ALWAYS = 'Always' -""" -Constant for ``cmd2.ansi.allow_style`` to indicate ANSI style sequences should -always be output. -""" -# Controls when ANSI style sequences are allowed in output -allow_style = STYLE_TERMINAL -"""When using outside of a cmd2 app, set this variable to one of: +class AllowStyle(Enum): + """Values for ``cmd2.ansi.allow_style``""" -- ``STYLE_NEVER`` - remove ANSI style sequences from all output -- ``STYLE_TERMINAL`` - remove ANSI style sequences if the output is not going to the terminal -- ``STYLE_ALWAYS`` - always output ANSI style sequences + ALWAYS = 'Always' # Always output ANSI style sequences + NEVER = 'Never' # Remove ANSI style sequences from all output + TERMINAL = 'Terminal' # Remove ANSI style sequences if the output is not going to the terminal -to control the output of ANSI style sequences by methods in this module. - -The default is ``STYLE_TERMINAL``. -""" - -# Regular expression to match ANSI style sequences (including 8-bit and 24-bit colors) -ANSI_STYLE_RE = re.compile(r'\x1b\[[^m]*m') - - -class ColorBase(Enum): - """ - Base class used for defining color enums. See fg and bg classes for examples. - - Child classes should define enums in the follow structure: + def __str__(self) -> str: + """Return value instead of enum name for printing in cmd2's set command""" + return str(self.value) - key: color name (e.g. black) + def __repr__(self) -> str: + """Return quoted value instead of enum description for printing in cmd2's set command""" + return repr(self.value) - value: anything that when cast to a string returns an ANSI sequence - """ - def __str__(self) -> str: - """ - Return ANSI color sequence instead of enum name - This is helpful when using a ColorBase in an f-string or format() call - e.g. my_str = f"{fg.blue}hello{fg.reset}" - """ - return str(self.value) +# Controls when ANSI style sequences are allowed in output +allow_style = AllowStyle.TERMINAL +"""When using outside of a cmd2 app, set this variable to one of: - def __add__(self, other: Any) -> str: - """ - Support building a color string when self is the left operand - e.g. fg.blue + "hello" - """ - return cast(str, str(self) + other) +- ``AllowStyle.ALWAYS`` - always output ANSI style sequences +- ``AllowStyle.NEVER`` - remove ANSI style sequences from all output +- ``AllowStyle.TERMINAL`` - remove ANSI style sequences if the output is not going to the terminal - def __radd__(self, other: Any) -> str: - """ - Support building a color string when self is the right operand - e.g. "hello" + fg.reset - """ - return cast(str, other + str(self)) +to control how ANSI style sequences are handled by ``style_aware_write()``. - @classmethod - def colors(cls) -> List[str]: - """Return a list of color names.""" - # Use __members__ to ensure we get all key names, including those which are aliased - return [color for color in cls.__members__] +``style_aware_write()`` is called by cmd2 methods like ``poutput()``, ``perror()``, +``pwarning()``, etc. +The default is ``AllowStyle.TERMINAL``. +""" -# Foreground colors -# noinspection PyPep8Naming -class fg(ColorBase): - """Enum class for foreground colors""" - - black = Fore.BLACK - red = Fore.RED - green = Fore.GREEN - yellow = Fore.YELLOW - blue = Fore.BLUE - magenta = Fore.MAGENTA - cyan = Fore.CYAN - white = Fore.WHITE - bright_black = Fore.LIGHTBLACK_EX - bright_red = Fore.LIGHTRED_EX - bright_green = Fore.LIGHTGREEN_EX - bright_yellow = Fore.LIGHTYELLOW_EX - bright_blue = Fore.LIGHTBLUE_EX - bright_magenta = Fore.LIGHTMAGENTA_EX - bright_cyan = Fore.LIGHTCYAN_EX - bright_white = Fore.LIGHTWHITE_EX - reset = Fore.RESET - - -# Background colors -# noinspection PyPep8Naming -class bg(ColorBase): - """Enum class for background colors""" - - black = Back.BLACK - red = Back.RED - green = Back.GREEN - yellow = Back.YELLOW - blue = Back.BLUE - magenta = Back.MAGENTA - cyan = Back.CYAN - white = Back.WHITE - bright_black = Back.LIGHTBLACK_EX - bright_red = Back.LIGHTRED_EX - bright_green = Back.LIGHTGREEN_EX - bright_yellow = Back.LIGHTYELLOW_EX - bright_blue = Back.LIGHTBLUE_EX - bright_magenta = Back.LIGHTMAGENTA_EX - bright_cyan = Back.LIGHTCYAN_EX - bright_white = Back.LIGHTWHITE_EX - reset = Back.RESET - - -FG_RESET = fg.reset.value -"""ANSI sequence to reset the foreground attributes""" -BG_RESET = bg.reset.value -"""ANSI sequence to reset the terminal background attributes""" -RESET_ALL = Style.RESET_ALL -"""ANSI sequence to reset all terminal attributes""" - -# Text intensities -INTENSITY_BRIGHT = Style.BRIGHT -"""ANSI sequence to make the text bright""" -INTENSITY_DIM = Style.DIM -"""ANSI sequence to make the text dim""" -INTENSITY_NORMAL = Style.NORMAL -"""ANSI sequence to make the text normal""" - -# ANSI style sequences not provided by colorama -UNDERLINE_ENABLE = colorama.ansi.code_to_chars(4) -"""ANSI sequence to turn on underline""" -UNDERLINE_DISABLE = colorama.ansi.code_to_chars(24) -"""ANSI sequence to turn off underline""" +# Regular expression to match ANSI style sequences (including 8-bit and 24-bit colors) +ANSI_STYLE_RE = re.compile(r'\x1b\[[^m]*m') def strip_style(text: str) -> str: @@ -225,56 +116,843 @@ def style_aware_write(fileobj: IO[str], msg: str) -> None: :param fileobj: the file object being written to :param msg: the string being written """ - if allow_style.lower() == STYLE_NEVER.lower() or (allow_style.lower() == STYLE_TERMINAL.lower() and not fileobj.isatty()): + if allow_style == AllowStyle.NEVER or (allow_style == AllowStyle.TERMINAL and not fileobj.isatty()): msg = strip_style(msg) fileobj.write(msg) -def fg_lookup(fg_name: Union[str, fg]) -> str: +#################################################################################### +# Utility functions which create various ANSI sequences +#################################################################################### +def set_title(title: str) -> str: """ - Look up ANSI escape codes based on foreground color name. + Generate a string that, when printed, sets a terminal's window title. - :param fg_name: foreground color name or enum to look up ANSI escape code(s) for - :return: ANSI escape code(s) associated with this color - :raises: ValueError: if the color cannot be found + :param title: new title for the window + :return: the set title string """ - if isinstance(fg_name, fg): - return str(fg_name.value) + return f"{OSC}2;{title}{BEL}" - try: - ansi_escape = fg[fg_name.lower()].value - except KeyError: - raise ValueError(f"Foreground color '{fg_name}' does not exist; must be one of: {fg.colors()}") - return str(ansi_escape) + +def clear_screen(clear_type: int = 2) -> str: + """ + Generate a string that, when printed, clears a terminal screen based on value of clear_type. + + :param clear_type: integer which specifies how to clear the screen (Defaults to 2) + Possible values: + 0 - clear from cursor to end of screen + 1 - clear from cursor to beginning of the screen + 2 - clear entire screen + 3 - clear entire screen and delete all lines saved in the scrollback buffer + :return: the clear screen string + :raises: ValueError if clear_type is not a valid value + """ + if 0 <= clear_type <= 3: + return f"{CSI}{clear_type}J" + raise ValueError("clear_type must in an integer from 0 to 3") -def bg_lookup(bg_name: Union[str, bg]) -> str: +def clear_line(clear_type: int = 2) -> str: + """ + Generate a string that, when printed, clears a line based on value of clear_type. + + :param clear_type: integer which specifies how to clear the line (Defaults to 2) + Possible values: + 0 - clear from cursor to the end of the line + 1 - clear from cursor to beginning of the line + 2 - clear entire line + :return: the clear line string + :raises: ValueError if clear_type is not a valid value """ - Look up ANSI escape codes based on background color name. + if 0 <= clear_type <= 2: + return f"{CSI}{clear_type}K" + raise ValueError("clear_type must in an integer from 0 to 2") - :param bg_name: background color name or enum to look up ANSI escape code(s) for - :return: ANSI escape code(s) associated with this color - :raises: ValueError: if the color cannot be found + +#################################################################################### +# Base classes which are not intended to be used directly +#################################################################################### +class AnsiSequence: + """Base class to create ANSI sequence strings""" + + def __add__(self, other: Any) -> str: + """ + Support building an ANSI sequence string when self is the left operand + e.g. Fg.LIGHT_MAGENTA + "hello" + """ + return str(self) + str(other) + + def __radd__(self, other: Any) -> str: + """ + Support building an ANSI sequence string when self is the right operand + e.g. "hello" + Fg.RESET + """ + return str(other) + str(self) + + +class FgColor(AnsiSequence): + """Base class for ANSI Sequences which set foreground text color""" + + pass + + +class BgColor(AnsiSequence): + """Base class for ANSI Sequences which set background text color""" + + pass + + +#################################################################################### +# Implementations intended for direct use +#################################################################################### +# noinspection PyPep8Naming +class Cursor: + """Create ANSI sequences to alter the cursor position""" + + @staticmethod + def UP(count: int = 1) -> str: + """Move the cursor up a specified amount of lines (Defaults to 1)""" + return f"{CSI}{count}A" + + @staticmethod + def DOWN(count: int = 1) -> str: + """Move the cursor down a specified amount of lines (Defaults to 1)""" + return f"{CSI}{count}B" + + @staticmethod + def FORWARD(count: int = 1) -> str: + """Move the cursor forward a specified amount of lines (Defaults to 1)""" + return f"{CSI}{count}C" + + @staticmethod + def BACK(count: int = 1) -> str: + """Move the cursor back a specified amount of lines (Defaults to 1)""" + return f"{CSI}{count}D" + + @staticmethod + def SET_POS(x: int, y: int) -> str: + """Set the cursor position to coordinates which are 1-based""" + return f"{CSI}{y};{x}H" + + +class TextStyle(AnsiSequence, Enum): + """Create text style ANSI sequences""" + + # Resets all styles and colors of text + RESET_ALL = 0 + + INTENSITY_BOLD = 1 + INTENSITY_DIM = 2 + INTENSITY_NORMAL = 22 + + ITALIC_ENABLE = 3 + ITALIC_DISABLE = 23 + + OVERLINE_ENABLE = 53 + OVERLINE_DISABLE = 55 + + STRIKETHROUGH_ENABLE = 9 + STRIKETHROUGH_DISABLE = 29 + + UNDERLINE_ENABLE = 4 + UNDERLINE_DISABLE = 24 + + def __str__(self) -> str: + """ + Return ANSI text style sequence instead of enum name + This is helpful when using a TextStyle in an f-string or format() call + e.g. my_str = f"{TextStyle.UNDERLINE_ENABLE}hello{TextStyle.UNDERLINE_DISABLE}" + """ + return f"{CSI}{self.value}m" + + +class Fg(FgColor, Enum): + """ + Create ANSI sequences for the 16 standard terminal foreground text colors. + A terminal's color settings affect how these colors appear. + To reset any foreground color, use Fg.RESET. """ - if isinstance(bg_name, bg): - return str(bg_name.value) - try: - ansi_escape = bg[bg_name.lower()].value - except KeyError: - raise ValueError(f"Background color '{bg_name}' does not exist; must be one of: {bg.colors()}") - return str(ansi_escape) + BLACK = 30 + RED = 31 + GREEN = 32 + YELLOW = 33 + BLUE = 34 + MAGENTA = 35 + CYAN = 36 + LIGHT_GRAY = 37 + DARK_GRAY = 90 + LIGHT_RED = 91 + LIGHT_GREEN = 92 + LIGHT_YELLOW = 93 + LIGHT_BLUE = 94 + LIGHT_MAGENTA = 95 + LIGHT_CYAN = 96 + WHITE = 97 + + RESET = 39 + def __str__(self) -> str: + """ + Return ANSI color sequence instead of enum name + This is helpful when using an Fg in an f-string or format() call + e.g. my_str = f"{Fg.BLUE}hello{Fg.RESET}" + """ + return f"{CSI}{self.value}m" + + +class Bg(BgColor, Enum): + """ + Create ANSI sequences for the 16 standard terminal background text colors. + A terminal's color settings affect how these colors appear. + To reset any background color, use Bg.RESET. + """ + + BLACK = 40 + RED = 41 + GREEN = 42 + YELLOW = 44 + BLUE = 44 + MAGENTA = 45 + CYAN = 46 + LIGHT_GRAY = 47 + DARK_GRAY = 100 + LIGHT_RED = 101 + LIGHT_GREEN = 102 + LIGHT_YELLOW = 103 + LIGHT_BLUE = 104 + LIGHT_MAGENTA = 105 + LIGHT_CYAN = 106 + WHITE = 107 + + RESET = 49 + + def __str__(self) -> str: + """ + Return ANSI color sequence instead of enum name + This is helpful when using a Bg in an f-string or format() call + e.g. my_str = f"{Bg.BLACK}hello{Bg.RESET}" + """ + return f"{CSI}{self.value}m" + + +class EightBitFg(FgColor, Enum): + """ + Create ANSI sequences for 8-bit terminal foreground text colors. Most terminals support 8-bit/256-color mode. + The first 16 colors correspond to the 16 colors from Fg and behave the same way. + To reset any foreground color, including 8-bit, use Fg.RESET. + """ + + BLACK = 0 + RED = 1 + GREEN = 2 + YELLOW = 3 + BLUE = 4 + MAGENTA = 5 + CYAN = 6 + LIGHT_GRAY = 7 + DARK_GRAY = 8 + LIGHT_RED = 9 + LIGHT_GREEN = 10 + LIGHT_YELLOW = 11 + LIGHT_BLUE = 12 + LIGHT_MAGENTA = 13 + LIGHT_CYAN = 14 + WHITE = 15 + GRAY_0 = 16 + NAVY_BLUE = 17 + DARK_BLUE = 18 + BLUE_3A = 19 + BLUE_3B = 20 + BLUE_1 = 21 + DARK_GREEN = 22 + DEEP_SKY_BLUE_4A = 23 + DEEP_SKY_BLUE_4B = 24 + DEEP_SKY_BLUE_4C = 25 + DODGER_BLUE_3 = 26 + DODGER_BLUE_2 = 27 + GREEN_4 = 28 + SPRING_GREEN_4 = 29 + TURQUOISE_4 = 30 + DEEP_SKY_BLUE_3A = 31 + DEEP_SKY_BLUE_3B = 32 + DODGER_BLUE_1 = 33 + GREEN_3A = 34 + SPRING_GREEN_3A = 35 + DARK_CYAN = 36 + LIGHT_SEA_GREEN = 37 + DEEP_SKY_BLUE_2 = 38 + DEEP_SKY_BLUE_1 = 39 + GREEN_3B = 40 + SPRING_GREEN_3B = 41 + SPRING_GREEN_2A = 42 + CYAN_3 = 43 + DARK_TURQUOISE = 44 + TURQUOISE_2 = 45 + GREEN_1 = 46 + SPRING_GREEN_2B = 47 + SPRING_GREEN_1 = 48 + MEDIUM_SPRING_GREEN = 49 + CYAN_2 = 50 + CYAN_1 = 51 + DARK_RED_1 = 52 + DEEP_PINK_4A = 53 + PURPLE_4A = 54 + PURPLE_4B = 55 + PURPLE_3 = 56 + BLUE_VIOLET = 57 + ORANGE_4A = 58 + GRAY_37 = 59 + MEDIUM_PURPLE_4 = 60 + SLATE_BLUE_3A = 61 + SLATE_BLUE_3B = 62 + ROYAL_BLUE_1 = 63 + CHARTREUSE_4 = 64 + DARK_SEA_GREEN_4A = 65 + PALE_TURQUOISE_4 = 66 + STEEL_BLUE = 67 + STEEL_BLUE_3 = 68 + CORNFLOWER_BLUE = 69 + CHARTREUSE_3A = 70 + DARK_SEA_GREEN_4B = 71 + CADET_BLUE_2 = 72 + CADET_BLUE_1 = 73 + SKY_BLUE_3 = 74 + STEEL_BLUE_1A = 75 + CHARTREUSE_3B = 76 + PALE_GREEN_3A = 77 + SEA_GREEN_3 = 78 + AQUAMARINE_3 = 79 + MEDIUM_TURQUOISE = 80 + STEEL_BLUE_1B = 81 + CHARTREUSE_2A = 82 + SEA_GREEN_2 = 83 + SEA_GREEN_1A = 84 + SEA_GREEN_1B = 85 + AQUAMARINE_1A = 86 + DARK_SLATE_GRAY_2 = 87 + DARK_RED_2 = 88 + DEEP_PINK_4B = 89 + DARK_MAGENTA_1 = 90 + DARK_MAGENTA_2 = 91 + DARK_VIOLET_1A = 92 + PURPLE_1A = 93 + ORANGE_4B = 94 + LIGHT_PINK_4 = 95 + PLUM_4 = 96 + MEDIUM_PURPLE_3A = 97 + MEDIUM_PURPLE_3B = 98 + SLATE_BLUE_1 = 99 + YELLOW_4A = 100 + WHEAT_4 = 101 + GRAY_53 = 102 + LIGHT_SLATE_GRAY = 103 + MEDIUM_PURPLE = 104 + LIGHT_SLATE_BLUE = 105 + YELLOW_4B = 106 + DARK_OLIVE_GREEN_3A = 107 + DARK_GREEN_SEA = 108 + LIGHT_SKY_BLUE_3A = 109 + LIGHT_SKY_BLUE_3B = 110 + SKY_BLUE_2 = 111 + CHARTREUSE_2B = 112 + DARK_OLIVE_GREEN_3B = 113 + PALE_GREEN_3B = 114 + DARK_SEA_GREEN_3A = 115 + DARK_SLATE_GRAY_3 = 116 + SKY_BLUE_1 = 117 + CHARTREUSE_1 = 118 + LIGHT_GREEN_2 = 119 + LIGHT_GREEN_3 = 120 + PALE_GREEN_1A = 121 + AQUAMARINE_1B = 122 + DARK_SLATE_GRAY_1 = 123 + RED_3A = 124 + DEEP_PINK_4C = 125 + MEDIUM_VIOLET_RED = 126 + MAGENTA_3A = 127 + DARK_VIOLET_1B = 128 + PURPLE_1B = 129 + DARK_ORANGE_3A = 130 + INDIAN_RED_1A = 131 + HOT_PINK_3A = 132 + MEDIUM_ORCHID_3 = 133 + MEDIUM_ORCHID = 134 + MEDIUM_PURPLE_2A = 135 + DARK_GOLDENROD = 136 + LIGHT_SALMON_3A = 137 + ROSY_BROWN = 138 + GRAY_63 = 139 + MEDIUM_PURPLE_2B = 140 + MEDIUM_PURPLE_1 = 141 + GOLD_3A = 142 + DARK_KHAKI = 143 + NAVAJO_WHITE_3 = 144 + GRAY_69 = 145 + LIGHT_STEEL_BLUE_3 = 146 + LIGHT_STEEL_BLUE = 147 + YELLOW_3A = 148 + DARK_OLIVE_GREEN_3 = 149 + DARK_SEA_GREEN_3B = 150 + DARK_SEA_GREEN_2 = 151 + LIGHT_CYAN_3 = 152 + LIGHT_SKY_BLUE_1 = 153 + GREEN_YELLOW = 154 + DARK_OLIVE_GREEN_2 = 155 + PALE_GREEN_1B = 156 + DARK_SEA_GREEN_5B = 157 + DARK_SEA_GREEN_5A = 158 + PALE_TURQUOISE_1 = 159 + RED_3B = 160 + DEEP_PINK_3A = 161 + DEEP_PINK_3B = 162 + MAGENTA_3B = 163 + MAGENTA_3C = 164 + MAGENTA_2A = 165 + DARK_ORANGE_3B = 166 + INDIAN_RED_1B = 167 + HOT_PINK_3B = 168 + HOT_PINK_2 = 169 + ORCHID = 170 + MEDIUM_ORCHID_1A = 171 + ORANGE_3 = 172 + LIGHT_SALMON_3B = 173 + LIGHT_PINK_3 = 174 + PINK_3 = 175 + PLUM_3 = 176 + VIOLET = 177 + GOLD_3B = 178 + LIGHT_GOLDENROD_3 = 179 + TAN = 180 + MISTY_ROSE_3 = 181 + THISTLE_3 = 182 + PLUM_2 = 183 + YELLOW_3B = 184 + KHAKI_3 = 185 + LIGHT_GOLDENROD_2A = 186 + LIGHT_YELLOW_3 = 187 + GRAY_84 = 188 + LIGHT_STEEL_BLUE_1 = 189 + YELLOW_2 = 190 + DARK_OLIVE_GREEN_1A = 191 + DARK_OLIVE_GREEN_1B = 192 + DARK_SEA_GREEN_1 = 193 + HONEYDEW_2 = 194 + LIGHT_CYAN_1 = 195 + RED_1 = 196 + DEEP_PINK_2 = 197 + DEEP_PINK_1A = 198 + DEEP_PINK_1B = 199 + MAGENTA_2B = 200 + MAGENTA_1 = 201 + ORANGE_RED_1 = 202 + INDIAN_RED_1C = 203 + INDIAN_RED_1D = 204 + HOT_PINK_1A = 205 + HOT_PINK_1B = 206 + MEDIUM_ORCHID_1B = 207 + DARK_ORANGE = 208 + SALMON_1 = 209 + LIGHT_CORAL = 210 + PALE_VIOLET_RED_1 = 211 + ORCHID_2 = 212 + ORCHID_1 = 213 + ORANGE_1 = 214 + SANDY_BROWN = 215 + LIGHT_SALMON_1 = 216 + LIGHT_PINK_1 = 217 + PINK_1 = 218 + PLUM_1 = 219 + GOLD_1 = 220 + LIGHT_GOLDENROD_2B = 221 + LIGHT_GOLDENROD_2C = 222 + NAVAJO_WHITE_1 = 223 + MISTY_ROSE1 = 224 + THISTLE_1 = 225 + YELLOW_1 = 226 + LIGHT_GOLDENROD_1 = 227 + KHAKI_1 = 228 + WHEAT_1 = 229 + CORNSILK_1 = 230 + GRAY_100 = 231 + GRAY_3 = 232 + GRAY_7 = 233 + GRAY_11 = 234 + GRAY_15 = 235 + GRAY_19 = 236 + GRAY_23 = 237 + GRAY_27 = 238 + GRAY_30 = 239 + GRAY_35 = 240 + GRAY_39 = 241 + GRAY_42 = 242 + GRAY_46 = 243 + GRAY_50 = 244 + GRAY_54 = 245 + GRAY_58 = 246 + GRAY_62 = 247 + GRAY_66 = 248 + GRAY_70 = 249 + GRAY_74 = 250 + GRAY_78 = 251 + GRAY_82 = 252 + GRAY_85 = 253 + GRAY_89 = 254 + GRAY_93 = 255 + + def __str__(self) -> str: + """ + Return ANSI color sequence instead of enum name + This is helpful when using an EightBitFg in an f-string or format() call + e.g. my_str = f"{EightBitFg.SLATE_BLUE_1}hello{Fg.RESET}" + """ + return f"{CSI}{38};5;{self.value}m" + + +class EightBitBg(BgColor, Enum): + """ + Create ANSI sequences for 8-bit terminal background text colors. Most terminals support 8-bit/256-color mode. + The first 16 colors correspond to the 16 colors from Bg and behave the same way. + To reset any background color, including 8-bit, use Bg.RESET. + """ + + BLACK = 0 + RED = 1 + GREEN = 2 + YELLOW = 3 + BLUE = 4 + MAGENTA = 5 + CYAN = 6 + LIGHT_GRAY = 7 + DARK_GRAY = 8 + LIGHT_RED = 9 + LIGHT_GREEN = 10 + LIGHT_YELLOW = 11 + LIGHT_BLUE = 12 + LIGHT_MAGENTA = 13 + LIGHT_CYAN = 14 + WHITE = 15 + GRAY_0 = 16 + NAVY_BLUE = 17 + DARK_BLUE = 18 + BLUE_3A = 19 + BLUE_3B = 20 + BLUE_1 = 21 + DARK_GREEN = 22 + DEEP_SKY_BLUE_4A = 23 + DEEP_SKY_BLUE_4B = 24 + DEEP_SKY_BLUE_4C = 25 + DODGER_BLUE_3 = 26 + DODGER_BLUE_2 = 27 + GREEN_4 = 28 + SPRING_GREEN_4 = 29 + TURQUOISE_4 = 30 + DEEP_SKY_BLUE_3A = 31 + DEEP_SKY_BLUE_3B = 32 + DODGER_BLUE_1 = 33 + GREEN_3A = 34 + SPRING_GREEN_3A = 35 + DARK_CYAN = 36 + LIGHT_SEA_GREEN = 37 + DEEP_SKY_BLUE_2 = 38 + DEEP_SKY_BLUE_1 = 39 + GREEN_3B = 40 + SPRING_GREEN_3B = 41 + SPRING_GREEN_2A = 42 + CYAN_3 = 43 + DARK_TURQUOISE = 44 + TURQUOISE_2 = 45 + GREEN_1 = 46 + SPRING_GREEN_2B = 47 + SPRING_GREEN_1 = 48 + MEDIUM_SPRING_GREEN = 49 + CYAN_2 = 50 + CYAN_1 = 51 + DARK_RED_1 = 52 + DEEP_PINK_4A = 53 + PURPLE_4A = 54 + PURPLE_4B = 55 + PURPLE_3 = 56 + BLUE_VIOLET = 57 + ORANGE_4A = 58 + GRAY_37 = 59 + MEDIUM_PURPLE_4 = 60 + SLATE_BLUE_3A = 61 + SLATE_BLUE_3B = 62 + ROYAL_BLUE_1 = 63 + CHARTREUSE_4 = 64 + DARK_SEA_GREEN_4A = 65 + PALE_TURQUOISE_4 = 66 + STEEL_BLUE = 67 + STEEL_BLUE_3 = 68 + CORNFLOWER_BLUE = 69 + CHARTREUSE_3A = 70 + DARK_SEA_GREEN_4B = 71 + CADET_BLUE_2 = 72 + CADET_BLUE_1 = 73 + SKY_BLUE_3 = 74 + STEEL_BLUE_1A = 75 + CHARTREUSE_3B = 76 + PALE_GREEN_3A = 77 + SEA_GREEN_3 = 78 + AQUAMARINE_3 = 79 + MEDIUM_TURQUOISE = 80 + STEEL_BLUE_1B = 81 + CHARTREUSE_2A = 82 + SEA_GREEN_2 = 83 + SEA_GREEN_1A = 84 + SEA_GREEN_1B = 85 + AQUAMARINE_1A = 86 + DARK_SLATE_GRAY_2 = 87 + DARK_RED_2 = 88 + DEEP_PINK_4B = 89 + DARK_MAGENTA_1 = 90 + DARK_MAGENTA_2 = 91 + DARK_VIOLET_1A = 92 + PURPLE_1A = 93 + ORANGE_4B = 94 + LIGHT_PINK_4 = 95 + PLUM_4 = 96 + MEDIUM_PURPLE_3A = 97 + MEDIUM_PURPLE_3B = 98 + SLATE_BLUE_1 = 99 + YELLOW_4A = 100 + WHEAT_4 = 101 + GRAY_53 = 102 + LIGHT_SLATE_GRAY = 103 + MEDIUM_PURPLE = 104 + LIGHT_SLATE_BLUE = 105 + YELLOW_4B = 106 + DARK_OLIVE_GREEN_3A = 107 + DARK_GREEN_SEA = 108 + LIGHT_SKY_BLUE_3A = 109 + LIGHT_SKY_BLUE_3B = 110 + SKY_BLUE_2 = 111 + CHARTREUSE_2B = 112 + DARK_OLIVE_GREEN_3B = 113 + PALE_GREEN_3B = 114 + DARK_SEA_GREEN_3A = 115 + DARK_SLATE_GRAY_3 = 116 + SKY_BLUE_1 = 117 + CHARTREUSE_1 = 118 + LIGHT_GREEN_2 = 119 + LIGHT_GREEN_3 = 120 + PALE_GREEN_1A = 121 + AQUAMARINE_1B = 122 + DARK_SLATE_GRAY_1 = 123 + RED_3A = 124 + DEEP_PINK_4C = 125 + MEDIUM_VIOLET_RED = 126 + MAGENTA_3A = 127 + DARK_VIOLET_1B = 128 + PURPLE_1B = 129 + DARK_ORANGE_3A = 130 + INDIAN_RED_1A = 131 + HOT_PINK_3A = 132 + MEDIUM_ORCHID_3 = 133 + MEDIUM_ORCHID = 134 + MEDIUM_PURPLE_2A = 135 + DARK_GOLDENROD = 136 + LIGHT_SALMON_3A = 137 + ROSY_BROWN = 138 + GRAY_63 = 139 + MEDIUM_PURPLE_2B = 140 + MEDIUM_PURPLE_1 = 141 + GOLD_3A = 142 + DARK_KHAKI = 143 + NAVAJO_WHITE_3 = 144 + GRAY_69 = 145 + LIGHT_STEEL_BLUE_3 = 146 + LIGHT_STEEL_BLUE = 147 + YELLOW_3A = 148 + DARK_OLIVE_GREEN_3 = 149 + DARK_SEA_GREEN_3B = 150 + DARK_SEA_GREEN_2 = 151 + LIGHT_CYAN_3 = 152 + LIGHT_SKY_BLUE_1 = 153 + GREEN_YELLOW = 154 + DARK_OLIVE_GREEN_2 = 155 + PALE_GREEN_1B = 156 + DARK_SEA_GREEN_5B = 157 + DARK_SEA_GREEN_5A = 158 + PALE_TURQUOISE_1 = 159 + RED_3B = 160 + DEEP_PINK_3A = 161 + DEEP_PINK_3B = 162 + MAGENTA_3B = 163 + MAGENTA_3C = 164 + MAGENTA_2A = 165 + DARK_ORANGE_3B = 166 + INDIAN_RED_1B = 167 + HOT_PINK_3B = 168 + HOT_PINK_2 = 169 + ORCHID = 170 + MEDIUM_ORCHID_1A = 171 + ORANGE_3 = 172 + LIGHT_SALMON_3B = 173 + LIGHT_PINK_3 = 174 + PINK_3 = 175 + PLUM_3 = 176 + VIOLET = 177 + GOLD_3B = 178 + LIGHT_GOLDENROD_3 = 179 + TAN = 180 + MISTY_ROSE_3 = 181 + THISTLE_3 = 182 + PLUM_2 = 183 + YELLOW_3B = 184 + KHAKI_3 = 185 + LIGHT_GOLDENROD_2A = 186 + LIGHT_YELLOW_3 = 187 + GRAY_84 = 188 + LIGHT_STEEL_BLUE_1 = 189 + YELLOW_2 = 190 + DARK_OLIVE_GREEN_1A = 191 + DARK_OLIVE_GREEN_1B = 192 + DARK_SEA_GREEN_1 = 193 + HONEYDEW_2 = 194 + LIGHT_CYAN_1 = 195 + RED_1 = 196 + DEEP_PINK_2 = 197 + DEEP_PINK_1A = 198 + DEEP_PINK_1B = 199 + MAGENTA_2B = 200 + MAGENTA_1 = 201 + ORANGE_RED_1 = 202 + INDIAN_RED_1C = 203 + INDIAN_RED_1D = 204 + HOT_PINK_1A = 205 + HOT_PINK_1B = 206 + MEDIUM_ORCHID_1B = 207 + DARK_ORANGE = 208 + SALMON_1 = 209 + LIGHT_CORAL = 210 + PALE_VIOLET_RED_1 = 211 + ORCHID_2 = 212 + ORCHID_1 = 213 + ORANGE_1 = 214 + SANDY_BROWN = 215 + LIGHT_SALMON_1 = 216 + LIGHT_PINK_1 = 217 + PINK_1 = 218 + PLUM_1 = 219 + GOLD_1 = 220 + LIGHT_GOLDENROD_2B = 221 + LIGHT_GOLDENROD_2C = 222 + NAVAJO_WHITE_1 = 223 + MISTY_ROSE1 = 224 + THISTLE_1 = 225 + YELLOW_1 = 226 + LIGHT_GOLDENROD_1 = 227 + KHAKI_1 = 228 + WHEAT_1 = 229 + CORNSILK_1 = 230 + GRAY_100 = 231 + GRAY_3 = 232 + GRAY_7 = 233 + GRAY_11 = 234 + GRAY_15 = 235 + GRAY_19 = 236 + GRAY_23 = 237 + GRAY_27 = 238 + GRAY_30 = 239 + GRAY_35 = 240 + GRAY_39 = 241 + GRAY_42 = 242 + GRAY_46 = 243 + GRAY_50 = 244 + GRAY_54 = 245 + GRAY_58 = 246 + GRAY_62 = 247 + GRAY_66 = 248 + GRAY_70 = 249 + GRAY_74 = 250 + GRAY_78 = 251 + GRAY_82 = 252 + GRAY_85 = 253 + GRAY_89 = 254 + GRAY_93 = 255 + + def __str__(self) -> str: + """ + Return ANSI color sequence instead of enum name + This is helpful when using an EightBitBg in an f-string or format() call + e.g. my_str = f"{EightBitBg.KHAKI_3}hello{Bg.RESET}" + """ + return f"{CSI}{48};5;{self.value}m" + + +class RgbFg(FgColor): + """ + Create ANSI sequences for 24-bit (RGB) terminal foreground text colors. The terminal must support 24-bit/true-color mode. + To reset any foreground color, including 24-bit, use Fg.RESET. + """ + + def __init__(self, r: int, g: int, b: int) -> None: + """ + RgbFg initializer + + :param r: integer from 0-255 for the red component of the color + :param g: integer from 0-255 for the green component of the color + :param b: integer from 0-255 for the blue component of the color + :raises: ValueError if r, g, or b is not in the range 0-255 + """ + if any(c < 0 or c > 255 for c in [r, g, b]): + raise ValueError("RGB values must be integers in the range of 0 to 255") + + self._sequence = f"{CSI}{38};2;{r};{g};{b}m" + + def __str__(self) -> str: + """ + Return ANSI color sequence instead of enum name + This is helpful when using an RgbFg in an f-string or format() call + e.g. my_str = f"{RgbFg(0, 55, 100)}hello{Fg.RESET}" + """ + return self._sequence + + +class RgbBg(BgColor): + """ + Create ANSI sequences for 24-bit (RGB) terminal background text colors. The terminal must support 24-bit/true-color mode. + To reset any background color, including 24-bit, use Bg.RESET. + """ + + def __init__(self, r: int, g: int, b: int) -> None: + """ + RgbBg initializer + + :param r: integer from 0-255 for the red component of the color + :param g: integer from 0-255 for the green component of the color + :param b: integer from 0-255 for the blue component of the color + :raises: ValueError if r, g, or b is not in the range 0-255 + """ + if any(c < 0 or c > 255 for c in [r, g, b]): + raise ValueError("RGB values must be integers in the range of 0 to 255") + + self._sequence = f"{CSI}{48};2;{r};{g};{b}m" + + def __str__(self) -> str: + """ + Return ANSI color sequence instead of enum name + This is helpful when using an RgbBg in an f-string or format() call + e.g. my_str = f"{RgbBg(100, 255, 27)}hello{Bg.RESET}" + """ + return self._sequence + +# TODO: Remove this PyShadowingNames usage when deprecated fg and bg classes are removed. # noinspection PyShadowingNames def style( text: Any, *, - fg: Union[str, fg] = '', - bg: Union[str, bg] = '', - bold: bool = False, - dim: bool = False, - underline: bool = False, + fg: Optional[FgColor] = None, + bg: Optional[BgColor] = None, + bold: Optional[bool] = None, + dim: Optional[bool] = None, + italic: Optional[bool] = None, + overline: Optional[bool] = None, + strikethrough: Optional[bool] = None, + underline: Optional[bool] = None, ) -> str: """ Apply ANSI colors and/or styles to a string and return it. @@ -282,59 +960,74 @@ def style( to undo whatever styling was done at the beginning. :param text: text to format (anything convertible to a str) - :param fg: foreground color. Relies on `fg_lookup()` to retrieve ANSI escape based on name or enum. + :param fg: foreground color provided as any subclass of FgColor (e.g. Fg, EightBitFg, RgbFg) Defaults to no color. - :param bg: background color. Relies on `bg_lookup()` to retrieve ANSI escape based on name or enum. + :param bg: foreground color provided as any subclass of BgColor (e.g. Bg, EightBitBg, RgbBg) Defaults to no color. - :param bold: apply the bold style if True. Can be combined with dim. Defaults to False. - :param dim: apply the dim style if True. Can be combined with bold. Defaults to False. + :param bold: apply the bold style if True. Defaults to False. + :param dim: apply the dim style if True. Defaults to False. + :param italic: apply the italic style if True. Defaults to False. + :param overline: apply the overline style if True. Defaults to False. + :param strikethrough: apply the strikethrough style if True. Defaults to False. :param underline: apply the underline style if True. Defaults to False. :return: the stylized string """ # List of strings that add style - additions: List[str] = [] + additions: List[AnsiSequence] = [] # List of strings that remove style - removals: List[str] = [] + removals: List[AnsiSequence] = [] # Convert the text object into a string if it isn't already one text_formatted = str(text) # Process the style settings - if fg: - additions.append(fg_lookup(fg)) - removals.append(FG_RESET) + if fg is not None: + additions.append(fg) + removals.append(Fg.RESET) - if bg: - additions.append(bg_lookup(bg)) - removals.append(BG_RESET) + if bg is not None: + additions.append(bg) + removals.append(Bg.RESET) if bold: - additions.append(INTENSITY_BRIGHT) - removals.append(INTENSITY_NORMAL) + additions.append(TextStyle.INTENSITY_BOLD) + removals.append(TextStyle.INTENSITY_NORMAL) if dim: - additions.append(INTENSITY_DIM) - removals.append(INTENSITY_NORMAL) + additions.append(TextStyle.INTENSITY_DIM) + removals.append(TextStyle.INTENSITY_NORMAL) + + if italic: + additions.append(TextStyle.ITALIC_ENABLE) + removals.append(TextStyle.ITALIC_DISABLE) + + if overline: + additions.append(TextStyle.OVERLINE_ENABLE) + removals.append(TextStyle.OVERLINE_DISABLE) + + if strikethrough: + additions.append(TextStyle.STRIKETHROUGH_ENABLE) + removals.append(TextStyle.STRIKETHROUGH_DISABLE) if underline: - additions.append(UNDERLINE_ENABLE) - removals.append(UNDERLINE_DISABLE) + additions.append(TextStyle.UNDERLINE_ENABLE) + removals.append(TextStyle.UNDERLINE_DISABLE) # Combine the ANSI style sequences with the text - return "".join(additions) + text_formatted + "".join(removals) + return "".join(map(str, additions)) + text_formatted + "".join(map(str, removals)) # Default styles for printing strings of various types. # These can be altered to suit an application's needs and only need to be a # function with the following structure: func(str) -> str -style_success = functools.partial(style, fg=fg.green) +style_success = functools.partial(style, fg=Fg.GREEN) """Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text to signify success""" -style_warning = functools.partial(style, fg=fg.bright_yellow) +style_warning = functools.partial(style, fg=Fg.LIGHT_YELLOW) """Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text to signify a warning""" -style_error = functools.partial(style, fg=fg.bright_red) +style_error = functools.partial(style, fg=Fg.LIGHT_RED) """Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text to signify an error""" @@ -348,9 +1041,6 @@ def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_off :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. """ - from colorama import ( - Cursor, - ) # Split the prompt lines since it can contain newline characters. prompt_lines = prompt.splitlines() @@ -385,20 +1075,76 @@ def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_off # 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) + terminal_str += (clear_line() + Cursor.UP(1)) * (total_lines - 1) # Clear the first prompt line - terminal_str += colorama.ansi.clear_line() + terminal_str += 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. +#################################################################################### +# The following classes are deprecated. +#################################################################################### +# noinspection PyPep8Naming +class fg(FgColor, Enum): + """Deprecated Enum class for foreground colors. Use Fg instead.""" + + black = 30 + red = 31 + green = 32 + yellow = 33 + blue = 34 + magenta = 35 + cyan = 36 + white = 37 + reset = 39 + bright_black = 90 + bright_red = 91 + bright_green = 92 + bright_yellow = 93 + bright_blue = 94 + bright_magenta = 95 + bright_cyan = 96 + bright_white = 97 - :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 cast(str, colorama.ansi.set_title(title)) + def __str__(self) -> str: + """ + Return ANSI color sequence instead of enum name + This is helpful when using an fg in an f-string or format() call + e.g. my_str = f"{fg.blue}hello{fg.reset}" + """ + return f"{CSI}{self.value}m" + + +# noinspection PyPep8Naming +class bg(BgColor, Enum): + """Deprecated Enum class for background colors. Use Bg instead.""" + + black = 40 + red = 41 + green = 42 + yellow = 43 + blue = 44 + magenta = 45 + cyan = 46 + white = 47 + reset = 49 + bright_black = 100 + bright_red = 101 + bright_green = 102 + bright_yellow = 103 + bright_blue = 104 + bright_magenta = 105 + bright_cyan = 106 + bright_white = 107 + + def __str__(self) -> str: + """ + Return ANSI color sequence instead of enum name + This is helpful when using a bg in an f-string or format() call + e.g. my_str = f"{bg.black}hello{bg.reset}" + """ + return f"{CSI}{self.value}m" |