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

69 statements  

1r""" 

2`kitty text sizing protocol`_ (OSC 66) parsing and measurement. 

3 

4The kitty text sizing protocol allows terminal apps to explicitly tell 

5terminals how many cells text occupies, using the escape sequence:: 

6 

7 ESC ] 66 ; metadata [ ; text ] BEL/ST 

8 

9The ``text`` field is optional; when omitted, the sequence occupies 

10``s * w`` cells (or zero when ``w`` is also unset). 

11 

12Metadata is colon-separated ``key=value`` pairs: 

13 

14- ``s``: scale 

15- ``w``: width in cells 

16- ``n``: fractional numerator 

17- ``d``: fractional denominator 

18- ``v``: vertical alignment 

19- ``h``: horizontal alignment 

20 

21Parsing is pretty straight-forward: 

22 

23- When ``w > 0``, return ``s * w``. 

24- Otherwise ``w == 0``, ``s * wcswidth(inner_text_width)`` cells. 

25 

26Numerator, denominator, and alignment codes and values are parsed but otherwise ignored 

27and have no effect on measurements made in this library. 

28 

29.. _`kitty text sizing protocol`: https://sw.kovidgoyal.net/kitty/text-sizing-protocol/ 

30 

31.. versionadded:: 0.7.0 

32""" 

33 

34from __future__ import annotations 

35 

36# std imports 

37import re 

38 

39import typing 

40 

41# local 

42from ._wcswidth import wcswidth 

43 

44 

45class _FieldMeta(typing.NamedTuple): 

46 name: str 

47 low: int 

48 high: int 

49 default: int 

50 

51 

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)} 

59 

60 

61class TextSizingParams(typing.NamedTuple): 

62 """ 

63 Parsed parameters from a text sizing escape sequence (OSC 66). 

64 

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 """ 

73 

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 

80 

81 def __repr__(self) -> str: 

82 """ 

83 Return a compact representation including only non-default fields. 

84 

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})' 

92 

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) 

101 

102 @classmethod 

103 def from_params(cls, raw: str, control_codes: str = 'parse') -> TextSizingParams: 

104 """ 

105 Parse colon-separated ``key=value`` metadata string. 

106 

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. 

113 

114 Example:: 

115 

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) 

151 

152 

153class TextSizing(typing.NamedTuple): 

154 """Basic horizontal width measurement for kitty text sizing protocol.""" 

155 

156 params: TextSizingParams 

157 text: str 

158 terminator: str 

159 

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. 

164 

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 

171 

172 Example:: 

173 

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)) 

181 

182 def display_width(self, ambiguous_width: int = 1) -> int: 

183 """ 

184 Calculate the display width of a text sizing sequence. 

185 

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``. 

190 

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 

200 

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}'