Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
sgr_state.py340 linesDownload Raw Back to wcwidth
1"""2SGR (Select Graphic Rendition) state tracking for terminal escape sequences.3 4This module provides functions for tracking and propagating terminal styling (bold, italic, colors,5etc.) via public API propagate_sgr(), and its dependent functions, cut() and wrap(). It only has6attributes necessary to perform its functions, eg 'RED' and 'BLUE' attributes are not defined.7"""8 9from __future__ import annotations10 11# std imports12import re13from enum import IntEnum14 15from typing import TYPE_CHECKING, Iterator, NamedTuple16 17if TYPE_CHECKING:  # pragma: no cover18    from typing import Sequence19 20 21class _SGR(IntEnum):22    """23    SGR (Select Graphic Rendition) parameter codes.24 25    References:26    - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html27    - https://github.com/tehmaze/ansi/tree/master/ansi/colour28    """29 30    RESET = 031    BOLD = 132    DIM = 233    ITALIC = 334    UNDERLINE = 435    BLINK = 536    RAPID_BLINK = 637    INVERSE = 738    HIDDEN = 839    STRIKETHROUGH = 940    DOUBLE_UNDERLINE = 2141    BOLD_DIM_OFF = 2242    ITALIC_OFF = 2343    UNDERLINE_OFF = 2444    BLINK_OFF = 2545    INVERSE_OFF = 2746    HIDDEN_OFF = 2847    STRIKETHROUGH_OFF = 2948    FG_BLACK = 3049    FG_WHITE = 3750    FG_EXTENDED = 3851    FG_DEFAULT = 3952    BG_BLACK = 4053    BG_WHITE = 4754    BG_EXTENDED = 4855    BG_DEFAULT = 4956    FG_BRIGHT_BLACK = 9057    FG_BRIGHT_WHITE = 9758    BG_BRIGHT_BLACK = 10059    BG_BRIGHT_WHITE = 10760 61 62# SGR sequence pattern: CSI followed by params (digits, semicolons, colons) ending with 'm'63# Colons are used in ITU T.416 (ISO 8613-6) extended color format: 38:2::R:G:B64# This colon format is less common than semicolon (38;2;R;G;B) but supported by kitty,65# iTerm2, and newer VTE-based terminals.66_SGR_PATTERN = re.compile(r'\x1b\[([\d;:]*)m')67 68# Fast path: quick check if any SGR sequence exists69_SGR_QUICK_CHECK = re.compile(r'\x1b\[[\d;:]*m')70 71# Reset sequence72_SGR_RESET = '\x1b[0m'73 74 75class _SGRState(NamedTuple):76    """77    Track active SGR terminal attributes by category (immutable).78 79    :param bold: Bold attribute (SGR 1).80    :param dim: Dim/faint attribute (SGR 2).81    :param italic: Italic attribute (SGR 3).82    :param underline: Underline attribute (SGR 4).83    :param blink: Slow blink attribute (SGR 5).84    :param rapid_blink: Rapid blink attribute (SGR 6).85    :param inverse: Inverse/reverse attribute (SGR 7).86    :param hidden: Hidden/invisible attribute (SGR 8).87    :param strikethrough: Strikethrough attribute (SGR 9).88    :param double_underline: Double underline attribute (SGR 21).89    :param foreground: Foreground color as tuple of SGR params, or None for default.90    :param background: Background color as tuple of SGR params, or None for default.91    """92 93    bold: bool = False94    dim: bool = False95    italic: bool = False96    underline: bool = False97    blink: bool = False98    rapid_blink: bool = False99    inverse: bool = False100    hidden: bool = False101    strikethrough: bool = False102    double_underline: bool = False103    foreground: tuple[int, ...] | None = None104    background: tuple[int, ...] | None = None105 106 107# Default state with no attributes set108_SGR_STATE_DEFAULT = _SGRState()109 110 111def _sgr_state_is_active(state: _SGRState) -> bool:112    """113    Return True if any attributes are set.114 115    :param state: The SGR state to check.116    :returns: True if any attribute differs from default.117    """118    return (state.bold or state.dim or state.italic or state.underline119            or state.blink or state.rapid_blink or state.inverse or state.hidden120            or state.strikethrough or state.double_underline121            or state.foreground is not None or state.background is not None)122 123 124def _sgr_state_to_sequence(state: _SGRState) -> str:125    """126    Generate minimal SGR sequence to restore this state from reset.127 128    :param state: The SGR state to convert.129    :returns: SGR escape sequence string, or empty string if no attributes set.130    """131    if not _sgr_state_is_active(state):132        return ''133 134    # Map boolean attributes to their SGR codes135    bool_attrs = [136        (state.bold, '1'), (state.dim, '2'), (state.italic, '3'),137        (state.underline, '4'), (state.blink, '5'), (state.rapid_blink, '6'),138        (state.inverse, '7'), (state.hidden, '8'), (state.strikethrough, '9'),139        (state.double_underline, '21'),140    ]141    params = [code for active, code in bool_attrs if active]142 143    # Add color params (already formatted as tuples)144    if state.foreground is not None:145        params.append(';'.join(str(p) for p in state.foreground))146    if state.background is not None:147        params.append(';'.join(str(p) for p in state.background))148 149    return f'\x1b[{";".join(params)}m'150 151 152def _parse_sgr_params(sequence: str) -> list[int | tuple[int, ...]]:153    r"""154    Parse SGR sequence and return list of parameter values.155 156    Handles compound sequences like ``\x1b[1;31;4m`` -> [1, 31, 4].157    Empty params (e.g., ``\x1b[m``) are treated as [0] (reset).158    Colon-separated extended colors like ``\x1b[38:2::255:0:0m`` are returned159    as tuples: [(38, 2, 255, 0, 0)].160 161    :param sequence: SGR escape sequence string.162    :returns: List of integer parameters or tuples for colon-separated colors.163    """164    match = _SGR_PATTERN.match(sequence)165    if not match:166        return []167    params_str = match.group(1)168    if not params_str:169        return [0]  # \x1b[m is equivalent to \x1b[0m170    result: list[int | tuple[int, ...]] = []171    for param in params_str.split(';'):172        if ':' in param:173            # Colon-separated extended color (ITU T.416 format)174            # e.g., "38:2::255:0:0" or "38:2:1:255:0:0" (with colorspace)175            parts = [int(p) if p else 0 for p in param.split(':')]176            result.append(tuple(parts))177        else:178            result.append(int(param) if param else 0)179    return result180 181 182def _parse_extended_color(183    params: Iterator[int | tuple[int, ...]], base: int184) -> tuple[int, ...] | None:185    """186    Parse extended color (256-color or RGB) from parameter iterator.187 188    :param params: Iterator of remaining SGR parameters (semicolon-separated format).189    :param base: Base code (38 for foreground, 48 for background).190    :returns: Color tuple like (38, 5, N) or (38, 2, R, G, B), or None if malformed.191    """192    try:193        mode = next(params)194        if isinstance(mode, tuple):195            return None  # Unexpected tuple, colon format handled separately196        if mode == 5:  # 256-color197            n = next(params)198            if isinstance(n, tuple):199                return None200            return (int(base), 5, n)201        if mode == 2:  # RGB202            r, g, b = next(params), next(params), next(params)203            if isinstance(r, tuple) or isinstance(g, tuple) or isinstance(b, tuple):204                return None205            return (int(base), 2, r, g, b)206    except StopIteration:207        pass208    return None209 210 211def _sgr_state_update(state: _SGRState, sequence: str) -> _SGRState:212    # pylint: disable=too-many-branches,too-complex,too-many-statements213    # NOTE: When minimum Python version is 3.10+, this can be simplified using match/case.214    """215    Parse SGR sequence and return new state with updates applied.216 217    :param state: Current SGR state.218    :param sequence: SGR escape sequence string.219    :returns: New SGRState with updates applied.220    """221    params_list = _parse_sgr_params(sequence)222    params = iter(params_list)223    for p in params:224        # Handle colon-separated extended colors (ITU T.416 format)225        if isinstance(p, tuple):226            if len(p) >= 2 and p[0] == _SGR.FG_EXTENDED:227                # Foreground: (38, 2, [colorspace,] R, G, B) or (38, 5, N)228                state = state._replace(foreground=p)229            elif len(p) >= 2 and p[0] == _SGR.BG_EXTENDED:230                # Background: (48, 2, [colorspace,] R, G, B) or (48, 5, N)231                state = state._replace(background=p)232            continue233        if p == _SGR.RESET:234            state = _SGR_STATE_DEFAULT235        # Attribute ON codes236        elif p == _SGR.BOLD:237            state = state._replace(bold=True)238        elif p == _SGR.DIM:239            state = state._replace(dim=True)240        elif p == _SGR.ITALIC:241            state = state._replace(italic=True)242        elif p == _SGR.UNDERLINE:243            state = state._replace(underline=True)244        elif p == _SGR.BLINK:245            state = state._replace(blink=True)246        elif p == _SGR.RAPID_BLINK:247            state = state._replace(rapid_blink=True)248        elif p == _SGR.INVERSE:249            state = state._replace(inverse=True)250        elif p == _SGR.HIDDEN:251            state = state._replace(hidden=True)252        elif p == _SGR.STRIKETHROUGH:253            state = state._replace(strikethrough=True)254        elif p == _SGR.DOUBLE_UNDERLINE:255            state = state._replace(double_underline=True)256        # Attribute OFF codes257        elif p == _SGR.BOLD_DIM_OFF:258            state = state._replace(bold=False, dim=False)259        elif p == _SGR.ITALIC_OFF:260            state = state._replace(italic=False)261        elif p == _SGR.UNDERLINE_OFF:262            state = state._replace(underline=False, double_underline=False)263        elif p == _SGR.BLINK_OFF:264            state = state._replace(blink=False, rapid_blink=False)265        elif p == _SGR.INVERSE_OFF:266            state = state._replace(inverse=False)267        elif p == _SGR.HIDDEN_OFF:268            state = state._replace(hidden=False)269        elif p == _SGR.STRIKETHROUGH_OFF:270            state = state._replace(strikethrough=False)271        # Basic colors (30-37, 40-47 standard; 90-97, 100-107 bright)272        elif (_SGR.FG_BLACK <= p <= _SGR.FG_WHITE273              or _SGR.FG_BRIGHT_BLACK <= p <= _SGR.FG_BRIGHT_WHITE):274            state = state._replace(foreground=(p,))275        elif (_SGR.BG_BLACK <= p <= _SGR.BG_WHITE276              or _SGR.BG_BRIGHT_BLACK <= p <= _SGR.BG_BRIGHT_WHITE):277            state = state._replace(background=(p,))278        elif p == _SGR.FG_DEFAULT:279            state = state._replace(foreground=None)280        elif p == _SGR.BG_DEFAULT:281            state = state._replace(background=None)282        # Extended colors (semicolon-separated format)283        elif p == _SGR.FG_EXTENDED:284            if color := _parse_extended_color(params, _SGR.FG_EXTENDED):285                state = state._replace(foreground=color)286        elif p == _SGR.BG_EXTENDED:287            if color := _parse_extended_color(params, _SGR.BG_EXTENDED):288                state = state._replace(background=color)289    return state290 291 292def propagate_sgr(lines: Sequence[str]) -> list[str]:293    r"""294    Propagate SGR codes across wrapped lines.295 296    When text with SGR styling is wrapped across multiple lines, each line297    needs to be self-contained for proper display. This function:298 299    - Ends each line with ``\x1b[0m`` if styles are active (prevents bleeding)300    - Starts each subsequent line with the active style restored301 302    :param lines: List of text lines, possibly containing SGR sequences.303    :returns: List of lines with SGR codes propagated.304 305    Example::306 307        >>> propagate_sgr(['\x1b[31mhello', 'world\x1b[0m'])308        ['\x1b[31mhello\x1b[0m', '\x1b[31mworld\x1b[0m']309 310    This is useful in cases of making special editors and viewers, and is used for the311    default modes (propagate_sgr=True) of :func:`wcwidth.wrap` and :func:`wcwidth.clip`.312 313    When wrapping and clipping text containing SGR sequences, maybe a previous line enabled the BLUE314    color--if we are viewing *only* the line following, we would want the carry over the BLUE color,315    and all lines with sequences should end with terminating reset (``\x1b[0m``).316    """317    # Fast path: check if any line contains SGR sequences318    if not any(_SGR_QUICK_CHECK.search(line) for line in lines) or not lines:319        return list(lines)320 321    result: list[str] = []322    state = _SGR_STATE_DEFAULT323 324    for line in lines:325        # Prefix with restoration sequence if state is active326        prefix = _sgr_state_to_sequence(state)327 328        # Update state by processing all SGR sequences in this line329        for match in _SGR_PATTERN.finditer(line):330            state = _sgr_state_update(state, match.group())331 332        # Build output line333        output_line = prefix + line if prefix else line334        if _sgr_state_is_active(state):335            output_line = output_line + _SGR_RESET336 337        result.append(output_line)338 339    return result340 
codekingpro/portable-devtools · Team Ai