diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a29b3103..50e0b27f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.0] - Unreleased + +### Added + +- CJK support + +### Changed + +- Added task_id to Progress.track + ## [0.7.2] - 2020-03-15 ### Fixed diff --git a/rich/_cell_widths.py b/rich/_cell_widths.py new file mode 100644 index 000000000..e1080d8fe --- /dev/null +++ b/rich/_cell_widths.py @@ -0,0 +1,407 @@ +# Auto generated by make_terminal_widths.py + +CELL_WIDTHS = [ + (0, 0, 0), + (1, 31, -1), + (127, 159, -1), + (768, 879, 0), + (1155, 1161, 0), + (1425, 1469, 0), + (1471, 1471, 0), + (1473, 1474, 0), + (1476, 1477, 0), + (1479, 1479, 0), + (1552, 1562, 0), + (1611, 1631, 0), + (1648, 1648, 0), + (1750, 1756, 0), + (1759, 1764, 0), + (1767, 1768, 0), + (1770, 1773, 0), + (1809, 1809, 0), + (1840, 1866, 0), + (1958, 1968, 0), + (2027, 2035, 0), + (2070, 2073, 0), + (2075, 2083, 0), + (2085, 2087, 0), + (2089, 2093, 0), + (2137, 2139, 0), + (2260, 2273, 0), + (2275, 2306, 0), + (2362, 2362, 0), + (2364, 2364, 0), + (2369, 2376, 0), + (2381, 2381, 0), + (2385, 2391, 0), + (2402, 2403, 0), + (2433, 2433, 0), + (2492, 2492, 0), + (2497, 2500, 0), + (2509, 2509, 0), + (2530, 2531, 0), + (2561, 2562, 0), + (2620, 2620, 0), + (2625, 2626, 0), + (2631, 2632, 0), + (2635, 2637, 0), + (2641, 2641, 0), + (2672, 2673, 0), + (2677, 2677, 0), + (2689, 2690, 0), + (2748, 2748, 0), + (2753, 2757, 0), + (2759, 2760, 0), + (2765, 2765, 0), + (2786, 2787, 0), + (2817, 2817, 0), + (2876, 2876, 0), + (2879, 2879, 0), + (2881, 2884, 0), + (2893, 2893, 0), + (2902, 2902, 0), + (2914, 2915, 0), + (2946, 2946, 0), + (3008, 3008, 0), + (3021, 3021, 0), + (3072, 3072, 0), + (3134, 3136, 0), + (3142, 3144, 0), + (3146, 3149, 0), + (3157, 3158, 0), + (3170, 3171, 0), + (3201, 3201, 0), + (3260, 3260, 0), + (3263, 3263, 0), + (3270, 3270, 0), + (3276, 3277, 0), + (3298, 3299, 0), + (3329, 3329, 0), + (3393, 3396, 0), + (3405, 3405, 0), + (3426, 3427, 0), + (3530, 3530, 0), + (3538, 3540, 0), + (3542, 3542, 0), + (3633, 3633, 0), + (3636, 3642, 0), + (3655, 3662, 0), + (3761, 3761, 0), + (3764, 3769, 0), + (3771, 3772, 0), + (3784, 3789, 0), + (3864, 3865, 0), + (3893, 3893, 0), + (3895, 3895, 0), + (3897, 3897, 0), + (3953, 3966, 0), + (3968, 3972, 0), + (3974, 3975, 0), + (3981, 3991, 0), + (3993, 4028, 0), + (4038, 4038, 0), + (4141, 4144, 0), + (4146, 4151, 0), + (4153, 4154, 0), + (4157, 4158, 0), + (4184, 4185, 0), + (4190, 4192, 0), + (4209, 4212, 0), + (4226, 4226, 0), + (4229, 4230, 0), + (4237, 4237, 0), + (4253, 4253, 0), + (4352, 4447, 2), + (4957, 4959, 0), + (5906, 5908, 0), + (5938, 5940, 0), + (5970, 5971, 0), + (6002, 6003, 0), + (6068, 6069, 0), + (6071, 6077, 0), + (6086, 6086, 0), + (6089, 6099, 0), + (6109, 6109, 0), + (6155, 6157, 0), + (6277, 6278, 0), + (6313, 6313, 0), + (6432, 6434, 0), + (6439, 6440, 0), + (6450, 6450, 0), + (6457, 6459, 0), + (6679, 6680, 0), + (6683, 6683, 0), + (6742, 6742, 0), + (6744, 6750, 0), + (6752, 6752, 0), + (6754, 6754, 0), + (6757, 6764, 0), + (6771, 6780, 0), + (6783, 6783, 0), + (6832, 6846, 0), + (6912, 6915, 0), + (6964, 6964, 0), + (6966, 6970, 0), + (6972, 6972, 0), + (6978, 6978, 0), + (7019, 7027, 0), + (7040, 7041, 0), + (7074, 7077, 0), + (7080, 7081, 0), + (7083, 7085, 0), + (7142, 7142, 0), + (7144, 7145, 0), + (7149, 7149, 0), + (7151, 7153, 0), + (7212, 7219, 0), + (7222, 7223, 0), + (7376, 7378, 0), + (7380, 7392, 0), + (7394, 7400, 0), + (7405, 7405, 0), + (7412, 7412, 0), + (7416, 7417, 0), + (7616, 7669, 0), + (7675, 7679, 0), + (8203, 8207, 0), + (8232, 8238, 0), + (8288, 8291, 0), + (8400, 8432, 0), + (8986, 8987, 2), + (9001, 9002, 2), + (9193, 9196, 2), + (9200, 9200, 2), + (9203, 9203, 2), + (9725, 9726, 2), + (9748, 9749, 2), + (9800, 9811, 2), + (9855, 9855, 2), + (9875, 9875, 2), + (9889, 9889, 2), + (9898, 9899, 2), + (9917, 9918, 2), + (9924, 9925, 2), + (9934, 9934, 2), + (9940, 9940, 2), + (9962, 9962, 2), + (9970, 9971, 2), + (9973, 9973, 2), + (9978, 9978, 2), + (9981, 9981, 2), + (9989, 9989, 2), + (9994, 9995, 2), + (10024, 10024, 2), + (10060, 10060, 2), + (10062, 10062, 2), + (10067, 10069, 2), + (10071, 10071, 2), + (10133, 10135, 2), + (10160, 10160, 2), + (10175, 10175, 2), + (11035, 11036, 2), + (11088, 11088, 2), + (11093, 11093, 2), + (11503, 11505, 0), + (11647, 11647, 0), + (11744, 11775, 0), + (11904, 11929, 2), + (11931, 12019, 2), + (12032, 12245, 2), + (12272, 12283, 2), + (12288, 12329, 2), + (12330, 12333, 0), + (12334, 12350, 2), + (12353, 12438, 2), + (12441, 12442, 0), + (12443, 12543, 2), + (12549, 12591, 2), + (12593, 12686, 2), + (12688, 12730, 2), + (12736, 12771, 2), + (12784, 12830, 2), + (12832, 12871, 2), + (12880, 19903, 2), + (19968, 42124, 2), + (42128, 42182, 2), + (42607, 42610, 0), + (42612, 42621, 0), + (42654, 42655, 0), + (42736, 42737, 0), + (43010, 43010, 0), + (43014, 43014, 0), + (43019, 43019, 0), + (43045, 43046, 0), + (43204, 43205, 0), + (43232, 43249, 0), + (43302, 43309, 0), + (43335, 43345, 0), + (43360, 43388, 2), + (43392, 43394, 0), + (43443, 43443, 0), + (43446, 43449, 0), + (43452, 43452, 0), + (43493, 43493, 0), + (43561, 43566, 0), + (43569, 43570, 0), + (43573, 43574, 0), + (43587, 43587, 0), + (43596, 43596, 0), + (43644, 43644, 0), + (43696, 43696, 0), + (43698, 43700, 0), + (43703, 43704, 0), + (43710, 43711, 0), + (43713, 43713, 0), + (43756, 43757, 0), + (43766, 43766, 0), + (44005, 44005, 0), + (44008, 44008, 0), + (44013, 44013, 0), + (44032, 55203, 2), + (63744, 64255, 2), + (64286, 64286, 0), + (65024, 65039, 0), + (65040, 65049, 2), + (65056, 65071, 0), + (65072, 65106, 2), + (65108, 65126, 2), + (65128, 65131, 2), + (65281, 65376, 2), + (65504, 65510, 2), + (66045, 66045, 0), + (66272, 66272, 0), + (66422, 66426, 0), + (68097, 68099, 0), + (68101, 68102, 0), + (68108, 68111, 0), + (68152, 68154, 0), + (68159, 68159, 0), + (68325, 68326, 0), + (69633, 69633, 0), + (69688, 69702, 0), + (69759, 69761, 0), + (69811, 69814, 0), + (69817, 69818, 0), + (69888, 69890, 0), + (69927, 69931, 0), + (69933, 69940, 0), + (70003, 70003, 0), + (70016, 70017, 0), + (70070, 70078, 0), + (70090, 70092, 0), + (70191, 70193, 0), + (70196, 70196, 0), + (70198, 70199, 0), + (70206, 70206, 0), + (70367, 70367, 0), + (70371, 70378, 0), + (70400, 70401, 0), + (70460, 70460, 0), + (70464, 70464, 0), + (70502, 70508, 0), + (70512, 70516, 0), + (70712, 70719, 0), + (70722, 70724, 0), + (70726, 70726, 0), + (70835, 70840, 0), + (70842, 70842, 0), + (70847, 70848, 0), + (70850, 70851, 0), + (71090, 71093, 0), + (71100, 71101, 0), + (71103, 71104, 0), + (71132, 71133, 0), + (71219, 71226, 0), + (71229, 71229, 0), + (71231, 71232, 0), + (71339, 71339, 0), + (71341, 71341, 0), + (71344, 71349, 0), + (71351, 71351, 0), + (71453, 71455, 0), + (71458, 71461, 0), + (71463, 71467, 0), + (72752, 72758, 0), + (72760, 72765, 0), + (72767, 72767, 0), + (72850, 72871, 0), + (72874, 72880, 0), + (72882, 72883, 0), + (72885, 72886, 0), + (92912, 92916, 0), + (92976, 92982, 0), + (94095, 94098, 0), + (94176, 94179, 2), + (94208, 100343, 2), + (100352, 101106, 2), + (110592, 110878, 2), + (110928, 110930, 2), + (110948, 110951, 2), + (110960, 111355, 2), + (113821, 113822, 0), + (119143, 119145, 0), + (119163, 119170, 0), + (119173, 119179, 0), + (119210, 119213, 0), + (119362, 119364, 0), + (121344, 121398, 0), + (121403, 121452, 0), + (121461, 121461, 0), + (121476, 121476, 0), + (121499, 121503, 0), + (121505, 121519, 0), + (122880, 122886, 0), + (122888, 122904, 0), + (122907, 122913, 0), + (122915, 122916, 0), + (122918, 122922, 0), + (125136, 125142, 0), + (125252, 125258, 0), + (126980, 126980, 2), + (127183, 127183, 2), + (127374, 127374, 2), + (127377, 127386, 2), + (127488, 127490, 2), + (127504, 127547, 2), + (127552, 127560, 2), + (127568, 127569, 2), + (127584, 127589, 2), + (127744, 127776, 2), + (127789, 127797, 2), + (127799, 127868, 2), + (127870, 127891, 2), + (127904, 127946, 2), + (127951, 127955, 2), + (127968, 127984, 2), + (127988, 127988, 2), + (127992, 128062, 2), + (128064, 128064, 2), + (128066, 128252, 2), + (128255, 128317, 2), + (128331, 128334, 2), + (128336, 128359, 2), + (128378, 128378, 2), + (128405, 128406, 2), + (128420, 128420, 2), + (128507, 128591, 2), + (128640, 128709, 2), + (128716, 128716, 2), + (128720, 128722, 2), + (128725, 128725, 2), + (128747, 128748, 2), + (128756, 128762, 2), + (128992, 129003, 2), + (129293, 129393, 2), + (129395, 129398, 2), + (129402, 129442, 2), + (129445, 129450, 2), + (129454, 129482, 2), + (129485, 129535, 2), + (129648, 129651, 2), + (129656, 129658, 2), + (129664, 129666, 2), + (129680, 129685, 2), + (131072, 196605, 2), + (196608, 262141, 2), + (917760, 917999, 0), +] diff --git a/rich/_wrap.py b/rich/_wrap.py index f36edc26f..bbfccda40 100644 --- a/rich/_wrap.py +++ b/rich/_wrap.py @@ -1,6 +1,9 @@ import re from typing import Iterable, List, Tuple +from .cells import cell_len, chop_cells +from ._tools import iter_last + re_word = re.compile(r"\s*\S+\s*") @@ -18,14 +21,18 @@ def divide_line(text: str, width: int) -> List[int]: divides: List[int] = [] append = divides.append line_position = 0 - for start, end, word in words(text): - if line_position + len(word.rstrip()) > width: + for start, _end, word in words(text): + if line_position + cell_len(word.rstrip()) > width: if line_position and start: append(start) - line_position = len(word) + line_position = cell_len(word) else: - divides.extend(range(start or width, end + 1, width)) - line_position = len(word) % width + for last, line in iter_last(chop_cells(text, width)): + if last: + line_position = cell_len(line) + else: + start += len(line) + append(start) else: - line_position += len(word) + line_position += cell_len(word) return divides diff --git a/rich/bar.py b/rich/bar.py index b0636e1b3..140c1ff5c 100644 --- a/rich/bar.py +++ b/rich/bar.py @@ -26,6 +26,7 @@ def __init__( style: StyleType = "bar.back", complete_style: StyleType = "bar.complete", finished_style: StyleType = "bar.finished", + carriage_return: bool = True, ): self.total = total self.completed = completed @@ -33,6 +34,7 @@ def __init__( self.style = style self.complete_style = complete_style self.finished_style = finished_style + self.carriage_return = carriage_return def __repr__(self) -> str: return f"" @@ -79,7 +81,8 @@ def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult remaining_bars -= 1 if remaining_bars: yield Segment(bar * remaining_bars, style) - yield Segment("\r") + if self.carriage_return: + yield Segment("\r") def __measure__(self, console: Console, max_width: int) -> Measurement: if self.width is not None: diff --git a/rich/cells.py b/rich/cells.py new file mode 100644 index 000000000..081e4e416 --- /dev/null +++ b/rich/cells.py @@ -0,0 +1,99 @@ +from functools import lru_cache +from itertools import takewhile +from typing import List, Tuple + +from ._cell_widths import CELL_WIDTHS + + +def cell_len(text: str) -> int: + """Get the number of cells required to display text. + + Args: + text (str): Text to display. + + Returns: + int: Number of cells required to display the text. + """ + _get_size = get_character_cell_size + return sum(_get_size(character) for character in text) + + +@lru_cache(maxsize=5000) +def get_character_cell_size(character: str, _table=CELL_WIDTHS) -> int: + """Get the cell size of a character. + + Args: + character (str): A single character. + + Returns: + int: Number of cells (0, 1 or 2) occupied by that character. + """ + assert len(character) == 1, "'character' should have a length of 1" + + codepoint = ord(character) + lower_bound = 0 + upper_bound = len(_table) - 1 + index = (lower_bound + upper_bound) // 2 + while True: + start, end, width = _table[index] + if codepoint < start: + upper_bound = index - 1 + elif codepoint > end: + lower_bound = index + 1 + else: + return 0 if width == -1 else width + if upper_bound < lower_bound: + break + index = (lower_bound + upper_bound) // 2 + return 1 + + +def set_cell_size(text: str, total: int) -> str: + """Set the length of a string to fit within given number of cells.""" + cell_size = cell_len(text) + if cell_size == total: + return text + if cell_size < total: + return text + " " * (total - cell_size) + + _get_character_cell_size = get_character_cell_size + character_sizes = [_get_character_cell_size(character) for character in text] + excess = cell_size - total + pop = character_sizes.pop + while excess > 0: + excess -= pop() + text = text[: len(character_sizes)] + if excess == -1: + text += " " + return text + + +def chop_cells(text: str, max_size: int) -> List[str]: + _get_character_cell_size = get_character_cell_size + characters = [ + (character, _get_character_cell_size(character)) for character in text + ][::-1] + total_size = 0 + lines: List[List[str]] = [[]] + + pop = characters.pop + while characters: + character, size = characters.pop() + if total_size + size > max_size: + lines.append([character]) + total_size = size + else: + total_size += size + lines[-1].append(character) + return ["".join(line) for line in lines] + + +if __name__ == "__main__": + + print(get_character_cell_size("😽")) + for line in chop_cells("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", 8): + print(line) + # for n in range(80, 1, -1): + # print(set_cell_size("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", n) + "|") + # print("x" * n) + diff --git a/rich/cjk.py b/rich/cjk.py index bd4a46860..35468ca06 100644 --- a/rich/cjk.py +++ b/rich/cjk.py @@ -1,17 +1,11 @@ from rich.console import Console from rich.panel import Panel -console = Console(width=15) +console = Console(width=16) -# console.print(Panel("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", expand=False,)) +console.print(Panel("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", expand=False)) -# console.print(Panel("""Hello, World!""", width=8, expand=False)) +console.print(Panel(""":pile_of_poo:""", expand=False)) -console.print(Panel(""":pile_of_poo::vampire::thumbs_up: """ * 20)) +console.print(Panel(""":pile_of_poo::vampire::thumbs_up: """ * 5)) print("x" * 15) - -# console = Console(width=15) -# from rich.text import Text - -# console.print("[u][b]Where[/b] there is a [i]Will[/i], there is a Way.[/u]") -# print("x" * 15) diff --git a/rich/console.py b/rich/console.py index bcbac906f..aeb228dd6 100644 --- a/rich/console.py +++ b/rich/console.py @@ -410,8 +410,6 @@ def size(self) -> ConsoleDimensions: return ConsoleDimensions(self._width, self._height) width, height = shutil.get_terminal_size() - # Fixes Issue with Windows console (https://github.com/willmcgugan/rich/issues/7) - width -= 1 return ConsoleDimensions( width if self._width is None else self._width, height if self._height is None else self._height, diff --git a/rich/progress.py b/rich/progress.py index 1fd95db7d..b49b0f9b9 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from collections import deque +from collections.abc import Sized from contextlib import contextmanager from dataclasses import dataclass, replace, field from datetime import timedelta @@ -37,25 +38,42 @@ def track( - sequence: Sequence[ProgressType], description="Working..." + sequence: Union[Sequence[ProgressType], Iterable[ProgressType]], + description="Working...", + total: int = None, + auto_refresh=True, ) -> Iterable[ProgressType]: """Track progress of processing a sequence. Args: - sequence (Sequence[ProgressType]): A sequence (must support "len") you wish to iterate over. - description (str, optional): [description]. Defaults to "Working". + sequence (Iterable[ProgressType]): A sequence (must support "len") you wish to iterate over. + description (str, optional): Description of task show next to progress bar. Defaults to "Working". + total: (int, optional): Total number of steps. Default is len(sequence). + auto_refresh (bool, optional): Automatic refresh, disable to force a refresh after each iteration. Default is True. Returns: Iterable[ProgressType]: An iterable of the values in the sequence. """ - progress = Progress(auto_refresh=False) - task_id = progress.add_task(description, total=len(sequence)) + progress = Progress(auto_refresh=auto_refresh) + + if total is None: + if isinstance(sequence, Sized): + task_total = len(sequence) + else: + raise ValueError( + f"unable to get size of {sequence!r}, please specify 'total'" + ) + else: + task_total = total + + task_id = progress.add_task(description, total=task_total) with progress: for completed, value in enumerate(sequence, 1): yield value progress.update(task_id, completed=completed) - progress.refresh() + if not auto_refresh: + progress.refresh() class ProgressColumn(ABC): @@ -104,7 +122,12 @@ def __init__(self, bar_width: Optional[int] = 40) -> None: def render(self, task: "Task") -> Bar: """Gets a progress bar widget for a task.""" - return Bar(total=task.total, completed=task.completed, width=self.bar_width) + return Bar( + total=task.total, + completed=task.completed, + width=self.bar_width, + carriage_return=False, + ) class TimeRemainingColumn(ProgressColumn): @@ -278,6 +301,7 @@ def __init__( self._lock = RLock() self._refresh_thread: Optional[_RefreshThread] = None self._refresh_count = 0 + self._enter_count = 0 @property def tasks_ids(self) -> List[TaskID]: @@ -313,29 +337,56 @@ def stop(self) -> None: self.console.show_cursor(True) def __enter__(self) -> "Progress": - self.start() - return self + with self._lock: + if self._enter_count: + self._enter_count += 1 + return self + self.start() + self._enter_count += 1 + return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: - self.stop() + with self._lock: + self._enter_count -= 1 + if not self._enter_count: + self.stop() def track( - self, sequence: Sequence[ProgressType], description="Working..." + self, + sequence: Sequence[ProgressType], + total: int = None, + task_id: Optional[TaskID] = None, + description="Working...", ) -> Iterable[ProgressType]: """[summary] Args: sequence (Sequence[ProgressType]): [description] + total: (int, optional): Total number of steps. Default is len(sequence). + task_id: (TaskID): Task to track. Default is new task. + description: (str, optional): Description of task, if new task is created. Returns: Iterable[ProgressType]: [description] """ - task_id = self.add_task(description, total=len(sequence)) + if total is None: + if isinstance(sequence, Sized): + task_total = len(sequence) + else: + raise ValueError( + f"unable to get size of {sequence!r}, please specify 'total'" + ) + else: + task_total = total + + if task_id is None: + task_id = self.add_task(description, total=task_total) + else: + self.update(task_id, total=task_total) with self: for completed, value in enumerate(sequence, 1): yield value self.update(task_id, completed=completed) - self.refresh() def start_task(self, task_id: TaskID) -> None: """Start a task. diff --git a/rich/segment.py b/rich/segment.py index a36390a8b..0a687f441 100644 --- a/rich/segment.py +++ b/rich/segment.py @@ -1,5 +1,6 @@ from typing import NamedTuple, Optional +from .cells import cell_len, set_cell_size from .style import Style from itertools import zip_longest @@ -104,7 +105,7 @@ def adjust_line_length( Returns: List[Segment]: A line of segments with the desired length. """ - line_length = sum(len(text) for text, _style in line) + line_length = sum(cell_len(text) for text, _style in line) new_line: List[Segment] if line_length < length: @@ -117,13 +118,14 @@ def adjust_line_length( append = new_line.append line_length = 0 for segment in line: - segment_length = len(segment.text) + segment_length = cell_len(segment.text) if line_length + segment_length < length: append(segment) line_length += segment_length else: text, style = segment - append(cls(text[: length - line_length], style)) + text = set_cell_size(text, length - line_length) + append(cls(text, style)) break else: new_line = line[:] @@ -139,7 +141,7 @@ def get_line_length(cls, line: List["Segment"]) -> int: Returns: int: The length of the line. """ - return sum(len(text) for text, _ in line) + return sum(cell_len(text) for text, _ in line) @classmethod def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]: diff --git a/rich/table.py b/rich/table.py index 49469af18..2c0967c21 100644 --- a/rich/table.py +++ b/rich/table.py @@ -461,8 +461,8 @@ def _render( if __name__ == "__main__": from .console import Console - c = Console(width=80) - table = Table() + c = Console() + table = Table(expand=True) table.add_column(no_wrap=True) table.add_column() table.add_row( diff --git a/rich/text.py b/rich/text.py index 6ac2d925b..8a8954ab2 100644 --- a/rich/text.py +++ b/rich/text.py @@ -23,6 +23,7 @@ RenderableType, ) +from .cells import cell_len from .containers import Lines from .style import Style from .segment import Segment @@ -322,9 +323,9 @@ def __console__( def __measure__(self, console: "Console", max_width: int) -> Measurement: text = self.text if not text.strip(): - return Measurement(len(text), len(text)) - max_text_width = max(len(line) for line in text.splitlines()) - min_text_width = max(len(word) for word in text.split()) + return Measurement(cell_len(text), cell_len(text)) + max_text_width = max(cell_len(line) for line in text.splitlines()) + min_text_width = max(cell_len(word) for word in text.split()) return Measurement(min_text_width, max_text_width) def _render_line( diff --git a/rich/traceback.py b/rich/traceback.py index 6921934d9..dc328b0b4 100644 --- a/rich/traceback.py +++ b/rich/traceback.py @@ -281,7 +281,7 @@ def _render_stack(self, stack: Stack) -> RenderResult: console = Console() import sys - def bar(a): + def bar(a): # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑 print(1 / a) def foo(a): diff --git a/tools/make_terminal_widths.py b/tools/make_terminal_widths.py index 6270b0e50..86e84b03c 100644 --- a/tools/make_terminal_widths.py +++ b/tools/make_terminal_widths.py @@ -1,62 +1,89 @@ -from functools import partial -from typing import List -import os.path -from urllib.request import urlopen +import subprocess +from typing import List, Tuple +import sys from rich.progress import Progress +from wcwidth import wcwidth -def download(url: str) -> str: - """Copy data from a url to a local file.""" - # This will break if the response doesn't contain content length - filename = url.rsplit("/")[-1] - if os.path.exists(filename): - print(f"{filename} exists") - return filename - progress = Progress() - task = progress.add_task(filename) - with progress: - response = urlopen(url) - progress.update(task, total=int(response.info()["Content-length"])) - with open(filename, "wb") as dest_file: - for data in iter(partial(response.read, 32768), b""): - dest_file.write(data) - progress.advance(task, len(data)) - return filename - - -def get_data(): - east_asian_filename = download( - "http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt" - ) - download( - "http://www.unicode.org/Public/UNIDATA/extracted/DerivedGeneralCategory.txt" +progress = Progress() + + +def make_widths_table(): + table: List[Tuple[int, int, int]] = [] + append = table.append + + make_table_task = progress.add_task("Calculating table...") + + widths = ( + (codepoint, wcwidth(chr(codepoint))) + for codepoint in range(0, sys.maxunicode + 1) ) - print(parse_east_asian(east_asian_filename)) - - -def parse_east_asian(filename: str) -> List[int]: - codepoints: List[int] = [] - for line in open(filename, "rt"): - if line.startswith("#") or not line.strip(): - continue - print(line) - first_field = line.split()[0] - if ";" not in first_field: - continue - codepoint_range, details = first_field.split(";", 1) - if ".." in codepoint_range: - start, end = codepoint_range.split("..") - codepoints.extend(range(int(start, 16), int(end, 16) + 1)) + + widths = [(codepoint, width) for codepoint, width in widths if width != 1] + iter_widths = iter(widths) + + endpoint, group_cell_size = next(iter_widths) + start_codepoint = end_codepoint = endpoint + for codepoint, cell_size in progress.track( + iter_widths, task_id=make_table_task, total=len(widths) - 1 + ): + if cell_size != group_cell_size or codepoint != end_codepoint + 1: + append((start_codepoint, end_codepoint, group_cell_size)) + start_codepoint = end_codepoint = codepoint + group_cell_size = cell_size else: - codepoints.append(int(codepoint_range, 16)) + end_codepoint = codepoint + append((start_codepoint, end_codepoint, group_cell_size)) + return table - return codepoints + +def get_cell_size(table: List[Tuple[int, int, int]], character: str) -> int: + + codepoint = ord(character) + lower_bound = 0 + upper_bound = len(table) - 1 + index = (lower_bound + upper_bound) // 2 + while True: + start, end, width = table[index] + if codepoint < start: + upper_bound = index - 1 + elif codepoint > end: + lower_bound = index + 1 + else: + return width + if upper_bound < lower_bound: + break + index = (lower_bound + upper_bound) // 2 + return 1 + + +def test(widths_table): + for codepoint in progress.track( + range(0, sys.maxunicode + 1), description="Testing..." + ): + character = chr(codepoint) + width1 = get_cell_size(widths_table, character) + width2 = wcwidth(character) + if width1 != width2: + print(f"{width1} != {width2}") + break def run(): - get_data() + with progress: + widths_table = make_widths_table() + test(widths_table) + table_file = f"""# Auto generated by make_terminal_widths.py + +CELL_WIDTHS = {widths_table!r} + +""" + with open("../rich/_cell_widths.py", "wt") as fh: + fh.write(table_file) + + subprocess.run("black ../rich/_cell_widths.py", shell=True) if __name__ == "__main__":