Source code for console.windows

# -*- coding: future_fstrings -*-
'''
    .. console - Comprehensive utility library for ANSI terminals.
    .. © 2018, Mike Miller - Released under the LGPL, version 3+.

    Module for traditional Windows API crud (though not WSL).
    Typically, it is not necessary to use this module directly;
    the detection module is preferred.

    https://docs.microsoft.com/en-us/windows/console/console-reference
'''
import sys
import logging
try:
    from ctypes import (byref, c_short, c_ushort, c_long, Structure, windll,
                        create_unicode_buffer)
    from ctypes.wintypes import DWORD, HANDLE

    kernel32 = windll.kernel32
    # https://stackoverflow.com/a/17998333/450917
    kernel32.GetStdHandle.restype = HANDLE

except (ValueError, NameError, ImportError):
    # handle Sphinx import under Linux errors
    c_short = c_ushort = c_long = Structure = kernel32 = DWORD = windll = object

import env

from . import color_tables
from .meta import defaults
from .constants import TermLevel


# winbase.h constants
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
STD_INPUT_HANDLE = -10
STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12

BUILD_ANSI_AVAIL = 10586  # Win10 TH2, Nov 2015
_mask_map = dict(
    foreground=0x000f,
    fg=0x000f,
    background=0x00f0,
    bg=0x00f0,
)
_win_to_ansi_offset_map = {
    # conhost, ansi
     0:   0,   # BLACK,  :  black
     1:   4,   # BLUE,   :  red
     2:   2,   # GREEN,  :  green
     3:   6,   # CYAN,   :  yellow
     4:   1,   # RED,    :  blue
     5:   5,   # MAGENTA :  magenta/purple
     6:   3,   # YELLOW  :  cyan,
     7:   7,   # GREY,   :  gray

     8:   8,   # BLACK,  :  light black
     9:  12,   # BLUE,   :  light red
    10:  10,   # GREEN,  :  light green
    11:  14,   # CYAN,   :  light yellow
    12:   9,   # RED,    :  light blue
    13:  13,   # MAGENTA :  light magenta
    14:  11,   # YELLOW  :  light cyan
    15:  15,   # GREY,   :  light white
}

log = logging.getLogger(__name__)


class _COORD(Structure):
    ''' Struct from wincon.h. '''
    _fields_ = [
        ('X', c_short),
        ('Y', c_short),
    ]


class _SMALL_RECT(Structure):
    ''' Struct from wincon.h. '''
    _fields_ = [
        ('Left', c_short),
        ('Top', c_short),
        ('Right', c_short),
        ('Bottom', c_short),
    ]


