segment.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. from enum import IntEnum
  2. from functools import lru_cache
  3. from itertools import filterfalse
  4. from logging import getLogger
  5. from operator import attrgetter
  6. from typing import (
  7. TYPE_CHECKING,
  8. Dict,
  9. Iterable,
  10. List,
  11. NamedTuple,
  12. Optional,
  13. Sequence,
  14. Tuple,
  15. Type,
  16. Union,
  17. )
  18. from .cells import (
  19. _is_single_cell_widths,
  20. cached_cell_len,
  21. cell_len,
  22. get_character_cell_size,
  23. set_cell_size,
  24. )
  25. from .repr import Result, rich_repr
  26. from .style import Style
  27. if TYPE_CHECKING:
  28. from .console import Console, ConsoleOptions, RenderResult
  29. log = getLogger("rich")
  30. class ControlType(IntEnum):
  31. """Non-printable control codes which typically translate to ANSI codes."""
  32. BELL = 1
  33. CARRIAGE_RETURN = 2
  34. HOME = 3
  35. CLEAR = 4
  36. SHOW_CURSOR = 5
  37. HIDE_CURSOR = 6
  38. ENABLE_ALT_SCREEN = 7
  39. DISABLE_ALT_SCREEN = 8
  40. CURSOR_UP = 9
  41. CURSOR_DOWN = 10
  42. CURSOR_FORWARD = 11
  43. CURSOR_BACKWARD = 12
  44. CURSOR_MOVE_TO_COLUMN = 13
  45. CURSOR_MOVE_TO = 14
  46. ERASE_IN_LINE = 15
  47. SET_WINDOW_TITLE = 16
  48. ControlCode = Union[
  49. Tuple[ControlType],
  50. Tuple[ControlType, Union[int, str]],
  51. Tuple[ControlType, int, int],
  52. ]
  53. @rich_repr()
  54. class Segment(NamedTuple):
  55. """A piece of text with associated style. Segments are produced by the Console render process and
  56. are ultimately converted in to strings to be written to the terminal.
  57. Args:
  58. text (str): A piece of text.
  59. style (:class:`~rich.style.Style`, optional): An optional style to apply to the text.
  60. control (Tuple[ControlCode], optional): Optional sequence of control codes.
  61. Attributes:
  62. cell_length (int): The cell length of this Segment.
  63. """
  64. text: str
  65. style: Optional[Style] = None
  66. control: Optional[Sequence[ControlCode]] = None
  67. @property
  68. def cell_length(self) -> int:
  69. """The number of terminal cells required to display self.text.
  70. Returns:
  71. int: A number of cells.
  72. """
  73. text, _style, control = self
  74. return 0 if control else cell_len(text)
  75. def __rich_repr__(self) -> Result:
  76. yield self.text
  77. if self.control is None:
  78. if self.style is not None:
  79. yield self.style
  80. else:
  81. yield self.style
  82. yield self.control
  83. def __bool__(self) -> bool:
  84. """Check if the segment contains text."""
  85. return bool(self.text)
  86. @property
  87. def is_control(self) -> bool:
  88. """Check if the segment contains control codes."""
  89. return self.control is not None
  90. @classmethod
  91. @lru_cache(1024 * 16)
  92. def _split_cells(cls, segment: "Segment", cut: int) -> Tuple["Segment", "Segment"]:
  93. """Split a segment in to two at a given cell position.
  94. Note that splitting a double-width character, may result in that character turning
  95. into two spaces.
  96. Args:
  97. segment (Segment): A segment to split.
  98. cut (int): A cell position to cut on.
  99. Returns:
  100. A tuple of two segments.
  101. """
  102. text, style, control = segment
  103. _Segment = Segment
  104. cell_length = segment.cell_length
  105. if cut >= cell_length:
  106. return segment, _Segment("", style, control)
  107. cell_size = get_character_cell_size
  108. pos = int((cut / cell_length) * len(text))
  109. while True:
  110. before = text[:pos]
  111. cell_pos = cell_len(before)
  112. out_by = cell_pos - cut
  113. if not out_by:
  114. return (
  115. _Segment(before, style, control),
  116. _Segment(text[pos:], style, control),
  117. )
  118. if out_by == -1 and cell_size(text[pos]) == 2:
  119. return (
  120. _Segment(text[:pos] + " ", style, control),
  121. _Segment(" " + text[pos + 1 :], style, control),
  122. )
  123. if out_by == +1 and cell_size(text[pos - 1]) == 2:
  124. return (
  125. _Segment(text[: pos - 1] + " ", style, control),
  126. _Segment(" " + text[pos:], style, control),
  127. )
  128. if cell_pos < cut:
  129. pos += 1
  130. else:
  131. pos -= 1
  132. def split_cells(self, cut: int) -> Tuple["Segment", "Segment"]:
  133. """Split segment in to two segments at the specified column.
  134. If the cut point falls in the middle of a 2-cell wide character then it is replaced
  135. by two spaces, to preserve the display width of the parent segment.
  136. Args:
  137. cut (int): Offset within the segment to cut.
  138. Returns:
  139. Tuple[Segment, Segment]: Two segments.
  140. """
  141. text, style, control = self
  142. assert cut >= 0
  143. if _is_single_cell_widths(text):
  144. # Fast path with all 1 cell characters
  145. if cut >= len(text):
  146. return self, Segment("", style, control)
  147. return (
  148. Segment(text[:cut], style, control),
  149. Segment(text[cut:], style, control),
  150. )
  151. return self._split_cells(self, cut)
  152. @classmethod
  153. def line(cls) -> "Segment":
  154. """Make a new line segment."""
  155. return cls("\n")
  156. @classmethod
  157. def apply_style(
  158. cls,
  159. segments: Iterable["Segment"],
  160. style: Optional[Style] = None,
  161. post_style: Optional[Style] = None,
  162. ) -> Iterable["Segment"]:
  163. """Apply style(s) to an iterable of segments.
  164. Returns an iterable of segments where the style is replaced by ``style + segment.style + post_style``.
  165. Args:
  166. segments (Iterable[Segment]): Segments to process.
  167. style (Style, optional): Base style. Defaults to None.
  168. post_style (Style, optional): Style to apply on top of segment style. Defaults to None.
  169. Returns:
  170. Iterable[Segments]: A new iterable of segments (possibly the same iterable).
  171. """
  172. result_segments = segments
  173. if style:
  174. apply = style.__add__
  175. result_segments = (
  176. cls(text, None if control else apply(_style), control)
  177. for text, _style, control in result_segments
  178. )
  179. if post_style:
  180. result_segments = (
  181. cls(
  182. text,
  183. (
  184. None
  185. if control
  186. else (_style + post_style if _style else post_style)
  187. ),
  188. control,
  189. )
  190. for text, _style, control in result_segments
  191. )
  192. return result_segments
  193. @classmethod
  194. def filter_control(
  195. cls, segments: Iterable["Segment"], is_control: bool = False
  196. ) -> Iterable["Segment"]:
  197. """Filter segments by ``is_control`` attribute.
  198. Args:
  199. segments (Iterable[Segment]): An iterable of Segment instances.
  200. is_control (bool, optional): is_control flag to match in search.
  201. Returns:
  202. Iterable[Segment]: And iterable of Segment instances.
  203. """
  204. if is_control:
  205. return filter(attrgetter("control"), segments)
  206. else:
  207. return filterfalse(attrgetter("control"), segments)
  208. @classmethod
  209. def split_lines(cls, segments: Iterable["Segment"]) -> Iterable[List["Segment"]]:
  210. """Split a sequence of segments in to a list of lines.
  211. Args:
  212. segments (Iterable[Segment]): Segments potentially containing line feeds.
  213. Yields:
  214. Iterable[List[Segment]]: Iterable of segment lists, one per line.
  215. """
  216. line: List[Segment] = []
  217. append = line.append
  218. for segment in segments:
  219. if "\n" in segment.text and not segment.control:
  220. text, style, _ = segment
  221. while text:
  222. _text, new_line, text = text.partition("\n")
  223. if _text:
  224. append(cls(_text, style))
  225. if new_line:
  226. yield line
  227. line = []
  228. append = line.append
  229. else:
  230. append(segment)
  231. if line:
  232. yield line
  233. @classmethod
  234. def split_and_crop_lines(
  235. cls,
  236. segments: Iterable["Segment"],
  237. length: int,
  238. style: Optional[Style] = None,
  239. pad: bool = True,
  240. include_new_lines: bool = True,
  241. ) -> Iterable[List["Segment"]]:
  242. """Split segments in to lines, and crop lines greater than a given length.
  243. Args:
  244. segments (Iterable[Segment]): An iterable of segments, probably
  245. generated from console.render.
  246. length (int): Desired line length.
  247. style (Style, optional): Style to use for any padding.
  248. pad (bool): Enable padding of lines that are less than `length`.
  249. Returns:
  250. Iterable[List[Segment]]: An iterable of lines of segments.
  251. """
  252. line: List[Segment] = []
  253. append = line.append
  254. adjust_line_length = cls.adjust_line_length
  255. new_line_segment = cls("\n")
  256. for segment in segments:
  257. if "\n" in segment.text and not segment.control:
  258. text, segment_style, _ = segment
  259. while text:
  260. _text, new_line, text = text.partition("\n")
  261. if _text:
  262. append(cls(_text, segment_style))
  263. if new_line:
  264. cropped_line = adjust_line_length(
  265. line, length, style=style, pad=pad
  266. )
  267. if include_new_lines:
  268. cropped_line.append(new_line_segment)
  269. yield cropped_line
  270. line.clear()
  271. else:
  272. append(segment)
  273. if line:
  274. yield adjust_line_length(line, length, style=style, pad=pad)
  275. @classmethod
  276. def adjust_line_length(
  277. cls,
  278. line: List["Segment"],
  279. length: int,
  280. style: Optional[Style] = None,
  281. pad: bool = True,
  282. ) -> List["Segment"]:
  283. """Adjust a line to a given width (cropping or padding as required).
  284. Args:
  285. segments (Iterable[Segment]): A list of segments in a single line.
  286. length (int): The desired width of the line.
  287. style (Style, optional): The style of padding if used (space on the end). Defaults to None.
  288. pad (bool, optional): Pad lines with spaces if they are shorter than `length`. Defaults to True.
  289. Returns:
  290. List[Segment]: A line of segments with the desired length.
  291. """
  292. line_length = sum(segment.cell_length for segment in line)
  293. new_line: List[Segment]
  294. if line_length < length:
  295. if pad:
  296. new_line = line + [cls(" " * (length - line_length), style)]
  297. else:
  298. new_line = line[:]
  299. elif line_length > length:
  300. new_line = []
  301. append = new_line.append
  302. line_length = 0
  303. for segment in line:
  304. segment_length = segment.cell_length
  305. if line_length + segment_length < length or segment.control:
  306. append(segment)
  307. line_length += segment_length
  308. else:
  309. text, segment_style, _ = segment
  310. text = set_cell_size(text, length - line_length)
  311. append(cls(text, segment_style))
  312. break
  313. else:
  314. new_line = line[:]
  315. return new_line
  316. @classmethod
  317. def get_line_length(cls, line: List["Segment"]) -> int:
  318. """Get the length of list of segments.
  319. Args:
  320. line (List[Segment]): A line encoded as a list of Segments (assumes no '\\\\n' characters),
  321. Returns:
  322. int: The length of the line.
  323. """
  324. _cell_len = cell_len
  325. return sum(_cell_len(text) for text, style, control in line if not control)
  326. @classmethod
  327. def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]:
  328. """Get the shape (enclosing rectangle) of a list of lines.
  329. Args:
  330. lines (List[List[Segment]]): A list of lines (no '\\\\n' characters).
  331. Returns:
  332. Tuple[int, int]: Width and height in characters.
  333. """
  334. get_line_length = cls.get_line_length
  335. max_width = max(get_line_length(line) for line in lines) if lines else 0
  336. return (max_width, len(lines))
  337. @classmethod
  338. def set_shape(
  339. cls,
  340. lines: List[List["Segment"]],
  341. width: int,
  342. height: Optional[int] = None,
  343. style: Optional[Style] = None,
  344. new_lines: bool = False,
  345. ) -> List[List["Segment"]]:
  346. """Set the shape of a list of lines (enclosing rectangle).
  347. Args:
  348. lines (List[List[Segment]]): A list of lines.
  349. width (int): Desired width.
  350. height (int, optional): Desired height or None for no change.
  351. style (Style, optional): Style of any padding added.
  352. new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
  353. Returns:
  354. List[List[Segment]]: New list of lines.
  355. """
  356. _height = height or len(lines)
  357. blank = (
  358. [cls(" " * width + "\n", style)] if new_lines else [cls(" " * width, style)]
  359. )
  360. adjust_line_length = cls.adjust_line_length
  361. shaped_lines = lines[:_height]
  362. shaped_lines[:] = [
  363. adjust_line_length(line, width, style=style) for line in lines
  364. ]
  365. if len(shaped_lines) < _height:
  366. shaped_lines.extend([blank] * (_height - len(shaped_lines)))
  367. return shaped_lines
  368. @classmethod
  369. def align_top(
  370. cls: Type["Segment"],
  371. lines: List[List["Segment"]],
  372. width: int,
  373. height: int,
  374. style: Style,
  375. new_lines: bool = False,
  376. ) -> List[List["Segment"]]:
  377. """Aligns lines to top (adds extra lines to bottom as required).
  378. Args:
  379. lines (List[List[Segment]]): A list of lines.
  380. width (int): Desired width.
  381. height (int, optional): Desired height or None for no change.
  382. style (Style): Style of any padding added.
  383. new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
  384. Returns:
  385. List[List[Segment]]: New list of lines.
  386. """
  387. extra_lines = height - len(lines)
  388. if not extra_lines:
  389. return lines[:]
  390. lines = lines[:height]
  391. blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
  392. lines = lines + [[blank]] * extra_lines
  393. return lines
  394. @classmethod
  395. def align_bottom(
  396. cls: Type["Segment"],
  397. lines: List[List["Segment"]],
  398. width: int,
  399. height: int,
  400. style: Style,
  401. new_lines: bool = False,
  402. ) -> List[List["Segment"]]:
  403. """Aligns render to bottom (adds extra lines above as required).
  404. Args:
  405. lines (List[List[Segment]]): A list of lines.
  406. width (int): Desired width.
  407. height (int, optional): Desired height or None for no change.
  408. style (Style): Style of any padding added. Defaults to None.
  409. new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
  410. Returns:
  411. List[List[Segment]]: New list of lines.
  412. """
  413. extra_lines = height - len(lines)
  414. if not extra_lines:
  415. return lines[:]
  416. lines = lines[:height]
  417. blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
  418. lines = [[blank]] * extra_lines + lines
  419. return lines
  420. @classmethod
  421. def align_middle(
  422. cls: Type["Segment"],
  423. lines: List[List["Segment"]],
  424. width: int,
  425. height: int,
  426. style: Style,
  427. new_lines: bool = False,
  428. ) -> List[List["Segment"]]:
  429. """Aligns lines to middle (adds extra lines to above and below as required).
  430. Args:
  431. lines (List[List[Segment]]): A list of lines.
  432. width (int): Desired width.
  433. height (int, optional): Desired height or None for no change.
  434. style (Style): Style of any padding added.
  435. new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
  436. Returns:
  437. List[List[Segment]]: New list of lines.
  438. """
  439. extra_lines = height - len(lines)
  440. if not extra_lines:
  441. return lines[:]
  442. lines = lines[:height]
  443. blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
  444. top_lines = extra_lines // 2
  445. bottom_lines = extra_lines - top_lines
  446. lines = [[blank]] * top_lines + lines + [[blank]] * bottom_lines
  447. return lines
  448. @classmethod
  449. def simplify(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
  450. """Simplify an iterable of segments by combining contiguous segments with the same style.
  451. Args:
  452. segments (Iterable[Segment]): An iterable of segments.
  453. Returns:
  454. Iterable[Segment]: A possibly smaller iterable of segments that will render the same way.
  455. """
  456. iter_segments = iter(segments)
  457. try:
  458. last_segment = next(iter_segments)
  459. except StopIteration:
  460. return
  461. _Segment = Segment
  462. for segment in iter_segments:
  463. if last_segment.style == segment.style and not segment.control:
  464. last_segment = _Segment(
  465. last_segment.text + segment.text, last_segment.style
  466. )
  467. else:
  468. yield last_segment
  469. last_segment = segment
  470. yield last_segment
  471. @classmethod
  472. def strip_links(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
  473. """Remove all links from an iterable of styles.
  474. Args:
  475. segments (Iterable[Segment]): An iterable segments.
  476. Yields:
  477. Segment: Segments with link removed.
  478. """
  479. for segment in segments:
  480. if segment.control or segment.style is None:
  481. yield segment
  482. else:
  483. text, style, _control = segment
  484. yield cls(text, style.update_link(None) if style else None)
  485. @classmethod
  486. def strip_styles(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
  487. """Remove all styles from an iterable of segments.
  488. Args:
  489. segments (Iterable[Segment]): An iterable segments.
  490. Yields:
  491. Segment: Segments with styles replace with None
  492. """
  493. for text, _style, control in segments:
  494. yield cls(text, None, control)
  495. @classmethod
  496. def remove_color(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
  497. """Remove all color from an iterable of segments.
  498. Args:
  499. segments (Iterable[Segment]): An iterable segments.
  500. Yields:
  501. Segment: Segments with colorless style.
  502. """
  503. cache: Dict[Style, Style] = {}
  504. for text, style, control in segments:
  505. if style:
  506. colorless_style = cache.get(style)
  507. if colorless_style is None:
  508. colorless_style = style.without_color
  509. cache[style] = colorless_style
  510. yield cls(text, colorless_style, control)
  511. else:
  512. yield cls(text, None, control)
  513. @classmethod
  514. def divide(
  515. cls, segments: Iterable["Segment"], cuts: Iterable[int]
  516. ) -> Iterable[List["Segment"]]:
  517. """Divides an iterable of segments in to portions.
  518. Args:
  519. cuts (Iterable[int]): Cell positions where to divide.
  520. Yields:
  521. [Iterable[List[Segment]]]: An iterable of Segments in List.
  522. """
  523. split_segments: List["Segment"] = []
  524. add_segment = split_segments.append
  525. iter_cuts = iter(cuts)
  526. while True:
  527. cut = next(iter_cuts, -1)
  528. if cut == -1:
  529. return
  530. if cut != 0:
  531. break
  532. yield []
  533. pos = 0
  534. segments_clear = split_segments.clear
  535. segments_copy = split_segments.copy
  536. _cell_len = cached_cell_len
  537. for segment in segments:
  538. text, _style, control = segment
  539. while text:
  540. end_pos = pos if control else pos + _cell_len(text)
  541. if end_pos < cut:
  542. add_segment(segment)
  543. pos = end_pos
  544. break
  545. if end_pos == cut:
  546. add_segment(segment)
  547. yield segments_copy()
  548. segments_clear()
  549. pos = end_pos
  550. cut = next(iter_cuts, -1)
  551. if cut == -1:
  552. if split_segments:
  553. yield segments_copy()
  554. return
  555. break
  556. else:
  557. before, segment = segment.split_cells(cut - pos)
  558. text, _style, control = segment
  559. add_segment(before)
  560. yield segments_copy()
  561. segments_clear()
  562. pos = cut
  563. cut = next(iter_cuts, -1)
  564. if cut == -1:
  565. if split_segments:
  566. yield segments_copy()
  567. return
  568. yield segments_copy()
  569. class Segments:
  570. """A simple renderable to render an iterable of segments. This class may be useful if
  571. you want to print segments outside of a __rich_console__ method.
  572. Args:
  573. segments (Iterable[Segment]): An iterable of segments.
  574. new_lines (bool, optional): Add new lines between segments. Defaults to False.
  575. """
  576. def __init__(self, segments: Iterable[Segment], new_lines: bool = False) -> None:
  577. self.segments = list(segments)
  578. self.new_lines = new_lines
  579. def __rich_console__(
  580. self, console: "Console", options: "ConsoleOptions"
  581. ) -> "RenderResult":
  582. if self.new_lines:
  583. line = Segment.line()
  584. for segment in self.segments:
  585. yield segment
  586. yield line
  587. else:
  588. yield from self.segments
  589. class SegmentLines:
  590. def __init__(self, lines: Iterable[List[Segment]], new_lines: bool = False) -> None:
  591. """A simple renderable containing a number of lines of segments. May be used as an intermediate
  592. in rendering process.
  593. Args:
  594. lines (Iterable[List[Segment]]): Lists of segments forming lines.
  595. new_lines (bool, optional): Insert new lines after each line. Defaults to False.
  596. """
  597. self.lines = list(lines)
  598. self.new_lines = new_lines
  599. def __rich_console__(
  600. self, console: "Console", options: "ConsoleOptions"
  601. ) -> "RenderResult":
  602. if self.new_lines:
  603. new_line = Segment.line()
  604. for line in self.lines:
  605. yield from line
  606. yield new_line
  607. else:
  608. for line in self.lines:
  609. yield from line
  610. if __name__ == "__main__": # pragma: no cover
  611. from rich.console import Console
  612. from rich.syntax import Syntax
  613. from rich.text import Text
  614. code = """from rich.console import Console
  615. console = Console()
  616. text = Text.from_markup("Hello, [bold magenta]World[/]!")
  617. console.print(text)"""
  618. text = Text.from_markup("Hello, [bold magenta]World[/]!")
  619. console = Console()
  620. console.rule("rich.Segment")
  621. console.print(
  622. "A Segment is the last step in the Rich render process before generating text with ANSI codes."
  623. )
  624. console.print("\nConsider the following code:\n")
  625. console.print(Syntax(code, "python", line_numbers=True))
  626. console.print()
  627. console.print(
  628. "When you call [b]print()[/b], Rich [i]renders[/i] the object in to the following:\n"
  629. )
  630. fragments = list(console.render(text))
  631. console.print(fragments)
  632. console.print()
  633. console.print("The Segments are then processed to produce the following output:\n")
  634. console.print(text)
  635. console.print(
  636. "\nYou will only need to know this if you are implementing your own Rich renderables."
  637. )