Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
_width.py340 linesDownload Raw Back to wcwidth
1"""This is a high-level width() supporting terminal output."""2 3from typing import Literal4 5# local6from ._wcwidth import wcwidth7from .bisearch import bisearch8from ._wcswidth import wcswidth9from ._constants import (_EMOJI_ZWJ_SET,10                         _ISC_VIRAMA_SET,11                         _CATEGORY_MC_TABLE,12                         _FITZPATRICK_RANGE,13                         _REGIONAL_INDICATOR_SET)14from .table_vs16 import VS16_NARROW_TO_WIDE15from .text_sizing import TextSizing, TextSizingParams16from .control_codes import ILLEGAL_CTRL, VERTICAL_CTRL, HORIZONTAL_CTRL, ZERO_WIDTH_CTRL17from .table_grapheme import ISC_CONSONANT18from .escape_sequences import (_SEQUENCE_CLASSIFY,19                               TEXT_SIZING_PATTERN,20                               CURSOR_MOVEMENT_SEQUENCE,21                               INDETERMINATE_EFFECT_SEQUENCE,22                               strip_sequences)23 24# In 'parse' mode, strings longer than this are checked for cursor-movement25# controls (BS, TAB, CR, cursor sequences); when absent, mode downgrades to26# 'ignore' to skip character-by-character parsing. The detection scan cost is27# negligible for long strings but wasted on short ones like labels or headings.28_WIDTH_FAST_PATH_MIN_LEN = 2029 30# Translation table to strip C0/C1 control characters for fast 'ignore' mode.31_CONTROL_CHAR_TABLE = str.maketrans('', '', (32    ''.join(chr(c) for c in range(0x00, 0x20)) +   # C0: NUL through US (including tab)33    '\x7f' +                                       # DEL34    ''.join(chr(c) for c in range(0x80, 0xa0))     # C1: U+0080-U+009F35))36 37 38def _width_ignored_codes(text: str, ambiguous_width: int = 1) -> int:39    """40    Fast path for width() with control_codes='ignore'.41 42    Strips escape sequences and control characters, then measures remaining text.43    """44    return wcswidth(45        strip_sequences(text).translate(_CONTROL_CHAR_TABLE),46        ambiguous_width=ambiguous_width47    )48 49 50def width(51    text: str,52    *,53    control_codes: Literal['parse', 'strict', 'ignore'] = 'parse',54    tabsize: int = 8,55    ambiguous_width: int = 1,56) -> int:57    r"""58    Return printable width of text containing many kinds of control codes and sequences.59 60    Unlike :func:`wcswidth`, this function handles most control characters and many popular terminal61    output sequences.  Never returns -1.62 63    :param text: String to measure.64    :param control_codes: How to handle control characters and sequences:65 66        - ``'parse'`` (default): Track horizontal cursor movement like BS ``\b``, CR ``\r``, TAB67          ``\t``, cursor left and right movement sequences.  Vertical movement (LF, VT, FF) and68          indeterminate terminal sequences are zero-width. OSC 66 Kitty Text Sizing protocol, OSC 869          Hyperlink, and many other kinds of output sequences are parsed for displayed measurements.70        - ``'strict'``: Like parse, but raises :exc:`ValueError` on control characters with71          indeterminate results of the screen or cursor, like clear or vertical movement. Generally,72          these should be handled with a virtual terminal emulator (like 'pyte').73        - ``'ignore'``: All C0 and C1 control characters and escape sequences are measured as74          width 0. This is the fastest measurement for text already filtered or known not to contain75          any kinds of control codes or sequences. TAB ``\t`` is zero-width; to ensure76          tab expansion, pre-process text using :func:`str.expandtabs`.77 78    :param tabsize: Tab stop width for ``'parse'`` and ``'strict'`` modes. Default is 8.79        Must be positive. Has no effect when ``control_codes='ignore'``.80    :param ambiguous_width: Width to use for East Asian Ambiguous (A)81        characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts.82    :returns: Maximum cursor position reached, "extent", accounting for cursor movement sequences83        present in ``text`` according to given parameters.  This represents the rightmost column the84        cursor reaches.  Always a non-negative integer.85 86    :raises ValueError: If ``control_codes='strict'`` and control characters with indeterminate87        effects, such as vertical movement or clear sequences are encountered, or on unexpected88        C0 or C1 control code. Also raised when ``control_codes`` is not one of the valid values.89 90    .. versionadded:: 0.3.091 92    .. versionchanged:: 0.7.093       Expanded strict-mode to raise :exc:`ValueError` when cursor-left movement94       (CSI D) would move beyond the beginning of the string. Previously, cursor-left95       was silently clamped to column 0 in all modes.96 97       Support horizontal cursor sequences (``cub``, ``cuf``, ``hpa``). Cursor-left (``cub``) or98       backspace (``\b``) now overwrites text.  ``column_address`` (``hpa``) and carriage return99       (``\r``) are now parsed, and some values conditionally raise ``ValueError`` when100       ``control_codes='parse'``.101 102    Examples::103 104        >>> width('hello')105        5106        >>> width('コンニチハ')107        10108        >>> width('\x1b[31mred\x1b[0m')109        3110        >>> width('\x1b[31mred\x1b[0m', control_codes='ignore')  # same result (ignored)111        3112        >>> width('123\b4')     # backspace overwrites previous cell (outputs '124')113        3114        >>> width('abc\t')      # tab caused cursor to move to column 8115        8116        >>> width('1\x1b[10C')  # '1' + cursor right 10, cursor ends on column 11117        11118        >>> width('1\x1b[10C', control_codes='ignore')   # faster but wrong in this case119        1120    """121    # pylint: disable=too-complex,too-many-branches,too-many-statements,too-many-locals122    # This could be broken into sub-functions (#1, #3, and #6 especially), but for reduced overhead123    # in consideration of this function a likely "hot path", they are inline, breaking many pylint124    # complexity rules.125 126    # Fast path for ASCII printable (no tabs, escapes, or control chars)127    if text.isascii() and text.isprintable():128        return len(text)129 130    # Fast parse: if no horizontal cursor movements are possible, switch to 'ignore' mode.131    # Only check longer strings - the detection overhead hurts short string performance.132    if control_codes == 'parse' and len(text) > _WIDTH_FAST_PATH_MIN_LEN:133        # Check for cursor-affecting control characters134        if '\b' not in text and '\t' not in text and '\r' not in text:135            # Check for escape sequences, if none contain cursor movement or136            # text sizing, downgrade to 'ignore'137            if '\x1b' not in text or (138                not CURSOR_MOVEMENT_SEQUENCE.search(text)139                and not TEXT_SIZING_PATTERN.search(text)140            ):141                control_codes = 'ignore'142 143    # Fast path for ignore mode, useful if you know the text is already free of control codes144    if control_codes == 'ignore':145        return _width_ignored_codes(text, ambiguous_width)146 147    strict = control_codes == 'strict'148    # Track absolute positions: tab stops need modulo on absolute column, CR resets to 0.149    # Initialize max_extent to 0 so backward movement (CR, BS) won't yield negative width.150    current_col = 0151    max_extent = 0152    idx = 0153    text_len = len(text)154 155    # Select wcwidth call pattern for best lru_cache performance:156    # - ambiguous_width=1 (default): single-arg calls share cache with direct wcwidth() calls157    # - ambiguous_width=2: full positional args needed (results differ, separate cache is correct)158    _wcwidth = wcwidth if ambiguous_width == 1 else lambda c: wcwidth(c, 'auto', ambiguous_width)159 160    # grapheme-clustering state161    last_measured_idx = -2162    last_measured_ucs = -1163    last_was_virama = False164    conjunct_pending = False165 166    while idx < text_len:167        char = text[idx]168 169        # 1. ESC sequences170        if char == '\x1b':171            m = _SEQUENCE_CLASSIFY.match(text, idx)172            if not m:173                # 1a. Errant ESC or unknown sequence: only the first character is zero-width174                idx += 1175            else:176                seq = m.group()177                if strict and INDETERMINATE_EFFECT_SEQUENCE.match(seq):178                    raise ValueError(f"Indeterminate cursor sequence at position {idx}, {seq!r}")179 180                # 2b. horizontal position absolute (before forward/backward to181                #     avoid other_seq match in _SEQUENCE_CLASSIFY)182                if (hpa_n := m.group('hpa_n')) is not None:183                    target_col = int(hpa_n) if hpa_n else 1184                    if strict:185                        raise ValueError(186                            f"Indeterminate horizontal position at position {idx}, "187                            f"{seq!r} (absolute column unknown)"188                        )189                    current_col = target_col - 1  # HPA is 1-indexed, convert to 0-indexed190                # 2c. cursor forward, backward191                elif (cforward_n := m.group('cforward_n')) is not None:192                    current_col += int(cforward_n) if cforward_n else 1193                elif (cbackward_n := m.group('cbackward_n')) is not None:194                    n_backward = int(cbackward_n) if cbackward_n else 1195                    if strict and n_backward > current_col:196                        raise ValueError(197                            f"Cursor left movement at position {idx} would move "198                            f"{n_backward} cells left from column {current_col}, "199                            f"exceeding string start"200                        )201                    current_col = max(0, current_col - n_backward)202                # 2d. OSC 66 Text Sizing — has positive display width203                elif (ts_meta := m.group('ts_meta')) is not None:204                    ts_text = m.group('ts_text')205                    ts_term = m.group('ts_term')206                    assert ts_text is not None and ts_term is not None207                    text_size = TextSizing(208                        TextSizingParams.from_params(ts_meta, control_codes=control_codes),209                        ts_text, ts_term)210                    current_col += text_size.display_width(ambiguous_width)211                # 2e. SGR and other zero-width sequences -- no column advance212                idx = m.end()213            # Escape sequences break VS16 adjacency: reset last-measured state214            last_measured_idx = -2215            last_measured_ucs = -1216            max_extent = max(max_extent, current_col)217            continue218 219        # 2. Vertical or Illegal control characters zero width or error when 'strict'220        if char in ILLEGAL_CTRL:221            if strict:222                raise ValueError(f"Illegal control character {ord(char):#x} at position {idx}")223            idx += 1224            last_measured_idx = -2225            last_measured_ucs = -1226            continue227 228        if char in VERTICAL_CTRL:229            if strict:230                raise ValueError(f"Vertical movement character {ord(char):#x} at position {idx}")231            idx += 1232            last_measured_idx = -2233            last_measured_ucs = -1234            continue235 236        # 3. Horizontal movement characters237        if char in HORIZONTAL_CTRL:238            if char == '\t' and tabsize > 0:239                current_col += tabsize - (current_col % tabsize)240            elif char == '\b':241                if current_col > 0:242                    current_col -= 1243            elif char == '\r':244                if strict:245                    raise ValueError(246                        f"Horizontal movement character \\r at position {idx}: "247                        "indeterminate starting column"248                    )249                current_col = 0250            max_extent = max(max_extent, current_col)251            idx += 1252            last_measured_idx = -2253            last_measured_ucs = -1254            continue255 256        # 4. Zero-width control characters257        if char in ZERO_WIDTH_CTRL:258            idx += 1259            last_measured_idx = -2260            last_measured_ucs = -1261            continue262 263        # 5. Inline grapheme-clustering: ZWJ, VS16, Regional Indicators,264        #    Fitzpatrick, Virama conjuncts, Mc, wcwidth265        ucs = ord(char)266 267        # ZWJ (U+200D)268        if ucs == 0x200D:269            if last_was_virama:270                idx += 1271            elif idx + 1 < text_len:272                last_was_virama = False273                idx += 2274            else:275                last_was_virama = False276                idx += 1277            continue278 279        # VS16 (U+FE0F): converts preceding narrow character to wide.280        if ucs == 0xFE0F and last_measured_idx >= 0:281            if bisearch(ord(text[last_measured_idx]), VS16_NARROW_TO_WIDE['9.0.0']):282                current_col += 1283                max_extent = max(max_extent, current_col)284            last_measured_idx = -2  # prevent double application285            idx += 1286            continue287 288        # Regional Indicator & Fitzpatrick (both above BMP)289        if ucs > 0xFFFF:290            if ucs in _REGIONAL_INDICATOR_SET:291                ri_before = 0292                j = idx - 1293                while j >= 0 and ord(text[j]) in _REGIONAL_INDICATOR_SET:294                    ri_before += 1295                    j -= 1296                if ri_before % 2 == 1:297                    last_measured_ucs = ucs298                    idx += 1299                    continue300            elif (_FITZPATRICK_RANGE[0] <= ucs <= _FITZPATRICK_RANGE[1]301                  and last_measured_ucs in _EMOJI_ZWJ_SET):302                idx += 1303                continue304 305        # Virama conjunct formation306        if last_was_virama and bisearch(ucs, ISC_CONSONANT):307            last_measured_idx = idx308            last_measured_ucs = ucs309            last_was_virama = False310            conjunct_pending = True311            idx += 1312            continue313 314        # Normal character: measure with wcwidth315        w = _wcwidth(char)316        if w > 0:317            if conjunct_pending:318                current_col += 1319                conjunct_pending = False320            current_col += w321            max_extent = max(max_extent, current_col)322            last_measured_idx = idx323            last_measured_ucs = ucs324            last_was_virama = False325        elif last_measured_idx >= 0 and bisearch(ucs, _CATEGORY_MC_TABLE):326            # Spacing Combining Mark (Mc) following a base character adds 1327            current_col += 1328            max_extent = max(max_extent, current_col)329            last_measured_idx = -2330            last_was_virama = False331            conjunct_pending = False332        else:333            last_was_virama = ucs in _ISC_VIRAMA_SET334        idx += 1335 336    if conjunct_pending:337        current_col += 1338        max_extent = max(max_extent, current_col)339    return max_extent340 
codekingpro/portable-devtools · Team Ai