[docs]class CONSOLE_SCREEN_BUFFER_INFO(Structure): ''' Struct from wincon.h. ''' _fields_ = [ ('dwSize', _COORD), ('dwCursorPosition', _COORD), ('wAttributes', c_ushort), ('srWindow', _SMALL_RECT), ('dwMaximumWindowSize', _COORD), ]
[docs]def add_os_sysexits(): ''' Make the sysexit.h exit-status constants available under Windows. ''' import os os.EX_OK = 0 # successful termination # os.EX__BASE = 64 # base value for error messages os.EX_USAGE = 64 # command line usage error os.EX_DATAERR = 65 # data format error os.EX_NOINPUT = 66 # cannot open input os.EX_NOUSER = 67 # addressee unknown os.EX_NOHOST = 68 # host name unknown os.EX_UNAVAILABLE = 69 # service unavailable os.EX_SOFTWARE = 70 # internal software error os.EX_OSERR = 71 # system error (e.g., can't fork) os.EX_OSFILE = 72 # critical OS file missing os.EX_CANTCREAT = 73 # can't create (user) output file os.EX_IOERR = 74 # input/output error os.EX_TEMPFAIL = 75 # temp failure; user is invited to retry os.EX_PROTOCOL = 76 # remote error in protocol os.EX_NOPERM = 77 # permission denied os.EX_CONFIG = 78 # configuration error
# EX__MAX = 78 # maximum listed value
[docs]def cls(): ''' Clear (reset) the console. ''' # Clumsy but works - Win32 API takes 50 lines of code # and manually fills entire screen with spaces :/ # https://docs.microsoft.com/en-us/windows/console/clearing-the-screen # https://github.com/tartley/colorama/blob/master/colorama/winterm.py#L111 from subprocess import call call('cls', shell=True)
[docs]def detect_terminal_level(basic_palette=None): ''' Returns whether we think the terminal supports basic, extended, or direct color; None if not able to tell. Windows variant. Returns: level: None or TermLevel member color_sep The extended color sequence separator character, i.e. ":" or ";". ''' ansicon = is_colorama = None _color_sep = env.PY_CONSOLE_COLOR_SEP or ';' # supports only ; for now level = TermLevel.DUMB TERM = env.TERM.value or '' # shortcut if TERM == 'alacritty': # try to alleviate issue #8 newfangled = True else: newfangled = is_ansi_capable() and all(enable_vt_processing()) if newfangled: level = TermLevel.ANSI_DIRECT else: is_colorama = is_colorama_installed() ansicon = env.ANSICON.value if is_colorama or TERM.startswith('xterm'): level = TermLevel.ANSI_BASIC # upgrades if ansicon or TERM.endswith('-256color'): level = TermLevel.ANSI_EXTENDED if env.COLORTERM in ('truecolor', '24bit') or TERM == 'cygwin': level = TermLevel.ANSI_DIRECT log.debug( f'Term support: {level.name!r} (nt, TERM={TERM!r}, ' f'COLORTERM={env.COLORTERM.value!r}, ANSICON={ansicon!r}, ' f'colorama={is_colorama}, color_sep={_color_sep!r}) ' ) return level, _color_sep
[docs]def detect_unicode_support(codepage='cp65001'): # aka utf8 ''' Return whether unicode/utf8 is supported by the console/terminal. ''' result = None if get_code_page() == codepage: result = True return result
def _find_basic_palette_from_os(): ''' Find the platform-dependent 16-color basic palette—Windows version. This is used for "downgrading to the nearest color" support. ''' if sys.getwindowsversion()[2] >= 16299: # new palette after Win10 1709 FCU pal_name = 'cmd_1709' basic_palette = color_tables.cmd1709_palette4 else: pal_name = 'cmd_legacy' basic_palette = color_tables.cmd_palette4 return pal_name, basic_palette
[docs]def enable_vt_processing(): ''' What it says on the tin. - https://docs.microsoft.com/en-us/windows/console/setconsolemode #ENABLE_VIRTUAL_TERMINAL_PROCESSING - https://stackoverflow.com/q/36760127/450917 Returns: Tuple of status codes from SetConsoleMode for (stdout, stderr). ''' results = [] for stream in (STD_OUTPUT_HANDLE, STD_ERROR_HANDLE): handle = kernel32.GetStdHandle(stream) # get current mode mode = DWORD() if not kernel32.GetConsoleMode(handle, byref(mode)): break # check if not set, then set if (mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0: results.append( kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) ) else: results.append('Already Enabled') results = tuple(results) log.debug('%s %s', all(results), results) return results
[docs]def is_ansi_capable(): ''' Check to see whether this version of Windows is recent enough to support "ANSI VT"" processing. ''' try: CURRENT_VERS = sys.getwindowsversion()[:3] if CURRENT_VERS[2] > BUILD_ANSI_AVAIL: result = True else: result = False log.debug('%s (Windows version: %s > %s)', result, CURRENT_VERS, BUILD_ANSI_AVAIL) return result except AttributeError: pass
[docs]def is_colorama_installed(stream=sys.stdout): ''' Detect if the colorama stream wrapper has been initialized. ''' result = None try: import colorama if isinstance(stream, colorama.ansitowin32.StreamWrapper): result = True else: result = False except ImportError: pass log.debug('%s', result) return result
[docs]def get_code_page(): ''' Return the code page for this console/terminal instance. ''' from locale import getpreferredencoding return getpreferredencoding()
[docs]def get_color(name, number=None, timeout=None): ''' Query the default terminal for colors, etc. Arguments: name: str - one of ('foreground', 'fg', 'background', 'bg') number: compatibility only timeout: compatibility only Returns: tuple[str]:  A tuple of four-digit hex strings after parsing, the last two digits are the least significant and can be chopped when needed: ``('DEAD', 'BEEF', 'CAFE')`` If an error occurs during retrieval or parsing, the tuple will be empty. Examples: >>> get_color('bg') ... ('0000', '0000', '0000') >>> get_color('index', 2) # second color in indexed ... ('4e4d', '9a9a', '0605') # palette, 2 aka 32 in basic Notes: On Windows, only able to find palette defaults, which may be different if they were customized. ''' color = () if name != 'index': # also applies to Windows Terminal color_id = get_color_id(name) if sys.getwindowsversion()[2] > 16299: # Win10 FCU, new palette basic_palette = color_tables.cmd1709_palette4 else: basic_palette = color_tables.cmd_palette4 color = tuple(f'{i:02x}' for i in basic_palette[color_id]) # compat log.debug('%s %s color: %r', name, number, color) return color
[docs]def get_color_id(name, stream=STD_OUTPUT_HANDLE): ''' Returns current color ids of console. https://docs.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo Arguments: name: one of ('background', 'bg', 'foreground', 'fg') stream: Handle to stdout, stderr, etc. Returns: int: a color id offset from the conhost palette. Ids 8 and under are dark colors, above light. Id numbers are converted to ANSI order. ''' stream = kernel32.GetStdHandle(stream) csbi = CONSOLE_SCREEN_BUFFER_INFO() kernel32.GetConsoleScreenBufferInfo(stream, byref(csbi)) color_id = csbi.wAttributes & _mask_map.get(name, name) log.debug('color_id from conhost: %d', color_id) if name in ('background', 'bg'): color_id /= 16 # divide by 16 log.debug('color_id divided: %d', color_id) # convert to ansi order color_id = _win_to_ansi_offset_map.get(color_id, color_id) log.debug('ansi color_id: %d', color_id) return color_id
[docs]def get_position(stream=STD_OUTPUT_HANDLE): ''' Returns current position of cursor, starts at 1. ''' stream = kernel32.GetStdHandle(stream) csbi = CONSOLE_SCREEN_BUFFER_INFO() kernel32.GetConsoleScreenBufferInfo(stream, byref(csbi)) pos = csbi.dwCursorPosition # zero based, add ones for compatibility. return (pos.X + 1, pos.Y + 1)
[docs]def get_theme(timeout=defaults.READ_TIMEOUT): ''' Checks terminal for light/dark theme information. First checks for the environment variable COLORFGBG. Next, queries terminal, supported on Windows and xterm, perhaps others. See notes on get_color(). Returns: str, None: 'dark', 'light', None if no information. ''' theme = None log.debug('COLORFGBG: %s', env.COLORFGBG) if env.COLORFGBG: # support this on Windows or not? FG, _, BG = env.COLORFGBG.partition(';') theme = 'dark' if BG < '8' else 'light' # background wins else: color_id = get_color_id('background') theme = 'dark' if color_id < 8 else 'light' log.debug('%r', theme) return theme
[docs]def get_title(mode=None): ''' Returns console title string. https://docs.microsoft.com/en-us/windows/console/getconsoletitle ''' MAX_LEN = 256 buffer_ = create_unicode_buffer(MAX_LEN) kernel32.GetConsoleTitleW(buffer_, MAX_LEN) log.debug('%s', buffer_.value) return buffer_.value
[docs]def set_position(x, y, stream=STD_OUTPUT_HANDLE): ''' Sets current position of the cursor. ''' stream = kernel32.GetStdHandle(stream) value = x + (y << 16) kernel32.SetConsoleCursorPosition(stream, c_long(value))
[docs]def set_title(title): ''' Set the console title. ''' return kernel32.SetConsoleTitleW(title)