Source code for efro.terminal
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to terminal IO."""
from __future__ import annotations
import sys
import os
from enum import Enum, unique
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any, ClassVar
[docs]
@unique
class TerminalColor(Enum):
"""Color codes for printing to terminals.
Generally the Clr class should be used when incorporating color into
terminal output, as it handles non-color-supporting terminals/etc.
"""
# Styles
RESET = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
INVERSE = '\033[7m'
# Normal foreground colors
BLACK = '\033[30m'
RED = '\033[31m'
GREEN = '\033[32m'
YELLOW = '\033[33m'
BLUE = '\033[34m'
MAGENTA = '\033[35m'
CYAN = '\033[36m'
WHITE = '\033[37m'
# Normal background colors.
BG_BLACK = '\033[40m'
BG_RED = '\033[41m'
BG_GREEN = '\033[42m'
BG_YELLOW = '\033[43m'
BG_BLUE = '\033[44m'
BG_MAGENTA = '\033[45m'
BG_CYAN = '\033[46m'
BG_WHITE = '\033[47m'
# Strong foreground colors
STRONG_BLACK = '\033[90m'
STRONG_RED = '\033[91m'
STRONG_GREEN = '\033[92m'
STRONG_YELLOW = '\033[93m'
STRONG_BLUE = '\033[94m'
STRONG_MAGENTA = '\033[95m'
STRONG_CYAN = '\033[96m'
STRONG_WHITE = '\033[97m'
# Strong background colors.
STRONG_BG_BLACK = '\033[100m'
STRONG_BG_RED = '\033[101m'
STRONG_BG_GREEN = '\033[102m'
STRONG_BG_YELLOW = '\033[103m'
STRONG_BG_BLUE = '\033[104m'
STRONG_BG_MAGENTA = '\033[105m'
STRONG_BG_CYAN = '\033[106m'
STRONG_BG_WHITE = '\033[107m'
def _default_color_enabled() -> bool:
"""Return whether we enable ANSI color codes by default."""
import platform
# If our stdout is not attached to a terminal, go with no-color.
assert sys.__stdout__ is not None
if not sys.__stdout__.isatty():
return False
termenv = os.environ.get('TERM')
# If TERM is unset, don't attempt color (this is currently the case
# in xcode).
if termenv is None:
return False
# A common way to say the terminal can't do fancy stuff like color.
if termenv == 'dumb':
return False
# On windows, try to enable ANSI color mode.
if platform.system() == 'Windows':
return _windows_enable_color()
# We seem to be a terminal with color support; let's do it!
return True
def _windows_enable_color() -> bool:
"""Attempt to enable ANSI color on windows terminal; return success."""
# pylint: disable=invalid-name, import-error, undefined-variable
# Pulled from: https://bugs.python.org/issue30075
import msvcrt
import ctypes
from ctypes import wintypes
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) # type: ignore
ERROR_INVALID_PARAMETER = 0x0057
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
def _check_bool(result: Any, _func: Any, args: Any) -> Any:
if not result:
raise ctypes.WinError(ctypes.get_last_error()) # type: ignore
return args
LPDWORD = ctypes.POINTER(wintypes.DWORD)
kernel32.GetConsoleMode.errcheck = _check_bool
kernel32.GetConsoleMode.argtypes = (wintypes.HANDLE, LPDWORD)
kernel32.SetConsoleMode.errcheck = _check_bool
kernel32.SetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.DWORD)
def set_conout_mode(new_mode: int, mask: int = 0xFFFFFFFF) -> int:
# don't assume StandardOutput is a console.
# open CONOUT$ instead
fdout = os.open('CONOUT$', os.O_RDWR)
try:
hout = msvcrt.get_osfhandle(fdout) # type: ignore
# pylint: disable=useless-suppression
# pylint: disable=no-value-for-parameter
old_mode = wintypes.DWORD()
# pylint: enable=useless-suppression
kernel32.GetConsoleMode(hout, ctypes.byref(old_mode))
mode = (new_mode & mask) | (old_mode.value & ~mask)
kernel32.SetConsoleMode(hout, mode)
return old_mode.value
finally:
os.close(fdout)
def enable_vt_mode() -> int:
mode = mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING
try:
return set_conout_mode(mode, mask)
except WindowsError as exc: # type: ignore
if exc.winerror == ERROR_INVALID_PARAMETER:
raise NotImplementedError from exc
raise
try:
enable_vt_mode()
return True
except NotImplementedError:
return False
[docs]
class ClrBase:
"""Base class for color convenience class."""
RST: ClassVar[str]
BLD: ClassVar[str]
UND: ClassVar[str]
INV: ClassVar[str]
# Normal foreground colors
BLK: ClassVar[str]
RED: ClassVar[str]
GRN: ClassVar[str]
YLW: ClassVar[str]
BLU: ClassVar[str]
MAG: ClassVar[str]
CYN: ClassVar[str]
WHT: ClassVar[str]
# Normal background colors.
BBLK: ClassVar[str]
BRED: ClassVar[str]
BGRN: ClassVar[str]
BYLW: ClassVar[str]
BBLU: ClassVar[str]
BMAG: ClassVar[str]
BCYN: ClassVar[str]
BWHT: ClassVar[str]
# Strong foreground colors
SBLK: ClassVar[str]
SRED: ClassVar[str]
SGRN: ClassVar[str]
SYLW: ClassVar[str]
SBLU: ClassVar[str]
SMAG: ClassVar[str]
SCYN: ClassVar[str]
SWHT: ClassVar[str]
# Strong background colors.
SBBLK: ClassVar[str]
SBRED: ClassVar[str]
SBGRN: ClassVar[str]
SBYLW: ClassVar[str]
SBBLU: ClassVar[str]
SBMAG: ClassVar[str]
SBCYN: ClassVar[str]
SBWHT: ClassVar[str]
[docs]
class ClrAlways(ClrBase):
"""Convenience class for color terminal output.
This version has colors always enabled. Generally you should use Clr which
points to the correct enabled/disabled class depending on the environment.
"""
color_enabled = True
# Styles
RST = TerminalColor.RESET.value
BLD = TerminalColor.BOLD.value
UND = TerminalColor.UNDERLINE.value
INV = TerminalColor.INVERSE.value
# Normal foreground colors
BLK = TerminalColor.BLACK.value
RED = TerminalColor.RED.value
GRN = TerminalColor.GREEN.value
YLW = TerminalColor.YELLOW.value
BLU = TerminalColor.BLUE.value
MAG = TerminalColor.MAGENTA.value
CYN = TerminalColor.CYAN.value
WHT = TerminalColor.WHITE.value
# Normal background colors.
BBLK = TerminalColor.BG_BLACK.value
BRED = TerminalColor.BG_RED.value
BGRN = TerminalColor.BG_GREEN.value
BYLW = TerminalColor.BG_YELLOW.value
BBLU = TerminalColor.BG_BLUE.value
BMAG = TerminalColor.BG_MAGENTA.value
BCYN = TerminalColor.BG_CYAN.value
BWHT = TerminalColor.BG_WHITE.value
# Strong foreground colors
SBLK = TerminalColor.STRONG_BLACK.value
SRED = TerminalColor.STRONG_RED.value
SGRN = TerminalColor.STRONG_GREEN.value
SYLW = TerminalColor.STRONG_YELLOW.value
SBLU = TerminalColor.STRONG_BLUE.value
SMAG = TerminalColor.STRONG_MAGENTA.value
SCYN = TerminalColor.STRONG_CYAN.value
SWHT = TerminalColor.STRONG_WHITE.value
# Strong background colors.
SBBLK = TerminalColor.STRONG_BG_BLACK.value
SBRED = TerminalColor.STRONG_BG_RED.value
SBGRN = TerminalColor.STRONG_BG_GREEN.value
SBYLW = TerminalColor.STRONG_BG_YELLOW.value
SBBLU = TerminalColor.STRONG_BG_BLUE.value
SBMAG = TerminalColor.STRONG_BG_MAGENTA.value
SBCYN = TerminalColor.STRONG_BG_CYAN.value
SBWHT = TerminalColor.STRONG_BG_WHITE.value
[docs]
class ClrNever(ClrBase):
"""Convenience class for color terminal output.
This version has colors disabled. Generally you should use Clr which
points to the correct enabled/disabled class depending on the environment.
"""
color_enabled = False
# Styles
RST = ''
BLD = ''
UND = ''
INV = ''
# Normal foreground colors
BLK = ''
RED = ''
GRN = ''
YLW = ''
BLU = ''
MAG = ''
CYN = ''
WHT = ''
# Normal background colors.
BBLK = ''
BRED = ''
BGRN = ''
BYLW = ''
BBLU = ''
BMAG = ''
BCYN = ''
BWHT = ''
# Strong foreground colors
SBLK = ''
SRED = ''
SGRN = ''
SYLW = ''
SBLU = ''
SMAG = ''
SCYN = ''
SWHT = ''
# Strong background colors.
SBBLK = ''
SBRED = ''
SBGRN = ''
SBYLW = ''
SBBLU = ''
SBMAG = ''
SBCYN = ''
SBWHT = ''
_envval = os.environ.get('EFRO_TERMCOLORS')
color_enabled: bool = (
True
if _envval == '1'
else False if _envval == '0' else _default_color_enabled()
)
Clr: type[ClrBase] = ClrAlways if color_enabled else ClrNever
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum