Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/cells.py: 25%
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
1from __future__ import annotations
3from functools import lru_cache
4from operator import itemgetter
5from typing import Callable, NamedTuple, Sequence, Tuple
7from rich._unicode_data import load as load_cell_table
9CellSpan = Tuple[int, int, int]
11_span_get_cell_len = itemgetter(2)
13# Ranges of unicode ordinals that produce a 1-cell wide character
14# This is non-exhaustive, but covers most common Western characters
15_SINGLE_CELL_UNICODE_RANGES: list[tuple[int, int]] = [
16 (0x20, 0x7E), # Latin (excluding non-printable)
17 (0xA0, 0xAC),
18 (0xAE, 0x002FF),
19 (0x00370, 0x00482), # Greek / Cyrillic
20 (0x02500, 0x025FC), # Box drawing, box elements, geometric shapes
21 (0x02800, 0x028FF), # Braille
22]
24# A frozen set of characters that are a single cell wide
25_SINGLE_CELLS = frozenset(
26 [
27 character
28 for _start, _end in _SINGLE_CELL_UNICODE_RANGES
29 for character in map(chr, range(_start, _end + 1))
30 ]
31)
33# When called with a string this will return True if all
34# characters are single-cell, otherwise False
35_is_single_cell_widths: Callable[[str], bool] = _SINGLE_CELLS.issuperset
38class CellTable(NamedTuple):
39 """Contains unicode data required to measure the cell widths of glyphs."""
41 unicode_version: str
42 widths: Sequence[tuple[int, int, int]]
43 narrow_to_wide: frozenset[str]
46@lru_cache(maxsize=4096)
47def get_character_cell_size(character: str, unicode_version: str = "auto") -> int:
48 """Get the cell size of a character.
50 Args:
51 character (str): A single character.
52 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version.
54 Returns:
55 int: Number of cells (0, 1 or 2) occupied by that character.
56 """
57 codepoint = ord(character)
58 if codepoint and codepoint < 32 or 0x07F <= codepoint < 0x0A0:
59 return 0
60 table = load_cell_table(unicode_version).widths
62 last_entry = table[-1]
63 if codepoint > last_entry[1]:
64 return 1
66 lower_bound = 0
67 upper_bound = len(table) - 1
69 while lower_bound <= upper_bound:
70 index = (lower_bound + upper_bound) >> 1
71 start, end, width = table[index]
72 if codepoint < start:
73 upper_bound = index - 1
74 elif codepoint > end:
75 lower_bound = index + 1
76 else:
77 return width
78 return 1
81@lru_cache(4096)
82def cached_cell_len(text: str, unicode_version: str = "auto") -> int:
83 """Get the number of cells required to display text.
85 This method always caches, which may use up a lot of memory. It is recommended to use
86 `cell_len` over this method.
88 Args:
89 text (str): Text to display.
90 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version.
92 Returns:
93 int: Get the number of cells required to display text.
94 """
95 return _cell_len(text, unicode_version)
98def cell_len(text: str, unicode_version: str = "auto") -> int:
99 """Get the cell length of a string (length as it appears in the terminal).
101 Args:
102 text: String to measure.
103 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version.
105 Returns:
106 Length of string in terminal cells.
107 """
108 if len(text) < 512:
109 return cached_cell_len(text, unicode_version)
110 return _cell_len(text, unicode_version)
113def _cell_len(text: str, unicode_version: str) -> int:
114 """Get the cell length of a string (length as it appears in the terminal).
116 Args:
117 text: String to measure.
118 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version.
120 Returns:
121 Length of string in terminal cells.
122 """
124 if _is_single_cell_widths(text):
125 return len(text)
127 # "\u200d" is zero width joiner
128 # "\ufe0f" is variation selector 16
129 if "\u200d" not in text and "\ufe0f" not in text:
130 # Simplest case with no unicode stuff that changes the size
131 return sum(
132 get_character_cell_size(character, unicode_version) for character in text
133 )
135 cell_table = load_cell_table(unicode_version)
136 total_width = 0
137 last_measured_character: str | None = None
139 SPECIAL = {"\u200d", "\ufe0f"}
141 index = 0
142 character_count = len(text)
144 while index < character_count:
145 character = text[index]
146 if character in SPECIAL:
147 if character == "\u200d":
148 index += 1
149 elif last_measured_character:
150 total_width += last_measured_character in cell_table.narrow_to_wide
151 last_measured_character = None
152 else:
153 if character_width := get_character_cell_size(character, unicode_version):
154 last_measured_character = character
155 total_width += character_width
156 index += 1
158 return total_width
161def split_graphemes(
162 text: str, unicode_version: str = "auto"
163) -> "tuple[list[CellSpan], int]":
164 """Divide text into spans that define a single grapheme.
166 Args:
167 text: String to split.
168 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version.
170 Returns:
171 List of spans.
172 """
174 cell_table = load_cell_table(unicode_version)
175 codepoint_count = len(text)
176 index = 0
177 last_measured_character: str | None = None
179 total_width = 0
180 spans: list[tuple[int, int, int]] = []
181 SPECIAL = {"\u200d", "\ufe0f"}
182 while index < codepoint_count:
183 if (character := text[index]) in SPECIAL:
184 if character == "\u200d":
185 # zero width joiner
186 index += 2
187 if spans:
188 start, _end, cell_length = spans[-1]
189 spans[-1] = (start, index, cell_length)
190 elif last_measured_character:
191 # variation selector 16
192 index += 1
193 if spans:
194 start, _end, cell_length = spans[-1]
195 if last_measured_character in cell_table.narrow_to_wide:
196 last_measured_character = None
197 cell_length += 1
198 total_width += 1
199 spans[-1] = (start, index, cell_length)
200 continue
202 if character_width := get_character_cell_size(character, unicode_version):
203 last_measured_character = character
204 spans.append((index, index := index + 1, character_width))
205 total_width += character_width
206 elif spans:
207 # zero width characters are associated with the previous character
208 start, _end, cell_length = spans[-1]
209 spans[-1] = (start, index := index + 1, cell_length)
211 return (spans, total_width)
214def _split_text(
215 text: str, cell_position: int, unicode_version: str = "auto"
216) -> tuple[str, str]:
217 """Split text by cell position.
219 If the cell position falls within a double width character, it is converted to two spaces.
221 Args:
222 text: Text to split.
223 cell_position Offset in cells.
224 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version.
226 Returns:
227 Tuple to two split strings.
228 """
229 if cell_position <= 0:
230 return "", text
232 spans, cell_length = split_graphemes(text, unicode_version)
234 # Guess initial offset
235 offset = int((cell_position / cell_length) * len(spans))
236 left_size = sum(map(_span_get_cell_len, spans[:offset]))
238 while True:
239 if left_size == cell_position:
240 if offset >= len(spans):
241 return text, ""
242 split_index = spans[offset][0]
243 return text[:split_index], text[split_index:]
244 if left_size < cell_position:
245 start, end, cell_size = spans[offset]
246 if left_size + cell_size > cell_position:
247 return text[:start] + " ", " " + text[end:]
248 offset += 1
249 left_size += cell_size
250 else: # left_size > cell_position
251 start, end, cell_size = spans[offset - 1]
252 if left_size - cell_size < cell_position:
253 return text[:start] + " ", " " + text[end:]
254 offset -= 1
255 left_size -= cell_size
258def split_text(
259 text: str, cell_position: int, unicode_version: str = "auto"
260) -> tuple[str, str]:
261 """Split text by cell position.
263 If the cell position falls within a double width character, it is converted to two spaces.
265 Args:
266 text: Text to split.
267 cell_position Offset in cells.
268 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version.
270 Returns:
271 Tuple to two split strings.
272 """
273 if _is_single_cell_widths(text):
274 return text[:cell_position], text[cell_position:]
275 return _split_text(text, cell_position, unicode_version)
278def set_cell_size(text: str, total: int, unicode_version: str = "auto") -> str:
279 """Adjust a string by cropping or padding with spaces such that it fits within the given number of cells.
281 Args:
282 text: String to adjust.
283 total: Desired size in cells.
284 unicode_version: Unicode version.
286 Returns:
287 A string with cell size equal to total.
288 """
289 if _is_single_cell_widths(text):
290 size = len(text)
291 if size < total:
292 return text + " " * (total - size)
293 return text[:total]
294 if total <= 0:
295 return ""
296 cell_size = cell_len(text)
297 if cell_size == total:
298 return text
299 if cell_size < total:
300 return text + " " * (total - cell_size)
301 text, _ = _split_text(text, total, unicode_version)
302 return text
305def chop_cells(text: str, width: int, unicode_version: str = "auto") -> list[str]:
306 """Split text into lines such that each line fits within the available (cell) width.
308 Args:
309 text: The text to fold such that it fits in the given width.
310 width: The width available (number of cells).
312 Returns:
313 A list of strings such that each string in the list has cell width
314 less than or equal to the available width.
315 """
316 if _is_single_cell_widths(text):
317 return [text[index : index + width] for index in range(0, len(text), width)]
318 spans, _ = split_graphemes(text, unicode_version)
319 line_size = 0 # Size of line in cells
320 lines: list[str] = []
321 line_offset = 0 # Offset (in codepoints) of start of line
322 for start, end, cell_size in spans:
323 if line_size + cell_size > width:
324 lines.append(text[line_offset:start])
325 line_offset = start
326 line_size = 0
327 line_size += cell_size
328 if line_size:
329 lines.append(text[line_offset:])
331 return lines