Merge pull request #22 from willmcgugan/terminal-width

Terminal width
This commit is contained in:
Will McGugan 2020-03-17 17:13:27 +00:00 committed by GitHub
commit 3f6625365a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 789 additions and 60 deletions

1
.gitignore vendored
View file

@ -4,6 +4,7 @@
mypy_report mypy_report
docs/build docs/build
docs/source/_build docs/source/_build
tools/*.txt
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/

View file

@ -5,6 +5,19 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.8.0] - 2020-03-17
### Added
- CJK support
- Console level highlight flag
- Added encoding argument to Syntax.from_path
### Changed
- Dropped support for Windows command prompt (try https://www.microsoft.com/en-gb/p/windows-terminal-preview/)
- Added task_id to Progress.track
## [0.7.2] - 2020-03-15 ## [0.7.2] - 2020-03-15
### Fixed ### Fixed

View file

@ -2,7 +2,7 @@
name = "rich" name = "rich"
homepage = "https://github.com/willmcgugan/rich" homepage = "https://github.com/willmcgugan/rich"
documentation = "https://rich.readthedocs.io/en/latest/" documentation = "https://rich.readthedocs.io/en/latest/"
version = "0.7.2" version = "0.8.0-alpha.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <willmcgugan@gmail.com>"] authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT" license = "MIT"
@ -25,7 +25,6 @@ python = "^3.6"
pprintpp = "^0.4.0" pprintpp = "^0.4.0"
typing-extensions = "^3.7.4" typing-extensions = "^3.7.4"
dataclasses = {version="^0.7", python = "~3.6"} dataclasses = {version="^0.7", python = "~3.6"}
colorama = "^0.4.3"
pygments = "^2.5.0" pygments = "^2.5.0"
commonmark = "^0.9.0" commonmark = "^0.9.0"

View file

@ -1,5 +1,3 @@
from colorama import init
from typing import Any, IO, Optional, TYPE_CHECKING from typing import Any, IO, Optional, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
@ -22,7 +20,5 @@ def print(
return write_console.print(*objects, sep=sep, end=end) return write_console.print(*objects, sep=sep, end=end)
init()
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover
print("Hello, **World**") print("Hello, **World**")

407
rich/_cell_widths.py Normal file
View file

@ -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),
]

34
rich/_lru_cache.py Normal file
View file

@ -0,0 +1,34 @@
from collections import OrderedDict
from typing import Generic, TypeVar
CacheKey = TypeVar("CacheKey")
CacheValue = TypeVar("CacheValue")
class LRUCache(Generic[CacheKey, CacheValue], OrderedDict):
"""
A dictionary-like container that stores a given maximum items.
If an additional item is added when the LRUCache is full, the least
recently used key is discarded to make room for the new item.
"""
def __init__(self, cache_size: int) -> None:
self.cache_size = cache_size
super(LRUCache, self).__init__()
def __setitem__(self, key: CacheKey, value: CacheValue) -> None:
"""Store a new views, potentially discarding an old value."""
if key not in self:
if len(self) >= self.cache_size:
self.popitem(last=False)
OrderedDict.__setitem__(self, key, value)
def __getitem__(self, key: CacheKey) -> CacheValue:
"""Gets the item, but also makes it most recent."""
value: CacheValue = OrderedDict.__getitem__(self, key)
OrderedDict.__delitem__(self, key)
OrderedDict.__setitem__(self, key, value)
return value

View file

@ -1,6 +1,9 @@
import re import re
from typing import Iterable, List, Tuple 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*") re_word = re.compile(r"\s*\S+\s*")
@ -18,14 +21,18 @@ def divide_line(text: str, width: int) -> List[int]:
divides: List[int] = [] divides: List[int] = []
append = divides.append append = divides.append
line_position = 0 line_position = 0
for start, end, word in words(text): for start, _end, word in words(text):
if line_position + len(word.rstrip()) > width: if line_position + cell_len(word.rstrip()) > width:
if line_position and start: if line_position and start:
append(start) append(start)
line_position = len(word) line_position = cell_len(word)
else: else:
divides.extend(range(start or width, end + 1, width)) for last, line in iter_last(chop_cells(text, width)):
line_position = len(word) % width if last:
line_position = cell_len(line)
else:
start += len(line)
append(start)
else: else:
line_position += len(word) line_position += cell_len(word)
return divides return divides

View file

@ -79,7 +79,6 @@ class Bar:
remaining_bars -= 1 remaining_bars -= 1
if remaining_bars: if remaining_bars:
yield Segment(bar * remaining_bars, style) yield Segment(bar * remaining_bars, style)
yield Segment("\r")
def __measure__(self, console: Console, max_width: int) -> Measurement: def __measure__(self, console: Console, max_width: int) -> Measurement:
if self.width is not None: if self.width is not None:
@ -97,6 +96,7 @@ if __name__ == "__main__":
for n in range(0, 101, 1): for n in range(0, 101, 1):
bar.update_progress(n) bar.update_progress(n)
console.print(bar) console.print(bar)
console.file.write("\r")
time.sleep(0.05) time.sleep(0.05)
console.show_cursor(True) console.show_cursor(True)
console.print() console.print()

113
rich/cells.py Normal file
View file

@ -0,0 +1,113 @@
from functools import lru_cache
from itertools import takewhile
from typing import List, Tuple
from ._cell_widths import CELL_WIDTHS
from ._lru_cache import LRUCache
def cell_len(text: str, _cache: LRUCache[str, int] = LRUCache(1024)) -> 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.
"""
cached_result = _cache.get(text, None)
if cached_result is not None:
return cached_result
_get_size = get_character_cell_size
total_size = sum(_get_size(character) for character in text)
if len(text) < 256:
_cache[text] = total_size
return total_size
@lru_cache(maxsize=5000)
def get_character_cell_size(character: str) -> 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.
"""
codepoint = ord(character)
if 127 > codepoint > 31:
# Shortcut for ascii
return 1
_table = CELL_WIDTHS
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]:
"""Break text in to equal (cell) length strings."""
_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]] = [[]]
append = lines[-1].append
pop = characters.pop
while characters:
character, size = pop()
if total_size + size > max_size:
lines.append([character])
append = lines[-1].append
total_size = size
else:
total_size += size
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)

11
rich/cjk.py Normal file
View file

@ -0,0 +1,11 @@
from rich.console import Console
from rich.panel import Panel
console = Console(width=16)
console.print(Panel("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", expand=False))
console.print(Panel(""":pile_of_poo:""", expand=False))
console.print(Panel(""":pile_of_poo::vampire::thumbs_up: """ * 5))
print("x" * 15)

View file

@ -235,8 +235,9 @@ class Console:
height (int, optional): The height of the terminal. Leave as default to auto-detect height. height (int, optional): The height of the terminal. Leave as default to auto-detect height.
record (bool, optional): Boolean to enable recording of terminal output, record (bool, optional): Boolean to enable recording of terminal output,
required to call :meth:`export_html` and :meth:`export_text`. Defaults to False. required to call :meth:`export_html` and :meth:`export_text`. Defaults to False.
emoji (Optional[bool], optional): Enable emoji code. Defaults to True.
markup (bool, optional): Boolean to enable :ref:`console_markup`. Defaults to True. markup (bool, optional): Boolean to enable :ref:`console_markup`. Defaults to True.
emoji (bool, optional): Enable emoji code. Defaults to True.
highlight (bool, optional): Enable automatic highlighting. Defaults to True.
log_time (bool, optional): Boolean to enable logging of time by :meth:`log` methods. Defaults to True. log_time (bool, optional): Boolean to enable logging of time by :meth:`log` methods. Defaults to True.
log_path (bool, optional): Boolean to enable the logging of the caller by :meth:`log`. Defaults to True. log_path (bool, optional): Boolean to enable the logging of the caller by :meth:`log`. Defaults to True.
log_time_format (str, optional): Log time format if ``log_time`` is enabled. Defaults to "[%X] ". log_time_format (str, optional): Log time format if ``log_time`` is enabled. Defaults to "[%X] ".
@ -256,6 +257,7 @@ class Console:
record: bool = False, record: bool = False,
markup: bool = True, markup: bool = True,
emoji: bool = True, emoji: bool = True,
highlight: bool = True,
log_time: bool = True, log_time: bool = True,
log_path: bool = True, log_path: bool = True,
log_time_format: str = "[%X] ", log_time_format: str = "[%X] ",
@ -271,6 +273,7 @@ class Console:
self.record = record self.record = record
self._markup = markup self._markup = markup
self._emoji = emoji self._emoji = emoji
self._highlight = highlight
if color_system is None: if color_system is None:
self._color_system = None self._color_system = None
@ -319,9 +322,7 @@ class Console:
"""Detect color system from env vars.""" """Detect color system from env vars."""
if not self.is_terminal: if not self.is_terminal:
return None return None
if WINDOWS: if os.environ.get("COLORTERM", "").strip().lower() in ("truecolor", "24bit"):
return ColorSystem.WINDOWS
if os.environ.get("COLORTERM", "").strip().lower() == "truecolor":
return ColorSystem.TRUECOLOR return ColorSystem.TRUECOLOR
# 256 can be considered standard nowadays # 256 can be considered standard nowadays
return ColorSystem.EIGHT_BIT return ColorSystem.EIGHT_BIT
@ -410,8 +411,6 @@ class Console:
return ConsoleDimensions(self._width, self._height) return ConsoleDimensions(self._width, self._height)
width, height = shutil.get_terminal_size() width, height = shutil.get_terminal_size()
# Fixes Issue with Windows console (https://github.com/willmcgugan/rich/issues/7)
width -= 1
return ConsoleDimensions( return ConsoleDimensions(
width if self._width is None else self._width, width if self._width is None else self._width,
height if self._height is None else self._height, height if self._height is None else self._height,
@ -445,10 +444,8 @@ class Console:
Args: Args:
show (bool, optional): Set visibility of the cursor. show (bool, optional): Set visibility of the cursor.
""" """
if WINDOWS:
return
self._buffer.append(Segment("\033[?25h" if show else "\033[?25l"))
self._check_buffer() self._check_buffer()
self.file.write("\033[?25h" if show else "\033[?25l")
def _render( def _render(
self, self,
@ -642,17 +639,17 @@ class Console:
end: str, end: str,
emoji: bool = None, emoji: bool = None,
markup: bool = None, markup: bool = None,
highlight: bool = True, highlight: bool = None,
) -> List[ConsoleRenderable]: ) -> List[ConsoleRenderable]:
"""Combined a number of renderables and text in to one renderable. """Combined a number of renderables and text in to one renderable.
Args: Args:
renderables (Iterable[Union[str, ConsoleRenderable]]): [description] renderables (Iterable[Union[str, ConsoleRenderable]]): Anyting that Rich can render.
sep (str, optional): String to write between print data. Defaults to " ". sep (str, optional): String to write between print data. Defaults to " ".
end (str, optional): String to write at end of print data. Defaults to "\n". end (str, optional): String to write at end of print data. Defaults to "\n".
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. markup (Optional[bool], optional): Enable markup, or ``None`` to use console default.
highlight (bool, optional): Perform highlighting. Defaults to True. highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default.
Returns: Returns:
List[ConsoleRenderable]: A list of things to render. List[ConsoleRenderable]: A list of things to render.
@ -664,7 +661,7 @@ class Console:
append_text = text.append append_text = text.append
_highlighter: HighlighterType _highlighter: HighlighterType
if highlight: if highlight or (highlight is None and self._highlight):
_highlighter = self.highlighter _highlighter = self.highlighter
else: else:
_highlighter = _null_highlighter _highlighter = _null_highlighter
@ -723,7 +720,7 @@ class Console:
style: Union[str, Style] = None, style: Union[str, Style] = None,
emoji: bool = None, emoji: bool = None,
markup: bool = None, markup: bool = None,
highlight: bool = True, highlight: bool = None,
) -> None: ) -> None:
r"""Print to the console. r"""Print to the console.
@ -732,9 +729,9 @@ class Console:
sep (str, optional): String to write between print data. Defaults to " ". sep (str, optional): String to write between print data. Defaults to " ".
end (str, optional): String to write at end of print data. Defaults to "\n". end (str, optional): String to write at end of print data. Defaults to "\n".
style (Union[str, Style], optional): A style to apply to output. Defaults to None. style (Union[str, Style], optional): A style to apply to output. Defaults to None.
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to None
highlight (bool, optional): Perform highlighting. Defaults to True. highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to None.
""" """
if not objects: if not objects:
self.line() self.line()
@ -783,7 +780,7 @@ class Console:
end="\n", end="\n",
emoji: bool = None, emoji: bool = None,
markup: bool = None, markup: bool = None,
highlight: bool = True, highlight: bool = None,
log_locals: bool = False, log_locals: bool = False,
_stack_offset=1, _stack_offset=1,
) -> None: ) -> None:
@ -793,9 +790,9 @@ class Console:
objects (positional args): Objects to log to the terminal. objects (positional args): Objects to log to the terminal.
sep (str, optional): String to write between print data. Defaults to " ". sep (str, optional): String to write between print data. Defaults to " ".
end (str, optional): String to write at end of print data. Defaults to "\n". end (str, optional): String to write at end of print data. Defaults to "\n".
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to None.
highlight (bool, optional): Perform highlighting. Defaults to True. highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to None.
log_locals (bool, optional): Boolean to enable logging of locals where ``log()`` log_locals (bool, optional): Boolean to enable logging of locals where ``log()``
was called. Defaults to False. was called. Defaults to False.
_stack_offset (int, optional): Offset of caller from end of call stack. Defaults to 1. _stack_offset (int, optional): Offset of caller from end of call stack. Defaults to 1.

View file

@ -1,5 +1,6 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import deque from collections import deque
from collections.abc import Sized
from contextlib import contextmanager from contextlib import contextmanager
from dataclasses import dataclass, replace, field from dataclasses import dataclass, replace, field
from datetime import timedelta from datetime import timedelta
@ -37,25 +38,42 @@ ProgressType = TypeVar("ProgressType")
def track( def track(
sequence: Sequence[ProgressType], description="Working..." sequence: Union[Sequence[ProgressType], Iterable[ProgressType]],
description="Working...",
total: int = None,
auto_refresh=True,
) -> Iterable[ProgressType]: ) -> Iterable[ProgressType]:
"""Track progress of processing a sequence. """Track progress of processing a sequence.
Args: Args:
sequence (Sequence[ProgressType]): A sequence (must support "len") you wish to iterate over. sequence (Iterable[ProgressType]): A sequence (must support "len") you wish to iterate over.
description (str, optional): [description]. Defaults to "Working". 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: Returns:
Iterable[ProgressType]: An iterable of the values in the sequence. Iterable[ProgressType]: An iterable of the values in the sequence.
""" """
progress = Progress(auto_refresh=False) progress = Progress(auto_refresh=auto_refresh)
task_id = progress.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
task_id = progress.add_task(description, total=task_total)
with progress: with progress:
for completed, value in enumerate(sequence, 1): for completed, value in enumerate(sequence, 1):
yield value yield value
progress.update(task_id, completed=completed) progress.update(task_id, completed=completed)
progress.refresh() if not auto_refresh:
progress.refresh()
class ProgressColumn(ABC): class ProgressColumn(ABC):
@ -117,7 +135,7 @@ class TimeRemainingColumn(ProgressColumn):
"""Show time remaining.""" """Show time remaining."""
remaining = task.time_remaining remaining = task.time_remaining
if remaining is None: if remaining is None:
return Text("?", style="progress.remaining") return Text("-:--:--", style="progress.remaining")
remaining_delta = timedelta(seconds=int(remaining)) remaining_delta = timedelta(seconds=int(remaining))
return Text(str(remaining_delta), style="progress.remaining") return Text(str(remaining_delta), style="progress.remaining")
@ -278,6 +296,7 @@ class Progress:
self._lock = RLock() self._lock = RLock()
self._refresh_thread: Optional[_RefreshThread] = None self._refresh_thread: Optional[_RefreshThread] = None
self._refresh_count = 0 self._refresh_count = 0
self._enter_count = 0
@property @property
def tasks_ids(self) -> List[TaskID]: def tasks_ids(self) -> List[TaskID]:
@ -313,29 +332,56 @@ class Progress:
self.console.show_cursor(True) self.console.show_cursor(True)
def __enter__(self) -> "Progress": def __enter__(self) -> "Progress":
self.start() with self._lock:
return self 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: 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( def track(
self, sequence: Sequence[ProgressType], description="Working..." self,
sequence: Sequence[ProgressType],
total: int = None,
task_id: Optional[TaskID] = None,
description="Working...",
) -> Iterable[ProgressType]: ) -> Iterable[ProgressType]:
"""[summary] """[summary]
Args: Args:
sequence (Sequence[ProgressType]): [description] 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: Returns:
Iterable[ProgressType]: [description] 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: with self:
for completed, value in enumerate(sequence, 1): for completed, value in enumerate(sequence, 1):
yield value yield value
self.update(task_id, completed=completed) self.update(task_id, completed=completed)
self.refresh()
def start_task(self, task_id: TaskID) -> None: def start_task(self, task_id: TaskID) -> None:
"""Start a task. """Start a task.
@ -412,6 +458,15 @@ class Progress:
if refresh: if refresh:
self.refresh() self.refresh()
def advance(self, task_id: TaskID, advance: float = 1) -> None:
"""Advance task by a number of steps.
Args:
task_id (TaskID): ID of task.
advance (float): Number of steps to advance. Default is 1.
"""
self.update(task_id, advance=advance)
def refresh(self) -> None: def refresh(self) -> None:
"""Refresh (render) the progress information.""" """Refresh (render) the progress information."""
with self._lock: with self._lock:

View file

@ -1,5 +1,6 @@
from typing import NamedTuple, Optional from typing import NamedTuple, Optional
from .cells import cell_len, set_cell_size
from .style import Style from .style import Style
from itertools import zip_longest from itertools import zip_longest
@ -104,7 +105,7 @@ class Segment(NamedTuple):
Returns: Returns:
List[Segment]: A line of segments with the desired length. 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] new_line: List[Segment]
if line_length < length: if line_length < length:
@ -117,13 +118,14 @@ class Segment(NamedTuple):
append = new_line.append append = new_line.append
line_length = 0 line_length = 0
for segment in line: for segment in line:
segment_length = len(segment.text) segment_length = cell_len(segment.text)
if line_length + segment_length < length: if line_length + segment_length < length:
append(segment) append(segment)
line_length += segment_length line_length += segment_length
else: else:
text, style = segment text, style = segment
append(cls(text[: length - line_length], style)) text = set_cell_size(text, length - line_length)
append(cls(text, style))
break break
else: else:
new_line = line[:] new_line = line[:]
@ -139,7 +141,7 @@ class Segment(NamedTuple):
Returns: Returns:
int: The length of the line. int: The length of the line.
""" """
return sum(len(text) for text, _ in line) return sum(cell_len(text) for text, _ in line)
@classmethod @classmethod
def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]: def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]:

View file

@ -73,6 +73,7 @@ class Syntax:
def from_path( def from_path(
cls, cls,
path: str, path: str,
encoding: str = "utf-8",
theme: Union[str, PygmentsStyle] = DEFAULT_THEME, theme: Union[str, PygmentsStyle] = DEFAULT_THEME,
dedent: bool = True, dedent: bool = True,
line_numbers: bool = False, line_numbers: bool = False,
@ -86,6 +87,7 @@ class Syntax:
Args: Args:
path (str): Path to file to highlight. path (str): Path to file to highlight.
encoding (str): Encoding of file.
lexer_name (str): Lexer to use (see https://pygments.org/docs/lexers/) lexer_name (str): Lexer to use (see https://pygments.org/docs/lexers/)
theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs". theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs".
dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True. dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True.
@ -99,7 +101,7 @@ class Syntax:
Returns: Returns:
[Syntax]: A Syntax object that may be printed to the console [Syntax]: A Syntax object that may be printed to the console
""" """
with open(path, "rt") as code_file: with open(path, "rt", encoding=encoding) as code_file:
code = code_file.read() code = code_file.read()
try: try:
lexer = guess_lexer_for_filename(path, code) lexer = guess_lexer_for_filename(path, code)
@ -242,7 +244,7 @@ class Syntax:
padding = _Segment(" " * numbers_column_width, background_style) padding = _Segment(" " * numbers_column_width, background_style)
new_line = _Segment("\n") new_line = _Segment("\n")
line_pointer = "->" if WINDOWS else "" line_pointer = ""
for line_no, line in enumerate(lines, self.start_line + line_offset): for line_no, line in enumerate(lines, self.start_line + line_offset):
wrapped_lines = console.render_lines( wrapped_lines = console.render_lines(

View file

@ -461,8 +461,8 @@ class Table:
if __name__ == "__main__": if __name__ == "__main__":
from .console import Console from .console import Console
c = Console(width=80) c = Console()
table = Table() table = Table(expand=True)
table.add_column(no_wrap=True) table.add_column(no_wrap=True)
table.add_column() table.add_column()
table.add_row( table.add_row(

View file

@ -23,6 +23,7 @@ if TYPE_CHECKING: # pragma: no cover
RenderableType, RenderableType,
) )
from .cells import cell_len
from .containers import Lines from .containers import Lines
from .style import Style from .style import Style
from .segment import Segment from .segment import Segment
@ -322,9 +323,9 @@ class Text:
def __measure__(self, console: "Console", max_width: int) -> Measurement: def __measure__(self, console: "Console", max_width: int) -> Measurement:
text = self.text text = self.text
if not text.strip(): if not text.strip():
return Measurement(len(text), len(text)) return Measurement(cell_len(text), cell_len(text))
max_text_width = max(len(line) for line in text.splitlines()) max_text_width = max(cell_len(line) for line in text.splitlines())
min_text_width = max(len(word) for word in text.split()) min_text_width = max(cell_len(word) for word in text.split())
return Measurement(min_text_width, max_text_width) return Measurement(min_text_width, max_text_width)
def _render_line( def _render_line(

View file

@ -281,7 +281,7 @@ if __name__ == "__main__": # pragma: no cover
console = Console() console = Console()
import sys import sys
def bar(a): def bar(a): # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑
print(1 / a) print(1 / a)
def foo(a): def foo(a):

View file

@ -0,0 +1,91 @@
import subprocess
from typing import List, Tuple
import sys
from rich.progress import Progress
from wcwidth import wcwidth
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)
)
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:
end_codepoint = codepoint
append((start_codepoint, end_codepoint, group_cell_size))
return table
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():
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__":
run()