Initial commit: Tamigo CLI with Gitea Actions and global installation support
This commit is contained in:
338
venv/lib/python3.12/site-packages/wcwidth/sgr_state.py
Normal file
338
venv/lib/python3.12/site-packages/wcwidth/sgr_state.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
SGR (Select Graphic Rendition) state tracking for terminal escape sequences.
|
||||
|
||||
This module provides functions for tracking and propagating terminal styling (bold, italic, colors,
|
||||
etc.) via public API propagate_sgr(), and its dependent functions, cut() and wrap(). It only has
|
||||
attributes necessary to perform its functions, eg 'RED' and 'BLUE' attributes are not defined.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# std imports
|
||||
import re
|
||||
from enum import IntEnum
|
||||
|
||||
from typing import TYPE_CHECKING, Iterator, NamedTuple
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
class _SGR(IntEnum):
|
||||
"""
|
||||
SGR (Select Graphic Rendition) parameter codes.
|
||||
|
||||
References:
|
||||
- https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
- https://github.com/tehmaze/ansi/tree/master/ansi/colour
|
||||
"""
|
||||
|
||||
RESET = 0
|
||||
BOLD = 1
|
||||
DIM = 2
|
||||
ITALIC = 3
|
||||
UNDERLINE = 4
|
||||
BLINK = 5
|
||||
RAPID_BLINK = 6
|
||||
INVERSE = 7
|
||||
HIDDEN = 8
|
||||
STRIKETHROUGH = 9
|
||||
DOUBLE_UNDERLINE = 21
|
||||
BOLD_DIM_OFF = 22
|
||||
ITALIC_OFF = 23
|
||||
UNDERLINE_OFF = 24
|
||||
BLINK_OFF = 25
|
||||
INVERSE_OFF = 27
|
||||
HIDDEN_OFF = 28
|
||||
STRIKETHROUGH_OFF = 29
|
||||
FG_BLACK = 30
|
||||
FG_WHITE = 37
|
||||
FG_EXTENDED = 38
|
||||
FG_DEFAULT = 39
|
||||
BG_BLACK = 40
|
||||
BG_WHITE = 47
|
||||
BG_EXTENDED = 48
|
||||
BG_DEFAULT = 49
|
||||
FG_BRIGHT_BLACK = 90
|
||||
FG_BRIGHT_WHITE = 97
|
||||
BG_BRIGHT_BLACK = 100
|
||||
BG_BRIGHT_WHITE = 107
|
||||
|
||||
|
||||
# SGR sequence pattern: CSI followed by params (digits, semicolons, colons) ending with 'm'
|
||||
# Colons are used in ITU T.416 (ISO 8613-6) extended color format: 38:2::R:G:B
|
||||
# This colon format is less common than semicolon (38;2;R;G;B) but supported by kitty,
|
||||
# iTerm2, and newer VTE-based terminals.
|
||||
_SGR_PATTERN = re.compile(r'\x1b\[([\d;:]*)m')
|
||||
|
||||
# Fast path: quick check if any SGR sequence exists
|
||||
_SGR_QUICK_CHECK = re.compile(r'\x1b\[[\d;:]*m')
|
||||
|
||||
# Reset sequence
|
||||
_SGR_RESET = '\x1b[0m'
|
||||
|
||||
|
||||
class _SGRState(NamedTuple):
|
||||
"""
|
||||
Track active SGR terminal attributes by category (immutable).
|
||||
|
||||
:param bold: Bold attribute (SGR 1).
|
||||
:param dim: Dim/faint attribute (SGR 2).
|
||||
:param italic: Italic attribute (SGR 3).
|
||||
:param underline: Underline attribute (SGR 4).
|
||||
:param blink: Slow blink attribute (SGR 5).
|
||||
:param rapid_blink: Rapid blink attribute (SGR 6).
|
||||
:param inverse: Inverse/reverse attribute (SGR 7).
|
||||
:param hidden: Hidden/invisible attribute (SGR 8).
|
||||
:param strikethrough: Strikethrough attribute (SGR 9).
|
||||
:param double_underline: Double underline attribute (SGR 21).
|
||||
:param foreground: Foreground color as tuple of SGR params, or None for default.
|
||||
:param background: Background color as tuple of SGR params, or None for default.
|
||||
"""
|
||||
|
||||
bold: bool = False
|
||||
dim: bool = False
|
||||
italic: bool = False
|
||||
underline: bool = False
|
||||
blink: bool = False
|
||||
rapid_blink: bool = False
|
||||
inverse: bool = False
|
||||
hidden: bool = False
|
||||
strikethrough: bool = False
|
||||
double_underline: bool = False
|
||||
foreground: tuple[int, ...] | None = None
|
||||
background: tuple[int, ...] | None = None
|
||||
|
||||
|
||||
# Default state with no attributes set
|
||||
_SGR_STATE_DEFAULT = _SGRState()
|
||||
|
||||
|
||||
def _sgr_state_is_active(state: _SGRState) -> bool:
|
||||
"""
|
||||
Return True if any attributes are set.
|
||||
|
||||
:param state: The SGR state to check.
|
||||
:returns: True if any attribute differs from default.
|
||||
"""
|
||||
return (state.bold or state.dim or state.italic or state.underline
|
||||
or state.blink or state.rapid_blink or state.inverse or state.hidden
|
||||
or state.strikethrough or state.double_underline
|
||||
or state.foreground is not None or state.background is not None)
|
||||
|
||||
|
||||
def _sgr_state_to_sequence(state: _SGRState) -> str:
|
||||
"""
|
||||
Generate minimal SGR sequence to restore this state from reset.
|
||||
|
||||
:param state: The SGR state to convert.
|
||||
:returns: SGR escape sequence string, or empty string if no attributes set.
|
||||
"""
|
||||
if not _sgr_state_is_active(state):
|
||||
return ''
|
||||
|
||||
# Map boolean attributes to their SGR codes
|
||||
bool_attrs = [
|
||||
(state.bold, '1'), (state.dim, '2'), (state.italic, '3'),
|
||||
(state.underline, '4'), (state.blink, '5'), (state.rapid_blink, '6'),
|
||||
(state.inverse, '7'), (state.hidden, '8'), (state.strikethrough, '9'),
|
||||
(state.double_underline, '21'),
|
||||
]
|
||||
params = [code for active, code in bool_attrs if active]
|
||||
|
||||
# Add color params (already formatted as tuples)
|
||||
if state.foreground is not None:
|
||||
params.append(';'.join(str(p) for p in state.foreground))
|
||||
if state.background is not None:
|
||||
params.append(';'.join(str(p) for p in state.background))
|
||||
|
||||
return f'\x1b[{";".join(params)}m'
|
||||
|
||||
|
||||
def _parse_sgr_params(sequence: str) -> list[int | tuple[int, ...]]:
|
||||
r"""
|
||||
Parse SGR sequence and return list of parameter values.
|
||||
|
||||
Handles compound sequences like ``\x1b[1;31;4m`` -> [1, 31, 4].
|
||||
Empty params (e.g., ``\x1b[m``) are treated as [0] (reset).
|
||||
Colon-separated extended colors like ``\x1b[38:2::255:0:0m`` are returned
|
||||
as tuples: [(38, 2, 255, 0, 0)].
|
||||
|
||||
:param sequence: SGR escape sequence string.
|
||||
:returns: List of integer parameters or tuples for colon-separated colors.
|
||||
"""
|
||||
match = _SGR_PATTERN.match(sequence)
|
||||
if not match:
|
||||
return []
|
||||
params_str = match.group(1)
|
||||
if not params_str:
|
||||
return [0] # \x1b[m is equivalent to \x1b[0m
|
||||
result: list[int | tuple[int, ...]] = []
|
||||
for param in params_str.split(';'):
|
||||
if ':' in param:
|
||||
# Colon-separated extended color (ITU T.416 format)
|
||||
# e.g., "38:2::255:0:0" or "38:2:1:255:0:0" (with colorspace)
|
||||
parts = [int(p) if p else 0 for p in param.split(':')]
|
||||
result.append(tuple(parts))
|
||||
else:
|
||||
result.append(int(param) if param else 0)
|
||||
return result
|
||||
|
||||
|
||||
def _parse_extended_color(
|
||||
params: Iterator[int | tuple[int, ...]], base: int
|
||||
) -> tuple[int, ...] | None:
|
||||
"""
|
||||
Parse extended color (256-color or RGB) from parameter iterator.
|
||||
|
||||
:param params: Iterator of remaining SGR parameters (semicolon-separated format).
|
||||
:param base: Base code (38 for foreground, 48 for background).
|
||||
:returns: Color tuple like (38, 5, N) or (38, 2, R, G, B), or None if malformed.
|
||||
"""
|
||||
try:
|
||||
mode = next(params)
|
||||
if isinstance(mode, tuple):
|
||||
return None # Unexpected tuple, colon format handled separately
|
||||
if mode == 5: # 256-color
|
||||
n = next(params)
|
||||
if isinstance(n, tuple):
|
||||
return None
|
||||
return (int(base), 5, n)
|
||||
if mode == 2: # RGB
|
||||
r, g, b = next(params), next(params), next(params)
|
||||
if isinstance(r, tuple) or isinstance(g, tuple) or isinstance(b, tuple):
|
||||
return None
|
||||
return (int(base), 2, r, g, b)
|
||||
except StopIteration:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _sgr_state_update(state: _SGRState, sequence: str) -> _SGRState:
|
||||
# pylint: disable=too-many-branches,too-complex,too-many-statements
|
||||
# NOTE: When minimum Python version is 3.10+, this can be simplified using match/case.
|
||||
"""
|
||||
Parse SGR sequence and return new state with updates applied.
|
||||
|
||||
:param state: Current SGR state.
|
||||
:param sequence: SGR escape sequence string.
|
||||
:returns: New SGRState with updates applied.
|
||||
"""
|
||||
params_list = _parse_sgr_params(sequence)
|
||||
params = iter(params_list)
|
||||
for p in params:
|
||||
# Handle colon-separated extended colors (ITU T.416 format)
|
||||
if isinstance(p, tuple):
|
||||
if len(p) >= 2 and p[0] == _SGR.FG_EXTENDED:
|
||||
# Foreground: (38, 2, [colorspace,] R, G, B) or (38, 5, N)
|
||||
state = state._replace(foreground=p)
|
||||
elif len(p) >= 2 and p[0] == _SGR.BG_EXTENDED:
|
||||
# Background: (48, 2, [colorspace,] R, G, B) or (48, 5, N)
|
||||
state = state._replace(background=p)
|
||||
continue
|
||||
if p == _SGR.RESET:
|
||||
state = _SGR_STATE_DEFAULT
|
||||
# Attribute ON codes
|
||||
elif p == _SGR.BOLD:
|
||||
state = state._replace(bold=True)
|
||||
elif p == _SGR.DIM:
|
||||
state = state._replace(dim=True)
|
||||
elif p == _SGR.ITALIC:
|
||||
state = state._replace(italic=True)
|
||||
elif p == _SGR.UNDERLINE:
|
||||
state = state._replace(underline=True)
|
||||
elif p == _SGR.BLINK:
|
||||
state = state._replace(blink=True)
|
||||
elif p == _SGR.RAPID_BLINK:
|
||||
state = state._replace(rapid_blink=True)
|
||||
elif p == _SGR.INVERSE:
|
||||
state = state._replace(inverse=True)
|
||||
elif p == _SGR.HIDDEN:
|
||||
state = state._replace(hidden=True)
|
||||
elif p == _SGR.STRIKETHROUGH:
|
||||
state = state._replace(strikethrough=True)
|
||||
elif p == _SGR.DOUBLE_UNDERLINE:
|
||||
state = state._replace(double_underline=True)
|
||||
# Attribute OFF codes
|
||||
elif p == _SGR.BOLD_DIM_OFF:
|
||||
state = state._replace(bold=False, dim=False)
|
||||
elif p == _SGR.ITALIC_OFF:
|
||||
state = state._replace(italic=False)
|
||||
elif p == _SGR.UNDERLINE_OFF:
|
||||
state = state._replace(underline=False, double_underline=False)
|
||||
elif p == _SGR.BLINK_OFF:
|
||||
state = state._replace(blink=False, rapid_blink=False)
|
||||
elif p == _SGR.INVERSE_OFF:
|
||||
state = state._replace(inverse=False)
|
||||
elif p == _SGR.HIDDEN_OFF:
|
||||
state = state._replace(hidden=False)
|
||||
elif p == _SGR.STRIKETHROUGH_OFF:
|
||||
state = state._replace(strikethrough=False)
|
||||
# Basic colors (30-37, 40-47 standard; 90-97, 100-107 bright)
|
||||
elif (_SGR.FG_BLACK <= p <= _SGR.FG_WHITE
|
||||
or _SGR.FG_BRIGHT_BLACK <= p <= _SGR.FG_BRIGHT_WHITE):
|
||||
state = state._replace(foreground=(p,))
|
||||
elif (_SGR.BG_BLACK <= p <= _SGR.BG_WHITE
|
||||
or _SGR.BG_BRIGHT_BLACK <= p <= _SGR.BG_BRIGHT_WHITE):
|
||||
state = state._replace(background=(p,))
|
||||
elif p == _SGR.FG_DEFAULT:
|
||||
state = state._replace(foreground=None)
|
||||
elif p == _SGR.BG_DEFAULT:
|
||||
state = state._replace(background=None)
|
||||
# Extended colors (semicolon-separated format)
|
||||
elif p == _SGR.FG_EXTENDED:
|
||||
if color := _parse_extended_color(params, _SGR.FG_EXTENDED):
|
||||
state = state._replace(foreground=color)
|
||||
elif p == _SGR.BG_EXTENDED:
|
||||
if color := _parse_extended_color(params, _SGR.BG_EXTENDED):
|
||||
state = state._replace(background=color)
|
||||
return state
|
||||
|
||||
|
||||
def propagate_sgr(lines: Sequence[str]) -> list[str]:
|
||||
r"""
|
||||
Propagate SGR codes across wrapped lines.
|
||||
|
||||
When text with SGR styling is wrapped across multiple lines, each line
|
||||
needs to be self-contained for proper display. This function:
|
||||
|
||||
- Ends each line with ``\x1b[0m`` if styles are active (prevents bleeding)
|
||||
- Starts each subsequent line with the active style restored
|
||||
|
||||
:param lines: List of text lines, possibly containing SGR sequences.
|
||||
:returns: List of lines with SGR codes propagated.
|
||||
|
||||
Example::
|
||||
|
||||
>>> propagate_sgr(['\x1b[31mhello', 'world\x1b[0m'])
|
||||
['\x1b[31mhello\x1b[0m', '\x1b[31mworld\x1b[0m']
|
||||
|
||||
This is useful in cases of making special editors and viewers, and is used for the
|
||||
default modes (propagate_sgr=True) of :func:`wcwidth.width` and :func:`wcwidth.clip`.
|
||||
|
||||
When wrapping and clipping text containing SGR sequences, maybe a previous line enabled the BLUE
|
||||
color--if we are viewing *only* the line following, we would want the carry over the BLUE color,
|
||||
and all lines with sequences should end with terminating reset (``\x1b[0m``).
|
||||
"""
|
||||
# Fast path: check if any line contains SGR sequences
|
||||
if not any(_SGR_QUICK_CHECK.search(line) for line in lines) or not lines:
|
||||
return list(lines)
|
||||
|
||||
result: list[str] = []
|
||||
state = _SGR_STATE_DEFAULT
|
||||
|
||||
for line in lines:
|
||||
# Prefix with restoration sequence if state is active
|
||||
prefix = _sgr_state_to_sequence(state)
|
||||
|
||||
# Update state by processing all SGR sequences in this line
|
||||
for match in _SGR_PATTERN.finditer(line):
|
||||
state = _sgr_state_update(state, match.group())
|
||||
|
||||
# Build output line
|
||||
output_line = prefix + line if prefix else line
|
||||
if _sgr_state_is_active(state):
|
||||
output_line = output_line + _SGR_RESET
|
||||
|
||||
result.append(output_line)
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user