Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/wcwidth/text_sizing.py: 43%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1r"""
2`kitty text sizing protocol`_ (OSC 66) parsing and measurement.
4The kitty text sizing protocol allows terminal apps to explicitly tell
5terminals how many cells text occupies, using the escape sequence::
7 ESC ] 66 ; metadata [ ; text ] BEL/ST
9The ``text`` field is optional; when omitted, the sequence occupies
10``s * w`` cells (or zero when ``w`` is also unset).
12Metadata is colon-separated ``key=value`` pairs:
14- ``s``: scale
15- ``w``: width in cells
16- ``n``: fractional numerator
17- ``d``: fractional denominator
18- ``v``: vertical alignment
19- ``h``: horizontal alignment
21Parsing is pretty straight-forward:
23- When ``w > 0``, return ``s * w``.
24- Otherwise ``w == 0``, ``s * wcswidth(inner_text_width)`` cells.
26Numerator, denominator, and alignment codes and values are parsed but otherwise ignored
27and have no effect on measurements made in this library.
29.. _`kitty text sizing protocol`: https://sw.kovidgoyal.net/kitty/text-sizing-protocol/
31.. versionadded:: 0.7.0
32"""
34from __future__ import annotations
36# std imports
37import re
39import typing
41# local
42from ._wcswidth import wcswidth
45class _FieldMeta(typing.NamedTuple):
46 name: str
47 low: int
48 high: int
49 default: int
52TEXT_FIELD_MAPPING: dict[str, _FieldMeta] = {
53 's': _FieldMeta(name='scale', low=1, high=7, default=1),
54 'w': _FieldMeta(name='width', low=0, high=7, default=0),
55 'n': _FieldMeta(name='numerator', low=0, high=15, default=0),
56 'd': _FieldMeta(name='denominator', low=0, high=15, default=0),
57 'v': _FieldMeta(name='vertical_align', low=0, high=2, default=0),
58 'h': _FieldMeta(name='horizontal_align', low=0, high=2, default=0)}
61class TextSizingParams(typing.NamedTuple):
62 """
63 Parsed parameters from a text sizing escape sequence (OSC 66).
65 :param scale: Scale factor (1-7). Text occupies ``scale`` rows tall and ``scale * width``
66 columns wide.
67 :param width: Width in cells (0-7). When 0, width is auto-calculated from the inner text.
68 :param numerator: Fractional scaling numerator (0-15).
69 :param denominator: Fractional scaling denominator (0-15).
70 :param vertical_align: Vertical alignment (0=top, 1=bottom, 2=center).
71 :param horizontal_align: Horizontal alignment (0=left, 1=right, 2=center).
72 """
74 scale: int = 1
75 width: int = 0
76 numerator: int = 0
77 denominator: int = 0
78 vertical_align: int = 0
79 horizontal_align: int = 0
81 def __repr__(self) -> str:
82 """
83 Return a compact representation including only non-default fields.
85 This avoids verbose output when most fields are defaults.
86 """
87 # modified to show values only when non-default
88 repr_fmt = ', '.join(f'{field.name}={getattr(self, field.name)}'
89 for field in TEXT_FIELD_MAPPING.values()
90 if getattr(self, field.name) != field.default)
91 return f'{self.__class__.__name__}({repr_fmt})'
93 def make_sequence(self) -> str:
94 """Build and return sub-part of an OSC 66 sequence."""
95 parts = []
96 # build string for all known parameters of non-default values
97 for field_key, field in TEXT_FIELD_MAPPING.items():
98 if (val := getattr(self, field.name)) != field.default:
99 parts.append(f'{field_key}={val}')
100 return ':'.join(parts)
102 @classmethod
103 def from_params(cls, raw: str, control_codes: str = 'parse') -> TextSizingParams:
104 """
105 Parse colon-separated ``key=value`` metadata string.
107 :param raw: Metadata string, e.g. ``'s=2:w=3'``.
108 :param control_codes: 'parse' or 'strict'.
109 :raises ValueError: If ``control_codes='strict'`` unrecognized text sizing parameters raise
110 ValueError.
111 :returns: Parsed parameters with values clamped to valid ranges.
112 Unknown keys are ignored. Non-integer values use defaults.
114 Example::
116 >>> TextSizingParams.from_params('s=2:w=3')
117 TextSizingParams(scale=2, width=3)
118 """
119 kwargs: typing.Dict[str, int] = {}
120 if not raw:
121 return cls()
122 for part in raw.split(':'):
123 if '=' not in part:
124 if control_codes == 'strict':
125 raise ValueError(f"Expected '=' in text sizing parameter (key=val), "
126 f"got {part!r} in OSC 66 sequence, {raw!r}")
127 continue
128 key, _eq, val = part.partition('=')
129 field = TEXT_FIELD_MAPPING.get(key)
130 if field is None:
131 if control_codes == 'strict':
132 raise ValueError(f"Unknown text sizing field '{key}' "
133 f"in OSC 66 sequence, {raw!r}")
134 # ignore unknown fields unless 'strict'
135 continue
136 try:
137 value = int(val)
138 except ValueError as exc:
139 if control_codes == 'strict':
140 raise ValueError(f"Illegal text sizing value '{val}' "
141 f"in OSC 66 sequence, {raw!r}: {exc}") from exc
142 # ignore value, uses default value without warning unless 'strict'
143 continue
144 if control_codes == 'strict' and (value > field.high or value < field.low):
145 raise ValueError(f"Out of bounds text sizing value '{val}' "
146 f"in OSC 66 sequence, {raw!r}: "
147 f"allowed range for '{key}' ({field.name}) "
148 f"is {field.low} to {field.high}")
149 kwargs[field.name] = max(field.low, min(field.high, value))
150 return cls(**kwargs)
153class TextSizing(typing.NamedTuple):
154 """Basic horizontal width measurement for kitty text sizing protocol."""
156 params: TextSizingParams
157 text: str
158 terminator: str
160 @classmethod
161 def from_match(cls, match: re.Match[str], control_codes: str = 'parse') -> TextSizing:
162 r"""
163 Parse using matching OSC 66 Sequence.
165 :param match: match object from :attr:`wcwidth.escape_sequences.TEXT_SIZING_PATTERN`.
166 :param control_codes: 'parse' or 'strict', same meaning as delegated by
167 :func:`wcwidth.width`.
168 :raises ValueError: When ``control_codes='strict'`` for unrecognized, invalid, or out of
169 bounds text sizing parameters.
170 :returns: TextSizing object from parsed sequence
172 Example::
174 >>> from wcwidth.escape_sequences import TEXT_SIZING_PATTERN
175 >>> TextSizing.from_match(TEXT_SIZING_PATTERN.match('\x1b]66;w=2;XY\x07'))
176 TextSizing(params=TextSizingParams(width=2), text='XY', terminator='\x07')
177 """
178 return cls(params=TextSizingParams.from_params(match.group(1), control_codes=control_codes),
179 text=match.group(2) or '',
180 terminator=match.group(3))
182 def display_width(self, ambiguous_width: int = 1) -> int:
183 """
184 Calculate the display width of a text sizing sequence.
186 :param ambiguous_width: Width for East Asian Ambiguous characters.
187 :returns: Display width in terminal cells. When ``width > 0``, returns
188 ``params.scale * params.width``. When ``width == 0``, returns
189 ``params.scale * measured_inner_width``.
191 .. note: Fractional scaling (numerator/denominator) does not affect the
192 cell count, it adjusts only the font size within the cells allocated by 'w'.
193 """
194 if self.params.width > 0:
195 return self.params.scale * self.params.width
196 w = wcswidth(self.text, ambiguous_width=ambiguous_width)
197 if w < 0:
198 w = 0
199 return self.params.scale * w
201 def make_sequence(self) -> str:
202 """Build and return complete OSC 66 Terminal Sequence."""
203 return f'\x1b]66;{self.params.make_sequence()};{self.text}{self.terminator}'