You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
210 lines
5.9 KiB
210 lines
5.9 KiB
8 months ago
|
"""Format colored console output."""
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
import os
|
||
|
import re
|
||
|
import shutil
|
||
|
import sys
|
||
|
from typing import TYPE_CHECKING
|
||
|
|
||
|
if TYPE_CHECKING:
|
||
|
from typing import Final
|
||
|
|
||
|
# fmt: off
|
||
|
def reset(text: str) -> str: ... # NoQA: E704
|
||
|
def bold(text: str) -> str: ... # NoQA: E704
|
||
|
def faint(text: str) -> str: ... # NoQA: E704
|
||
|
def standout(text: str) -> str: ... # NoQA: E704
|
||
|
def underline(text: str) -> str: ... # NoQA: E704
|
||
|
def blink(text: str) -> str: ... # NoQA: E704
|
||
|
|
||
|
def black(text: str) -> str: ... # NoQA: E704
|
||
|
def white(text: str) -> str: ... # NoQA: E704
|
||
|
def red(text: str) -> str: ... # NoQA: E704
|
||
|
def green(text: str) -> str: ... # NoQA: E704
|
||
|
def yellow(text: str) -> str: ... # NoQA: E704
|
||
|
def blue(text: str) -> str: ... # NoQA: E704
|
||
|
def fuchsia(text: str) -> str: ... # NoQA: E704
|
||
|
def teal(text: str) -> str: ... # NoQA: E704
|
||
|
|
||
|
def darkgray(text: str) -> str: ... # NoQA: E704
|
||
|
def lightgray(text: str) -> str: ... # NoQA: E704
|
||
|
def darkred(text: str) -> str: ... # NoQA: E704
|
||
|
def darkgreen(text: str) -> str: ... # NoQA: E704
|
||
|
def brown(text: str) -> str: ... # NoQA: E704
|
||
|
def darkblue(text: str) -> str: ... # NoQA: E704
|
||
|
def purple(text: str) -> str: ... # NoQA: E704
|
||
|
def turquoise(text: str) -> str: ... # NoQA: E704
|
||
|
# fmt: on
|
||
|
|
||
|
try:
|
||
|
# check if colorama is installed to support color on Windows
|
||
|
import colorama
|
||
|
except ImportError:
|
||
|
colorama = None
|
||
|
|
||
|
_CSI: Final[str] = re.escape('\x1b[') # 'ESC [': Control Sequence Introducer
|
||
|
|
||
|
# Pattern matching ANSI control sequences containing colors.
|
||
|
_ansi_color_re: Final[re.Pattern[str]] = re.compile(r'\x1b\[(?:\d+;){0,2}\d*m')
|
||
|
|
||
|
_ansi_re: Final[re.Pattern[str]] = re.compile(
|
||
|
_CSI
|
||
|
+ r"""
|
||
|
(?:
|
||
|
(?:\d+;){0,2}\d*m # ANSI color code ('m' is equivalent to '0m')
|
||
|
|
|
||
|
[012]?K # ANSI Erase in Line ('K' is equivalent to '0K')
|
||
|
)""",
|
||
|
re.VERBOSE | re.ASCII,
|
||
|
)
|
||
|
"""Pattern matching ANSI CSI colors (SGR) and erase line (EL) sequences.
|
||
|
|
||
|
See :func:`strip_escape_sequences` for details.
|
||
|
"""
|
||
|
|
||
|
codes: dict[str, str] = {}
|
||
|
|
||
|
|
||
|
def terminal_safe(s: str) -> str:
|
||
|
"""Safely encode a string for printing to the terminal."""
|
||
|
return s.encode('ascii', 'backslashreplace').decode('ascii')
|
||
|
|
||
|
|
||
|
def get_terminal_width() -> int:
|
||
|
"""Return the width of the terminal in columns."""
|
||
|
return shutil.get_terminal_size().columns - 1
|
||
|
|
||
|
|
||
|
_tw: int = get_terminal_width()
|
||
|
|
||
|
|
||
|
def term_width_line(text: str) -> str:
|
||
|
if not codes:
|
||
|
# if no coloring, don't output fancy backspaces
|
||
|
return text + '\n'
|
||
|
else:
|
||
|
# codes are not displayed, this must be taken into account
|
||
|
return text.ljust(_tw + len(text) - len(strip_escape_sequences(text))) + '\r'
|
||
|
|
||
|
|
||
|
def color_terminal() -> bool:
|
||
|
if 'NO_COLOR' in os.environ:
|
||
|
return False
|
||
|
if sys.platform == 'win32' and colorama is not None:
|
||
|
colorama.init()
|
||
|
return True
|
||
|
if 'FORCE_COLOR' in os.environ:
|
||
|
return True
|
||
|
if not hasattr(sys.stdout, 'isatty'):
|
||
|
return False
|
||
|
if not sys.stdout.isatty():
|
||
|
return False
|
||
|
if 'COLORTERM' in os.environ:
|
||
|
return True
|
||
|
term = os.environ.get('TERM', 'dumb').lower()
|
||
|
return term in ('xterm', 'linux') or 'color' in term
|
||
|
|
||
|
|
||
|
def nocolor() -> None:
|
||
|
if sys.platform == 'win32' and colorama is not None:
|
||
|
colorama.deinit()
|
||
|
codes.clear()
|
||
|
|
||
|
|
||
|
def coloron() -> None:
|
||
|
codes.update(_orig_codes)
|
||
|
|
||
|
|
||
|
def colorize(name: str, text: str, input_mode: bool = False) -> str:
|
||
|
def escseq(name: str) -> str:
|
||
|
# Wrap escape sequence with ``\1`` and ``\2`` to let readline know
|
||
|
# it is non-printable characters
|
||
|
# ref: https://tiswww.case.edu/php/chet/readline/readline.html
|
||
|
#
|
||
|
# Note: This hack does not work well in Windows (see #5059)
|
||
|
escape = codes.get(name, '')
|
||
|
if input_mode and escape and sys.platform != 'win32':
|
||
|
return '\1' + escape + '\2'
|
||
|
else:
|
||
|
return escape
|
||
|
|
||
|
return escseq(name) + text + escseq('reset')
|
||
|
|
||
|
|
||
|
def strip_colors(s: str) -> str:
|
||
|
"""Remove the ANSI color codes in a string *s*.
|
||
|
|
||
|
.. caution::
|
||
|
|
||
|
This function is not meant to be used in production and should only
|
||
|
be used for testing Sphinx's output messages.
|
||
|
|
||
|
.. seealso:: :func:`strip_escape_sequences`
|
||
|
"""
|
||
|
return _ansi_color_re.sub('', s)
|
||
|
|
||
|
|
||
|
def strip_escape_sequences(text: str, /) -> str:
|
||
|
r"""Remove the ANSI CSI colors and "erase in line" sequences.
|
||
|
|
||
|
Other `escape sequences `__ (e.g., VT100-specific functions) are not
|
||
|
supported and only control sequences *natively* known to Sphinx (i.e.,
|
||
|
colors declared in this module and "erase entire line" (``'\x1b[2K'``))
|
||
|
are eliminated by this function.
|
||
|
|
||
|
.. caution::
|
||
|
|
||
|
This function is not meant to be used in production and should only
|
||
|
be used for testing Sphinx's output messages that were not tempered
|
||
|
with by third-party extensions.
|
||
|
|
||
|
.. versionadded:: 7.3
|
||
|
|
||
|
This function is added as an *experimental* feature.
|
||
|
|
||
|
__ https://en.wikipedia.org/wiki/ANSI_escape_code
|
||
|
"""
|
||
|
return _ansi_re.sub('', text)
|
||
|
|
||
|
|
||
|
def create_color_func(name: str) -> None:
|
||
|
def inner(text: str) -> str:
|
||
|
return colorize(name, text)
|
||
|
|
||
|
globals()[name] = inner
|
||
|
|
||
|
|
||
|
_attrs = {
|
||
|
'reset': '39;49;00m',
|
||
|
'bold': '01m',
|
||
|
'faint': '02m',
|
||
|
'standout': '03m',
|
||
|
'underline': '04m',
|
||
|
'blink': '05m',
|
||
|
}
|
||
|
|
||
|
for __name, __value in _attrs.items():
|
||
|
codes[__name] = '\x1b[' + __value
|
||
|
|
||
|
_colors = [
|
||
|
('black', 'darkgray'),
|
||
|
('darkred', 'red'),
|
||
|
('darkgreen', 'green'),
|
||
|
('brown', 'yellow'),
|
||
|
('darkblue', 'blue'),
|
||
|
('purple', 'fuchsia'),
|
||
|
('turquoise', 'teal'),
|
||
|
('lightgray', 'white'),
|
||
|
]
|
||
|
|
||
|
for __i, (__dark, __light) in enumerate(_colors, 30):
|
||
|
codes[__dark] = '\x1b[%im' % __i
|
||
|
codes[__light] = '\x1b[%im' % (__i + 60)
|
||
|
|
||
|
_orig_codes = codes.copy()
|
||
|
|
||
|
for _name in codes:
|
||
|
create_color_func(_name)
|