summaryrefslogtreecommitdiffstats
path: root/rich
diff options
context:
space:
mode:
Diffstat (limited to 'rich')
-rw-r--r--rich/__init__.py120
-rw-r--r--rich/__main__.py277
-rw-r--r--rich/_cell_widths.py451
-rw-r--r--rich/_emoji_codes.py3610
-rw-r--r--rich/_emoji_replace.py17
-rw-r--r--rich/_inspect.py251
-rw-r--r--rich/_log_render.py88
-rw-r--r--rich/_loop.py43
-rw-r--r--rich/_lru_cache.py34
-rw-r--r--rich/_palettes.py309
-rw-r--r--rich/_pick.py17
-rw-r--r--rich/_ratio.py134
-rw-r--r--rich/_spinners.py848
-rw-r--r--rich/_stack.py16
-rw-r--r--rich/_timer.py18
-rw-r--r--rich/_windows.py71
-rw-r--r--rich/_wrap.py55
-rw-r--r--rich/abc.py33
-rw-r--r--rich/align.py304
-rw-r--r--rich/ansi.py228
-rw-r--r--rich/bar.py89
-rw-r--r--rich/box.py478
-rw-r--r--rich/cells.py124
-rw-r--r--rich/color.py575
-rw-r--r--rich/color_triplet.py38
-rw-r--r--rich/columns.py189
-rw-r--r--rich/console.py1821
-rw-r--r--rich/constrain.py35
-rw-r--r--rich/containers.py161
-rw-r--r--rich/control.py57
-rw-r--r--rich/default_styles.py154
-rw-r--r--rich/diagnose.py6
-rw-r--r--rich/emoji.py74
-rw-r--r--rich/errors.py30
-rw-r--r--rich/file_proxy.py54
-rw-r--r--rich/filesize.py62
-rw-r--r--rich/highlighter.py131
-rw-r--r--rich/jupyter.py79
-rw-r--r--rich/layout.py238
-rw-r--r--rich/live.py366
-rw-r--r--rich/live_render.py93
-rw-r--r--rich/logging.py256
-rw-r--r--rich/markdown.py620
-rw-r--r--rich/markup.py179
-rw-r--r--rich/measure.py149
-rw-r--r--rich/padding.py124
-rw-r--r--rich/pager.py33
-rw-r--r--rich/palette.py100
-rw-r--r--rich/panel.py206
-rw-r--r--rich/pretty.py585
-rw-r--r--rich/progress.py1019
-rw-r--r--rich/progress_bar.py214
-rw-r--r--rich/prompt.py378
-rw-r--r--rich/protocol.py8
-rw-r--r--rich/py.typed0
-rw-r--r--rich/rule.py115
-rw-r--r--rich/scope.py86
-rw-r--r--rich/screen.py40
-rw-r--r--rich/segment.py392
-rw-r--r--rich/spinner.py88
-rw-r--r--rich/status.py131
-rw-r--r--rich/style.py694
-rw-r--r--rich/styled.py40
-rw-r--r--rich/syntax.py669
-rw-r--r--rich/table.py881
-rw-r--r--rich/tabulate.py75
-rw-r--r--rich/terminal_theme.py55
-rw-r--r--rich/text.py1133
-rw-r--r--rich/theme.py110
-rw-r--r--rich/themes.py5
-rw-r--r--rich/traceback.py621
-rw-r--r--rich/tree.py240
72 files changed, 21024 insertions, 0 deletions
diff --git a/rich/__init__.py b/rich/__init__.py
new file mode 100644
index 0000000..b0e4c8d
--- /dev/null
+++ b/rich/__init__.py
@@ -0,0 +1,120 @@
+"""Rich text and beautiful formatting in the terminal."""
+
+import os
+from typing import Any, IO, Optional, TYPE_CHECKING
+
+__all__ = ["get_console", "reconfigure", "print", "inspect"]
+
+if TYPE_CHECKING:
+ from .console import Console
+
+# Global console used by alternative print
+_console: Optional["Console"] = None
+
+_IMPORT_CWD = os.path.abspath(os.getcwd())
+
+
+def get_console() -> "Console":
+ """Get a global :class:`~rich.console.Console` instance. This function is used when Rich requires a Console,
+ and hasn't been explicitly given one.
+
+ Returns:
+ Console: A console instance.
+ """
+ global _console
+ if _console is None:
+ from .console import Console
+
+ _console = Console()
+
+ return _console
+
+
+def reconfigure(*args, **kwargs) -> None:
+ """Reconfigures the global console bu replacing it with another.
+
+ Args:
+ console (Console): Replacement console instance.
+ """
+ from rich.console import Console
+
+ new_console = Console(*args, **kwargs)
+ _console.__dict__ = new_console.__dict__
+
+
+def print(*objects: Any, sep=" ", end="\n", file: IO[str] = None, flush: bool = False):
+ r"""Print object(s) supplied via positional arguments.
+ This function has an identical signature to the built-in print.
+ For more advanced features, see the :class:`~rich.console.Console` class.
+
+ Args:
+ sep (str, optional): Separator between printed objects. Defaults to " ".
+ end (str, optional): Character to write at end of output. Defaults to "\\n".
+ file (IO[str], optional): File to write to, or None for stdout. Defaults to None.
+ flush (bool, optional): Has no effect as Rich always flushes output. Defaults to False.
+
+ """
+ from .console import Console
+
+ write_console = get_console() if file is None else Console(file=file)
+ return write_console.print(*objects, sep=sep, end=end)
+
+
+def inspect(
+ obj: Any,
+ *,
+ console: "Console" = None,
+ title: str = None,
+ help: bool = False,
+ methods: bool = False,
+ docs: bool = True,
+ private: bool = False,
+ dunder: bool = False,
+ sort: bool = True,
+ all: bool = False,
+ value: bool = True
+):
+ """Inspect any Python object.
+
+ * inspect(<OBJECT>) to see summarized info.
+ * inspect(<OBJECT>, methods=True) to see methods.
+ * inspect(<OBJECT>, help=True) to see full (non-abbreviated) help.
+ * inspect(<OBJECT>, private=True) to see private attributes (single underscore).
+ * inspect(<OBJECT>, dunder=True) to see attributes beginning with double underscore.
+ * inspect(<OBJECT>, all=True) to see all attributes.
+
+ Args:
+ obj (Any): An object to inspect.
+ title (str, optional): Title to display over inspect result, or None use type. Defaults to None.
+ help (bool, optional): Show full help text rather than just first paragraph. Defaults to False.
+ methods (bool, optional): Enable inspection of callables. Defaults to False.
+ docs (bool, optional): Also render doc strings. Defaults to True.
+ private (bool, optional): Show private attributes (beginning with underscore). Defaults to False.
+ dunder (bool, optional): Show attributes starting with double underscore. Defaults to False.
+ sort (bool, optional): Sort attributes alphabetically. Defaults to True.
+ all (bool, optional): Show all attributes. Defaults to False.
+ value (bool, optional): Pretty print value. Defaults to True.
+ """
+ _console = console or get_console()
+ from rich._inspect import Inspect
+
+ # Special case for inspect(inspect)
+ is_inspect = obj is inspect
+
+ _inspect = Inspect(
+ obj,
+ title=title,
+ help=is_inspect or help,
+ methods=is_inspect or methods,
+ docs=is_inspect or docs,
+ private=private,
+ dunder=dunder,
+ sort=sort,
+ all=all,
+ value=value,
+ )
+ _console.print(_inspect)
+
+
+if __name__ == "__main__": # pragma: no cover
+ print("Hello, **World**")
diff --git a/rich/__main__.py b/rich/__main__.py
new file mode 100644
index 0000000..411c1f5
--- /dev/null
+++ b/rich/__main__.py
@@ -0,0 +1,277 @@
+import colorsys
+import io
+from time import process_time
+
+from rich import box
+from rich.color import Color
+from rich.console import Console, ConsoleOptions, RenderGroup, RenderResult
+from rich.markdown import Markdown
+from rich.measure import Measurement
+from rich.pretty import Pretty
+from rich.segment import Segment
+from rich.style import Style
+from rich.syntax import Syntax
+from rich.table import Table
+from rich.text import Text
+
+
+class ColorBox:
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ for y in range(0, 5):
+ for x in range(options.max_width):
+ h = x / options.max_width
+ l = 0.1 + ((y / 5) * 0.7)
+ r1, g1, b1 = colorsys.hls_to_rgb(h, l, 1.0)
+ r2, g2, b2 = colorsys.hls_to_rgb(h, l + 0.7 / 10, 1.0)
+ bgcolor = Color.from_rgb(r1 * 255, g1 * 255, b1 * 255)
+ color = Color.from_rgb(r2 * 255, g2 * 255, b2 * 255)
+ yield Segment("▄", Style(color=color, bgcolor=bgcolor))
+ yield Segment.line()
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
+ return Measurement(1, max_width)
+
+
+def make_test_card() -> Table:
+ """Get a renderable that demonstrates a number of features."""
+ table = Table.grid(padding=1, pad_edge=True)
+ table.title = "Rich features"
+ table.add_column("Feature", no_wrap=True, justify="center", style="bold red")
+ table.add_column("Demonstration")
+
+ color_table = Table(
+ box=None,
+ expand=False,
+ show_header=False,
+ show_edge=False,
+ pad_edge=False,
+ )
+ color_table.add_row(
+ # "[bold yellow]256[/] colors or [bold green]16.7 million[/] colors [blue](if supported by your terminal)[/].",
+ (
+ "✓ [bold green]4-bit color[/]\n"
+ "✓ [bold blue]8-bit color[/]\n"
+ "✓ [bold magenta]Truecolor (16.7 million)[/]\n"
+ "✓ [bold yellow]Dumb terminals[/]\n"
+ "✓ [bold cyan]Automatic color conversion"
+ ),
+ ColorBox(),
+ )
+
+ table.add_row("Colors", color_table)
+
+ table.add_row(
+ "Styles",
+ "All ansi styles: [bold]bold[/], [dim]dim[/], [italic]italic[/italic], [underline]underline[/], [strike]strikethrough[/], [reverse]reverse[/], and even [blink]blink[/].",
+ )
+
+ lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque in metus sed sapien ultricies pretium a at justo. Maecenas luctus velit et auctor maximus."
+ lorem_table = Table.grid(padding=1, collapse_padding=True)
+ lorem_table.pad_edge = False
+ lorem_table.add_row(
+ Text(lorem, justify="left", style="green"),
+ Text(lorem, justify="center", style="yellow"),
+ Text(lorem, justify="right", style="blue"),
+ Text(lorem, justify="full", style="red"),
+ )
+ table.add_row(
+ "Text",
+ RenderGroup(
+ Text.from_markup(
+ """Word wrap text. Justify [green]left[/], [yellow]center[/], [blue]right[/] or [red]full[/].\n"""
+ ),
+ lorem_table,
+ ),
+ )
+
+ def comparison(renderable1, renderable2) -> Table:
+ table = Table(show_header=False, pad_edge=False, box=None, expand=True)
+ table.add_column("1", ratio=1)
+ table.add_column("2", ratio=1)
+ table.add_row(renderable1, renderable2)
+ return table
+
+ table.add_row(
+ "Asian\nlanguage\nsupport",
+ ":flag_for_china: 该库支持中文,日文和韩文文本!\n:flag_for_japan: ライブラリは中国語、日本語、韓国語のテキストをサポートしています\n:flag_for_south_korea: 이 라이브러리는 중국어, 일본어 및 한국어 텍스트를 지원합니다",
+ )
+
+ markup_example = (
+ "[bold magenta]Rich[/] supports a simple [i]bbcode[/i] like [b]markup[/b] for [yellow]color[/], [underline]style[/], and emoji! "
+ ":+1: :apple: :ant: :bear: :baguette_bread: :bus: "
+ )
+ table.add_row("Markup", markup_example)
+
+ example_table = Table(
+ show_edge=False,
+ show_header=True,
+ expand=False,
+ row_styles=["none", "dim"],
+ box=box.SIMPLE,
+ )
+ example_table.add_column("[green]Date", style="green", no_wrap=True)
+ example_table.add_column("[blue]Title", style="blue")
+ example_table.add_column(
+ "[cyan]Production Budget",
+ style="cyan",
+ justify="right",
+ no_wrap=True,
+ )
+ example_table.add_column(
+ "[magenta]Box Office",
+ style="magenta",
+ justify="right",
+ no_wrap=True,
+ )
+ example_table.add_row(
+ "Dec 20, 2019",
+ "Star Wars: The Rise of Skywalker",
+ "$275,000,000",
+ "$375,126,118",
+ )
+ example_table.add_row(
+ "May 25, 2018",
+ "[b]Solo[/]: A Star Wars Story",
+ "$275,000,000",
+ "$393,151,347",
+ )
+ example_table.add_row(
+ "Dec 15, 2017",
+ "Star Wars Ep. VIII: The Last Jedi",
+ "$262,000,000",
+ "[bold]$1,332,539,889[/bold]",
+ )
+ example_table.add_row(
+ "May 19, 1999",
+ "Star Wars Ep. [b]I[/b]: [i]The phantom Menace",
+ "$115,000,000",
+ "$1,027,044,677",
+ )
+
+ table.add_row("Tables", example_table)
+
+ code = '''\
+def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ for value in iter_values:
+ yield False, previous_value
+ previous_value = value
+ yield True, previous_value'''
+
+ pretty_data = {
+ "foo": [
+ 3.1427,
+ (
+ "Paul Atriedies",
+ "Vladimir Harkonnen",
+ "Thufir Haway",
+ ),
+ ],
+ "atomic": (False, True, None),
+ }
+ table.add_row(
+ "Syntax\nhighlighting\n&\npretty\nprinting",
+ comparison(
+ Syntax(code, "python3", line_numbers=True, indent_guides=True),
+ Pretty(pretty_data, indent_guides=True),
+ ),
+ )
+
+ markdown_example = """\
+# Markdown
+
+Supports much of the *markdown*, __syntax__!
+
+- Headers
+- Basic formatting: **bold**, *italic*, `code`
+- Block quotes
+- Lists, and more...
+ """
+ table.add_row(
+ "Markdown", comparison("[cyan]" + markdown_example, Markdown(markdown_example))
+ )
+
+ table.add_row(
+ "+more!",
+ """Progress bars, columns, styled logging handler, tracebacks, etc...""",
+ )
+ return table
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ console = Console(
+ file=io.StringIO(),
+ force_terminal=True,
+ )
+ test_card = make_test_card()
+
+ # Print once to warm cache
+ console.print(test_card)
+ console.file = io.StringIO()
+
+ start = process_time()
+ console.print(test_card)
+ taken = round((process_time() - start) * 1000.0, 1)
+
+ text = console.file.getvalue()
+ # https://bugs.python.org/issue37871
+ for line in text.splitlines():
+ print(line)
+
+ print(f"rendered in {taken}ms")
+
+ from rich.panel import Panel
+
+ console = Console()
+
+ sponsor_message = Table.grid(padding=1)
+ sponsor_message.add_column(style="green", justify="right")
+ sponsor_message.add_column(no_wrap=True)
+ sponsor_message.add_row(
+ "Sponsor me",
+ "[u blue link=https://github.com/sponsors/willmcgugan]https://github.com/sponsors/willmcgugan",
+ )
+ sponsor_message.add_row(
+ "Buy me a :coffee:",
+ "[u blue link=https://ko-fi.com/willmcgugan]https://ko-fi.com/willmcgugan",
+ )
+ sponsor_message.add_row(
+ "Twitter",
+ "[u blue link=https://twitter.com/willmcgugan]https://twitter.com/willmcgugan",
+ )
+ sponsor_message.add_row(
+ "Blog", "[u blue link=https://www.willmcgugan.com]https://www.willmcgugan.com"
+ )
+
+ intro_message = Text.from_markup(
+ """\
+It takes a lot of time to develop Rich and to provide support.
+
+Consider supporting my work via Github Sponsors (ask your company / organization), or buy me a coffee to say thanks.
+
+- Will McGugan"""
+ )
+
+ message = Table.grid(padding=2)
+ message.add_column()
+ message.add_column(no_wrap=True)
+ message.add_row(intro_message, sponsor_message)
+
+ console.print(
+ Panel.fit(
+ message,
+ box=box.ROUNDED,
+ padding=(1, 2),
+ title="[b red]Thanks for trying out Rich!",
+ border_style="bright_blue",
+ ),
+ justify="center",
+ )
diff --git a/rich/_cell_widths.py b/rich/_cell_widths.py
new file mode 100644
index 0000000..36286df
--- /dev/null
+++ b/rich/_cell_widths.py
@@ -0,0 +1,451 @@
+# 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),
+ (2045, 2045, 0),
+ (2070, 2073, 0),
+ (2075, 2083, 0),
+ (2085, 2087, 0),
+ (2089, 2093, 0),
+ (2137, 2139, 0),
+ (2259, 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),
+ (2558, 2558, 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),
+ (2810, 2815, 0),
+ (2817, 2817, 0),
+ (2876, 2876, 0),
+ (2879, 2879, 0),
+ (2881, 2884, 0),
+ (2893, 2893, 0),
+ (2901, 2902, 0),
+ (2914, 2915, 0),
+ (2946, 2946, 0),
+ (3008, 3008, 0),
+ (3021, 3021, 0),
+ (3072, 3072, 0),
+ (3076, 3076, 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),
+ (3328, 3329, 0),
+ (3387, 3388, 0),
+ (3393, 3396, 0),
+ (3405, 3405, 0),
+ (3426, 3427, 0),
+ (3457, 3457, 0),
+ (3530, 3530, 0),
+ (3538, 3540, 0),
+ (3542, 3542, 0),
+ (3633, 3633, 0),
+ (3636, 3642, 0),
+ (3655, 3662, 0),
+ (3761, 3761, 0),
+ (3764, 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, 6848, 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, 7673, 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, 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),
+ (43052, 43052, 0),
+ (43204, 43205, 0),
+ (43232, 43249, 0),
+ (43263, 43263, 0),
+ (43302, 43309, 0),
+ (43335, 43345, 0),
+ (43360, 43388, 2),
+ (43392, 43394, 0),
+ (43443, 43443, 0),
+ (43446, 43449, 0),
+ (43452, 43453, 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),
+ (68900, 68903, 0),
+ (69291, 69292, 0),
+ (69446, 69456, 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),
+ (70089, 70092, 0),
+ (70095, 70095, 0),
+ (70191, 70193, 0),
+ (70196, 70196, 0),
+ (70198, 70199, 0),
+ (70206, 70206, 0),
+ (70367, 70367, 0),
+ (70371, 70378, 0),
+ (70400, 70401, 0),
+ (70459, 70460, 0),
+ (70464, 70464, 0),
+ (70502, 70508, 0),
+ (70512, 70516, 0),
+ (70712, 70719, 0),
+ (70722, 70724, 0),
+ (70726, 70726, 0),
+ (70750, 70750, 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),
+ (71727, 71735, 0),
+ (71737, 71738, 0),
+ (71995, 71996, 0),
+ (71998, 71998, 0),
+ (72003, 72003, 0),
+ (72148, 72151, 0),
+ (72154, 72155, 0),
+ (72160, 72160, 0),
+ (72193, 72202, 0),
+ (72243, 72248, 0),
+ (72251, 72254, 0),
+ (72263, 72263, 0),
+ (72273, 72278, 0),
+ (72281, 72283, 0),
+ (72330, 72342, 0),
+ (72344, 72345, 0),
+ (72752, 72758, 0),
+ (72760, 72765, 0),
+ (72767, 72767, 0),
+ (72850, 72871, 0),
+ (72874, 72880, 0),
+ (72882, 72883, 0),
+ (72885, 72886, 0),
+ (73009, 73014, 0),
+ (73018, 73018, 0),
+ (73020, 73021, 0),
+ (73023, 73029, 0),
+ (73031, 73031, 0),
+ (73104, 73105, 0),
+ (73109, 73109, 0),
+ (73111, 73111, 0),
+ (73459, 73460, 0),
+ (92912, 92916, 0),
+ (92976, 92982, 0),
+ (94031, 94031, 0),
+ (94095, 94098, 0),
+ (94176, 94179, 2),
+ (94180, 94180, 0),
+ (94192, 94193, 2),
+ (94208, 100343, 2),
+ (100352, 101589, 2),
+ (101632, 101640, 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),
+ (123184, 123190, 0),
+ (123628, 123631, 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, 128727, 2),
+ (128747, 128748, 2),
+ (128756, 128764, 2),
+ (128992, 129003, 2),
+ (129292, 129338, 2),
+ (129340, 129349, 2),
+ (129351, 129400, 2),
+ (129402, 129483, 2),
+ (129485, 129535, 2),
+ (129648, 129652, 2),
+ (129656, 129658, 2),
+ (129664, 129670, 2),
+ (129680, 129704, 2),
+ (129712, 129718, 2),
+ (129728, 129730, 2),
+ (129744, 129750, 2),
+ (131072, 196605, 2),
+ (196608, 262141, 2),
+ (917760, 917999, 0),
+]
diff --git a/rich/_emoji_codes.py b/rich/_emoji_codes.py
new file mode 100644
index 0000000..1f2877b
--- /dev/null
+++ b/rich/_emoji_codes.py
@@ -0,0 +1,3610 @@
+EMOJI = {
+ "1st_place_medal": "🥇",
+ "2nd_place_medal": "🥈",
+ "3rd_place_medal": "🥉",
+ "ab_button_(blood_type)": "🆎",
+ "atm_sign": "🏧",
+ "a_button_(blood_type)": "🅰",
+ "afghanistan": "🇦🇫",
+ "albania": "🇦🇱",
+ "algeria": "🇩🇿",
+ "american_samoa": "🇦🇸",
+ "andorra": "🇦🇩",
+ "angola": "🇦🇴",
+ "anguilla": "🇦🇮",
+ "antarctica": "🇦🇶",
+ "antigua_&_barbuda": "🇦🇬",
+ "aquarius": "♒",
+ "argentina": "🇦🇷",
+ "aries": "♈",
+ "armenia": "🇦🇲",
+ "aruba": "🇦🇼",
+ "ascension_island": "🇦🇨",
+ "australia": "🇦🇺",
+ "austria": "🇦🇹",
+ "azerbaijan": "🇦🇿",
+ "back_arrow": "🔙",
+ "b_button_(blood_type)": "🅱",
+ "bahamas": "🇧🇸",
+ "bahrain": "🇧🇭",
+ "bangladesh": "🇧🇩",
+ "barbados": "🇧🇧",
+ "belarus": "🇧🇾",
+ "belgium": "🇧🇪",
+ "belize": "🇧🇿",
+ "benin": "🇧🇯",
+ "bermuda": "🇧🇲",
+ "bhutan": "🇧🇹",
+ "bolivia": "🇧🇴",
+ "bosnia_&_herzegovina": "🇧🇦",
+ "botswana": "🇧🇼",
+ "bouvet_island": "🇧🇻",
+ "brazil": "🇧🇷",
+ "british_indian_ocean_territory": "🇮🇴",
+ "british_virgin_islands": "🇻🇬",
+ "brunei": "🇧🇳",
+ "bulgaria": "🇧🇬",
+ "burkina_faso": "🇧🇫",
+ "burundi": "🇧🇮",
+ "cl_button": "🆑",
+ "cool_button": "🆒",
+ "cambodia": "🇰🇭",
+ "cameroon": "🇨🇲",
+ "canada": "🇨🇦",
+ "canary_islands": "🇮🇨",
+ "cancer": "♋",
+ "cape_verde": "🇨🇻",
+ "capricorn": "♑",
+ "caribbean_netherlands": "🇧🇶",
+ "cayman_islands": "🇰🇾",
+ "central_african_republic": "🇨🇫",
+ "ceuta_&_melilla": "🇪🇦",
+ "chad": "🇹🇩",
+ "chile": "🇨🇱",
+ "china": "🇨🇳",
+ "christmas_island": "🇨🇽",
+ "christmas_tree": "🎄",
+ "clipperton_island": "🇨🇵",
+ "cocos_(keeling)_islands": "🇨🇨",
+ "colombia": "🇨🇴",
+ "comoros": "🇰🇲",
+ "congo_-_brazzaville": "🇨🇬",
+ "congo_-_kinshasa": "🇨🇩",
+ "cook_islands": "🇨🇰",
+ "costa_rica": "🇨🇷",
+ "croatia": "🇭🇷",
+ "cuba": "🇨🇺",
+ "curaçao": "🇨🇼",
+ "cyprus": "🇨🇾",
+ "czechia": "🇨🇿",
+ "côte_d’ivoire": "🇨🇮",
+ "denmark": "🇩🇰",
+ "diego_garcia": "🇩🇬",
+ "djibouti": "🇩🇯",
+ "dominica": "🇩🇲",
+ "dominican_republic": "🇩🇴",
+ "end_arrow": "🔚",
+ "ecuador": "🇪🇨",
+ "egypt": "🇪🇬",
+ "el_salvador": "🇸🇻",
+ "england": "🏴\U000e0067\U000e0062\U000e0065\U000e006e\U000e0067\U000e007f",
+ "equatorial_guinea": "🇬🇶",
+ "eritrea": "🇪🇷",
+ "estonia": "🇪🇪",
+ "ethiopia": "🇪🇹",
+ "european_union": "🇪🇺",
+ "free_button": "🆓",
+ "falkland_islands": "🇫🇰",
+ "faroe_islands": "🇫🇴",
+ "fiji": "🇫🇯",
+ "finland": "🇫🇮",
+ "france": "🇫🇷",
+ "french_guiana": "🇬🇫",
+ "french_polynesia": "🇵🇫",
+ "french_southern_territories": "🇹🇫",
+ "gabon": "🇬🇦",
+ "gambia": "🇬🇲",
+ "gemini": "♊",
+ "georgia": "🇬🇪",
+ "germany": "🇩🇪",
+ "ghana": "🇬🇭",
+ "gibraltar": "🇬🇮",
+ "greece": "🇬🇷",
+ "greenland": "🇬🇱",
+ "grenada": "🇬🇩",
+ "guadeloupe": "🇬🇵",
+ "guam": "🇬🇺",
+ "guatemala": "🇬🇹",
+ "guernsey": "🇬🇬",
+ "guinea": "🇬🇳",
+ "guinea-bissau": "🇬🇼",
+ "guyana": "🇬🇾",
+ "haiti": "🇭🇹",
+ "heard_&_mcdonald_islands": "🇭🇲",
+ "honduras": "🇭🇳",
+ "hong_kong_sar_china": "🇭🇰",
+ "hungary": "🇭🇺",
+ "id_button": "🆔",
+ "iceland": "🇮🇸",
+ "india": "🇮🇳",
+ "indonesia": "🇮🇩",
+ "iran": "🇮🇷",
+ "iraq": "🇮🇶",
+ "ireland": "🇮🇪",
+ "isle_of_man": "🇮🇲",
+ "israel": "🇮🇱",
+ "italy": "🇮🇹",
+ "jamaica": "🇯🇲",
+ "japan": "🗾",
+ "japanese_acceptable_button": "🉑",
+ "japanese_application_button": "🈸",
+ "japanese_bargain_button": "🉐",
+ "japanese_castle": "🏯",
+ "japanese_congratulations_button": "㊗",
+ "japanese_discount_button": "🈹",
+ "japanese_dolls": "🎎",
+ "japanese_free_of_charge_button": "🈚",
+ "japanese_here_button": "🈁",
+ "japanese_monthly_amount_button": "🈷",
+ "japanese_no_vacancy_button": "🈵",
+ "japanese_not_free_of_charge_button": "🈶",
+ "japanese_open_for_business_button": "🈺",
+ "japanese_passing_grade_button": "🈴",
+ "japanese_post_office": "🏣",
+ "japanese_prohibited_button": "🈲",
+ "japanese_reserved_button": "🈯",
+ "japanese_secret_button": "㊙",
+ "japanese_service_charge_button": "🈂",
+ "japanese_symbol_for_beginner": "🔰",
+ "japanese_vacancy_button": "🈳",
+ "jersey": "🇯🇪",
+ "jordan": "🇯🇴",
+ "kazakhstan": "🇰🇿",
+ "kenya": "🇰🇪",
+ "kiribati": "🇰🇮",
+ "kosovo": "🇽🇰",
+ "kuwait": "🇰🇼",
+ "kyrgyzstan": "🇰🇬",
+ "laos": "🇱🇦",
+ "latvia": "🇱🇻",
+ "lebanon": "🇱🇧",
+ "leo": "♌",
+ "lesotho": "🇱🇸",
+ "liberia": "🇱🇷",
+ "libra": "♎",
+ "libya": "🇱🇾",
+ "liechtenstein": "🇱🇮",
+ "lithuania": "🇱🇹",
+ "luxembourg": "🇱🇺",
+ "macau_sar_china": "🇲🇴",
+ "macedonia": "🇲🇰",
+ "madagascar": "🇲🇬",
+ "malawi": "🇲🇼",
+ "malaysia": "🇲🇾",
+ "maldives": "🇲🇻",
+ "mali": "🇲🇱",
+ "malta": "🇲🇹",
+ "marshall_islands": "🇲🇭",
+ "martinique": "🇲🇶",
+ "mauritania": "🇲🇷",
+ "mauritius": "🇲🇺",
+ "mayotte": "🇾🇹",
+ "mexico": "🇲🇽",
+ "micronesia": "🇫🇲",
+ "moldova": "🇲🇩",
+ "monaco": "🇲🇨",
+ "mongolia": "🇲🇳",
+ "montenegro": "🇲🇪",
+ "montserrat": "🇲🇸",
+ "morocco": "🇲🇦",
+ "mozambique": "🇲🇿",
+ "mrs._claus": "🤶",
+ "mrs._claus_dark_skin_tone": "🤶🏿",
+ "mrs._claus_light_skin_tone": "🤶🏻",
+ "mrs._claus_medium-dark_skin_tone": "🤶🏾",
+ "mrs._claus_medium-light_skin_tone": "🤶🏼",
+ "mrs._claus_medium_skin_tone": "🤶🏽",
+ "myanmar_(burma)": "🇲🇲",
+ "new_button": "🆕",
+ "ng_button": "🆖",
+ "namibia": "🇳🇦",
+ "nauru": "🇳🇷",
+ "nepal": "🇳🇵",
+ "netherlands": "🇳🇱",
+ "new_caledonia": "🇳🇨",
+ "new_zealand": "🇳🇿",
+ "nicaragua": "🇳🇮",
+ "niger": "🇳🇪",
+ "nigeria": "🇳🇬",
+ "niue": "🇳🇺",
+ "norfolk_island": "🇳🇫",
+ "north_korea": "🇰🇵",
+ "northern_mariana_islands": "🇲🇵",
+ "norway": "🇳🇴",
+ "ok_button": "🆗",
+ "ok_hand": "👌",
+ "ok_hand_dark_skin_tone": "👌🏿",
+ "ok_hand_light_skin_tone": "👌🏻",
+ "ok_hand_medium-dark_skin_tone": "👌🏾",
+ "ok_hand_medium-light_skin_tone": "👌🏼",
+ "ok_hand_medium_skin_tone": "👌🏽",
+ "on!_arrow": "🔛",
+ "o_button_(blood_type)": "🅾",
+ "oman": "🇴🇲",
+ "ophiuchus": "⛎",
+ "p_button": "🅿",
+ "pakistan": "🇵🇰",
+ "palau": "🇵🇼",
+ "palestinian_territories": "🇵🇸",
+ "panama": "🇵🇦",
+ "papua_new_guinea": "🇵🇬",
+ "paraguay": "🇵🇾",
+ "peru": "🇵🇪",
+ "philippines": "🇵🇭",
+ "pisces": "♓",
+ "pitcairn_islands": "🇵🇳",
+ "poland": "🇵🇱",
+ "portugal": "🇵🇹",
+ "puerto_rico": "🇵🇷",
+ "qatar": "🇶🇦",
+ "romania": "🇷🇴",
+ "russia": "🇷🇺",
+ "rwanda": "🇷🇼",
+ "réunion": "🇷🇪",
+ "soon_arrow": "🔜",
+ "sos_button": "🆘",
+ "sagittarius": "♐",
+ "samoa": "🇼🇸",
+ "san_marino": "🇸🇲",
+ "santa_claus": "🎅",
+ "santa_claus_dark_skin_tone": "🎅🏿",
+ "santa_claus_light_skin_tone": "🎅🏻",
+ "santa_claus_medium-dark_skin_tone": "🎅🏾",
+ "santa_claus_medium-light_skin_tone": "🎅🏼",
+ "santa_claus_medium_skin_tone": "🎅🏽",
+ "saudi_arabia": "🇸🇦",
+ "scorpio": "♏",
+ "scotland": "🏴\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f",
+ "senegal": "🇸🇳",
+ "serbia": "🇷🇸",
+ "seychelles": "🇸🇨",
+ "sierra_leone": "🇸🇱",
+ "singapore": "🇸🇬",
+ "sint_maarten": "🇸🇽",
+ "slovakia": "🇸🇰",
+ "slovenia": "🇸🇮",
+ "solomon_islands": "🇸🇧",
+ "somalia": "🇸🇴",
+ "south_africa": "🇿🇦",
+ "south_georgia_&_south_sandwich_islands": "🇬🇸",
+ "south_korea": "🇰🇷",
+ "south_sudan": "🇸🇸",
+ "spain": "🇪🇸",
+ "sri_lanka": "🇱🇰",
+ "st._barthélemy": "🇧🇱",
+ "st._helena": "🇸🇭",
+ "st._kitts_&_nevis": "🇰🇳",
+ "st._lucia": "🇱🇨",
+ "st._martin": "🇲🇫",
+ "st._pierre_&_miquelon": "🇵🇲",
+ "st._vincent_&_grenadines": "🇻🇨",
+ "statue_of_liberty": "🗽",
+ "sudan": "🇸🇩",
+ "suriname": "🇸🇷",
+ "svalbard_&_jan_mayen": "🇸🇯",
+ "swaziland": "🇸🇿",
+ "sweden": "🇸🇪",
+ "switzerland": "🇨🇭",
+ "syria": "🇸🇾",
+ "são_tomé_&_príncipe": "🇸🇹",
+ "t-rex": "🦖",
+ "top_arrow": "🔝",
+ "taiwan": "🇹🇼",
+ "tajikistan": "🇹🇯",
+ "tanzania": "🇹🇿",
+ "taurus": "♉",
+ "thailand": "🇹🇭",
+ "timor-leste": "🇹🇱",
+ "togo": "🇹🇬",
+ "tokelau": "🇹🇰",
+ "tokyo_tower": "🗼",
+ "tonga": "🇹🇴",
+ "trinidad_&_tobago": "🇹🇹",
+ "tristan_da_cunha": "🇹🇦",
+ "tunisia": "🇹🇳",
+ "turkey": "🦃",
+ "turkmenistan": "🇹🇲",
+ "turks_&_caicos_islands": "🇹🇨",
+ "tuvalu": "🇹🇻",
+ "u.s._outlying_islands": "🇺🇲",
+ "u.s._virgin_islands": "🇻🇮",
+ "up!_button": "🆙",
+ "uganda": "🇺🇬",
+ "ukraine": "🇺🇦",
+ "united_arab_emirates": "🇦🇪",
+ "united_kingdom": "🇬🇧",
+ "united_nations": "🇺🇳",
+ "united_states": "🇺🇸",
+ "uruguay": "🇺🇾",
+ "uzbekistan": "🇺🇿",
+ "vs_button": "🆚",
+ "vanuatu": "🇻🇺",
+ "vatican_city": "🇻🇦",
+ "venezuela": "🇻🇪",
+ "vietnam": "🇻🇳",
+ "virgo": "♍",
+ "wales": "🏴\U000e0067\U000e0062\U000e0077\U000e006c\U000e0073\U000e007f",
+ "wallis_&_futuna": "🇼🇫",
+ "western_sahara": "🇪🇭",
+ "yemen": "🇾🇪",
+ "zambia": "🇿🇲",
+ "zimbabwe": "🇿🇼",
+ "abacus": "🧮",
+ "adhesive_bandage": "🩹",
+ "admission_tickets": "🎟",
+ "adult": "🧑",
+ "adult_dark_skin_tone": "🧑🏿",
+ "adult_light_skin_tone": "🧑🏻",
+ "adult_medium-dark_skin_tone": "🧑🏾",
+ "adult_medium-light_skin_tone": "🧑🏼",
+ "adult_medium_skin_tone": "🧑🏽",
+ "aerial_tramway": "🚡",
+ "airplane": "✈",
+ "airplane_arrival": "🛬",
+ "airplane_departure": "🛫",
+ "alarm_clock": "⏰",
+ "alembic": "⚗",
+ "alien": "👽",
+ "alien_monster": "👾",
+ "ambulance": "🚑",
+ "american_football": "🏈",
+ "amphora": "🏺",
+ "anchor": "⚓",
+ "anger_symbol": "💢",
+ "angry_face": "😠",
+ "angry_face_with_horns": "👿",
+ "anguished_face": "😧",
+ "ant": "🐜",
+ "antenna_bars": "📶",
+ "anxious_face_with_sweat": "😰",
+ "articulated_lorry": "🚛",
+ "artist_palette": "🎨",
+ "astonished_face": "😲",
+ "atom_symbol": "⚛",
+ "auto_rickshaw": "🛺",
+ "automobile": "🚗",
+ "avocado": "🥑",
+ "axe": "🪓",
+ "baby": "👶",
+ "baby_angel": "👼",
+ "baby_angel_dark_skin_tone": "👼🏿",
+ "baby_angel_light_skin_tone": "👼🏻",
+ "baby_angel_medium-dark_skin_tone": "👼🏾",
+ "baby_angel_medium-light_skin_tone": "👼🏼",
+ "baby_angel_medium_skin_tone": "👼🏽",
+ "baby_bottle": "🍼",
+ "baby_chick": "🐤",
+ "baby_dark_skin_tone": "👶🏿",
+ "baby_light_skin_tone": "👶🏻",
+ "baby_medium-dark_skin_tone": "👶🏾",
+ "baby_medium-light_skin_tone": "👶🏼",
+ "baby_medium_skin_tone": "👶🏽",
+ "baby_symbol": "🚼",
+ "backhand_index_pointing_down": "👇",
+ "backhand_index_pointing_down_dark_skin_tone": "👇🏿",
+ "backhand_index_pointing_down_light_skin_tone": "👇🏻",
+ "backhand_index_pointing_down_medium-dark_skin_tone": "👇🏾",
+ "backhand_index_pointing_down_medium-light_skin_tone": "👇🏼",
+ "backhand_index_pointing_down_medium_skin_tone": "👇🏽",
+ "backhand_index_pointing_left": "👈",
+ "backhand_index_pointing_left_dark_skin_tone": "👈🏿",
+ "backhand_index_pointing_left_light_skin_tone": "👈🏻",
+ "backhand_index_pointing_left_medium-dark_skin_tone": "👈🏾",
+ "backhand_index_pointing_left_medium-light_skin_tone": "👈🏼",
+ "backhand_index_pointing_left_medium_skin_tone": "👈🏽",
+ "backhand_index_pointing_right": "👉",
+ "backhand_index_pointing_right_dark_skin_tone": "👉🏿",
+ "backhand_index_pointing_right_light_skin_tone": "👉🏻",
+ "backhand_index_pointing_right_medium-dark_skin_tone": "👉🏾",
+ "backhand_index_pointing_right_medium-light_skin_tone": "👉🏼",
+ "backhand_index_pointing_right_medium_skin_tone": "👉🏽",
+ "backhand_index_pointing_up": "👆",
+ "backhand_index_pointing_up_dark_skin_tone": "👆🏿",
+ "backhand_index_pointing_up_light_skin_tone": "👆🏻",
+ "backhand_index_pointing_up_medium-dark_skin_tone": "👆🏾",
+ "backhand_index_pointing_up_medium-light_skin_tone": "👆🏼",
+ "backhand_index_pointing_up_medium_skin_tone": "👆🏽",
+ "bacon": "🥓",
+ "badger": "🦡",
+ "badminton": "🏸",
+ "bagel": "🥯",
+ "baggage_claim": "🛄",
+ "baguette_bread": "🥖",
+ "balance_scale": "⚖",
+ "bald": "🦲",
+ "bald_man": "👨\u200d🦲",
+ "bald_woman": "👩\u200d🦲",
+ "ballet_shoes": "🩰",
+ "balloon": "🎈",
+ "ballot_box_with_ballot": "🗳",
+ "ballot_box_with_check": "☑",
+ "banana": "🍌",
+ "banjo": "🪕",
+ "bank": "🏦",
+ "bar_chart": "📊",
+ "barber_pole": "💈",
+ "baseball": "⚾",
+ "basket": "🧺",
+ "basketball": "🏀",
+ "bat": "🦇",
+ "bathtub": "🛁",
+ "battery": "🔋",
+ "beach_with_umbrella": "🏖",
+ "beaming_face_with_smiling_eyes": "😁",
+ "bear_face": "🐻",
+ "bearded_person": "🧔",
+ "bearded_person_dark_skin_tone": "🧔🏿",
+ "bearded_person_light_skin_tone": "🧔🏻",
+ "bearded_person_medium-dark_skin_tone": "🧔🏾",
+ "bearded_person_medium-light_skin_tone": "🧔🏼",
+ "bearded_person_medium_skin_tone": "🧔🏽",
+ "beating_heart": "💓",
+ "bed": "🛏",
+ "beer_mug": "🍺",
+ "bell": "🔔",
+ "bell_with_slash": "🔕",
+ "bellhop_bell": "🛎",
+ "bento_box": "🍱",
+ "beverage_box": "🧃",
+ "bicycle": "🚲",
+ "bikini": "👙",
+ "billed_cap": "🧢",
+ "biohazard": "☣",
+ "bird": "🐦",
+ "birthday_cake": "🎂",
+ "black_circle": "⚫",
+ "black_flag": "🏴",
+ "black_heart": "🖤",
+ "black_large_square": "⬛",
+ "black_medium-small_square": "◾",
+ "black_medium_square": "◼",
+ "black_nib": "✒",
+ "black_small_square": "▪",
+ "black_square_button": "🔲",
+ "blond-haired_man": "👱\u200d♂️",
+ "blond-haired_man_dark_skin_tone": "👱🏿\u200d♂️",
+ "blond-haired_man_light_skin_tone": "👱🏻\u200d♂️",
+ "blond-haired_man_medium-dark_skin_tone": "👱🏾\u200d♂️",
+ "blond-haired_man_medium-light_skin_tone": "👱🏼\u200d♂️",
+ "blond-haired_man_medium_skin_tone": "👱🏽\u200d♂️",
+ "blond-haired_person": "👱",
+ "blond-haired_person_dark_skin_tone": "👱🏿",
+ "blond-haired_person_light_skin_tone": "👱🏻",
+ "blond-haired_person_medium-dark_skin_tone": "👱🏾",
+ "blond-haired_person_medium-light_skin_tone": "👱🏼",
+ "blond-haired_person_medium_skin_tone": "👱🏽",
+ "blond-haired_woman": "👱\u200d♀️",
+ "blond-haired_woman_dark_skin_tone": "👱🏿\u200d♀️",
+ "blond-haired_woman_light_skin_tone": "👱🏻\u200d♀️",
+ "blond-haired_woman_medium-dark_skin_tone": "👱🏾\u200d♀️",
+ "blond-haired_woman_medium-light_skin_tone": "👱🏼\u200d♀️",
+ "blond-haired_woman_medium_skin_tone": "👱🏽\u200d♀️",
+ "blossom": "🌼",
+ "blowfish": "🐡",
+ "blue_book": "📘",
+ "blue_circle": "🔵",
+ "blue_heart": "💙",
+ "blue_square": "🟦",
+ "boar": "🐗",
+ "bomb": "💣",
+ "bone": "🦴",
+ "bookmark": "🔖",
+ "bookmark_tabs": "📑",
+ "books": "📚",
+ "bottle_with_popping_cork": "🍾",
+ "bouquet": "💐",
+ "bow_and_arrow": "🏹",
+ "bowl_with_spoon": "🥣",
+ "bowling": "🎳",
+ "boxing_glove": "🥊",
+ "boy": "👦",
+ "boy_dark_skin_tone": "👦🏿",
+ "boy_light_skin_tone": "👦🏻",
+ "boy_medium-dark_skin_tone": "👦🏾",
+ "boy_medium-light_skin_tone": "👦🏼",
+ "boy_medium_skin_tone": "👦🏽",
+ "brain": "🧠",
+ "bread": "🍞",
+ "breast-feeding": "🤱",
+ "breast-feeding_dark_skin_tone": "🤱🏿",
+ "breast-feeding_light_skin_tone": "🤱🏻",
+ "breast-feeding_medium-dark_skin_tone": "🤱🏾",
+ "breast-feeding_medium-light_skin_tone": "🤱🏼",
+ "breast-feeding_medium_skin_tone": "🤱🏽",
+ "brick": "🧱",
+ "bride_with_veil": "👰",
+ "bride_with_veil_dark_skin_tone": "👰🏿",
+ "bride_with_veil_light_skin_tone": "👰🏻",
+ "bride_with_veil_medium-dark_skin_tone": "👰🏾",
+ "bride_with_veil_medium-light_skin_tone": "👰🏼",
+ "bride_with_veil_medium_skin_tone": "👰🏽",
+ "bridge_at_night": "🌉",
+ "briefcase": "💼",
+ "briefs": "🩲",
+ "bright_button": "🔆",
+ "broccoli": "🥦",
+ "broken_heart": "💔",
+ "broom": "🧹",
+ "brown_circle": "🟤",
+ "brown_heart": "🤎",
+ "brown_square": "🟫",
+ "bug": "🐛",
+ "building_construction": "🏗",
+ "bullet_train": "🚅",
+ "burrito": "🌯",
+ "bus": "🚌",
+ "bus_stop": "🚏",
+ "bust_in_silhouette": "👤",
+ "busts_in_silhouette": "👥",
+ "butter": "🧈",
+ "butterfly": "🦋",
+ "cactus": "🌵",
+ "calendar": "📆",
+ "call_me_hand": "🤙",
+ "call_me_hand_dark_skin_tone": "🤙🏿",
+ "call_me_hand_light_skin_tone": "🤙🏻",
+ "call_me_hand_medium-dark_skin_tone": "🤙🏾",
+ "call_me_hand_medium-light_skin_tone": "🤙🏼",
+ "call_me_hand_medium_skin_tone": "🤙🏽",
+ "camel": "🐫",
+ "camera": "📷",
+ "camera_with_flash": "📸",
+ "camping": "🏕",
+ "candle": "🕯",
+ "candy": "🍬",
+ "canned_food": "🥫",
+ "canoe": "🛶",
+ "card_file_box": "🗃",
+ "card_index": "📇",
+ "card_index_dividers": "🗂",
+ "carousel_horse": "🎠",
+ "carp_streamer": "🎏",
+ "carrot": "🥕",
+ "castle": "🏰",
+ "cat": "🐱",
+ "cat_face": "🐱",
+ "cat_face_with_tears_of_joy": "😹",
+ "cat_face_with_wry_smile": "😼",
+ "chains": "⛓",
+ "chair": "🪑",
+ "chart_decreasing": "📉",
+ "chart_increasing": "📈",
+ "chart_increasing_with_yen": "💹",
+ "cheese_wedge": "🧀",
+ "chequered_flag": "🏁",
+ "cherries": "🍒",
+ "cherry_blossom": "🌸",
+ "chess_pawn": "♟",
+ "chestnut": "🌰",
+ "chicken": "🐔",
+ "child": "🧒",
+ "child_dark_skin_tone": "🧒🏿",
+ "child_light_skin_tone": "🧒🏻",
+ "child_medium-dark_skin_tone": "🧒🏾",
+ "child_medium-light_skin_tone": "🧒🏼",
+ "child_medium_skin_tone": "🧒🏽",
+ "children_crossing": "🚸",
+ "chipmunk": "🐿",
+ "chocolate_bar": "🍫",
+ "chopsticks": "🥢",
+ "church": "⛪",
+ "cigarette": "🚬",
+ "cinema": "🎦",
+ "circled_m": "Ⓜ",
+ "circus_tent": "🎪",
+ "cityscape": "🏙",
+ "cityscape_at_dusk": "🌆",
+ "clamp": "🗜",
+ "clapper_board": "🎬",
+ "clapping_hands": "👏",
+ "clapping_hands_dark_skin_tone": "👏🏿",
+ "clapping_hands_light_skin_tone": "👏🏻",
+ "clapping_hands_medium-dark_skin_tone": "👏🏾",
+ "clapping_hands_medium-light_skin_tone": "👏🏼",
+ "clapping_hands_medium_skin_tone": "👏🏽",
+ "classical_building": "🏛",
+ "clinking_beer_mugs": "🍻",
+ "clinking_glasses": "🥂",
+ "clipboard": "📋",
+ "clockwise_vertical_arrows": "🔃",
+ "closed_book": "📕",
+ "closed_mailbox_with_lowered_flag": "📪",
+ "closed_mailbox_with_raised_flag": "📫",
+ "closed_umbrella": "🌂",
+ "cloud": "☁",
+ "cloud_with_lightning": "🌩",
+ "cloud_with_lightning_and_rain": "⛈",
+ "cloud_with_rain": "🌧",
+ "cloud_with_snow": "🌨",
+ "clown_face": "🤡",
+ "club_suit": "♣",
+ "clutch_bag": "👝",
+ "coat": "🧥",
+ "cocktail_glass": "🍸",
+ "coconut": "🥥",
+ "coffin": "⚰",
+ "cold_face": "🥶",
+ "collision": "💥",
+ "comet": "☄",
+ "compass": "🧭",
+ "computer_disk": "💽",
+ "computer_mouse": "🖱",
+ "confetti_ball": "🎊",
+ "confounded_face": "😖",
+ "confused_face": "😕",
+ "construction": "🚧",
+ "construction_worker": "👷",
+ "construction_worker_dark_skin_tone": "👷🏿",
+ "construction_worker_light_skin_tone": "👷🏻",
+ "construction_worker_medium-dark_skin_tone": "👷🏾",
+ "construction_worker_medium-light_skin_tone": "👷🏼",
+ "construction_worker_medium_skin_tone": "👷🏽",
+ "control_knobs": "🎛",
+ "convenience_store": "🏪",
+ "cooked_rice": "🍚",
+ "cookie": "🍪",
+ "cooking": "🍳",
+ "copyright": "©",
+ "couch_and_lamp": "🛋",
+ "counterclockwise_arrows_button": "🔄",
+ "couple_with_heart": "💑",
+ "couple_with_heart_man_man": "👨\u200d❤️\u200d👨",
+ "couple_with_heart_woman_man": "👩\u200d❤️\u200d👨",
+ "couple_with_heart_woman_woman": "👩\u200d❤️\u200d👩",
+ "cow": "🐮",
+ "cow_face": "🐮",
+ "cowboy_hat_face": "🤠",
+ "crab": "🦀",
+ "crayon": "🖍",
+ "credit_card": "💳",
+ "crescent_moon": "🌙",
+ "cricket": "🦗",
+ "cricket_game": "🏏",
+ "crocodile": "🐊",
+ "croissant": "🥐",
+ "cross_mark": "❌",
+ "cross_mark_button": "❎",
+ "crossed_fingers": "🤞",
+ "crossed_fingers_dark_skin_tone": "🤞🏿",
+ "crossed_fingers_light_skin_tone": "🤞🏻",
+ "crossed_fingers_medium-dark_skin_tone": "🤞🏾",
+ "crossed_fingers_medium-light_skin_tone": "🤞🏼",
+ "crossed_fingers_medium_skin_tone": "🤞🏽",
+ "crossed_flags": "🎌",
+ "crossed_swords": "⚔",
+ "crown": "👑",
+ "crying_cat_face": "😿",
+ "crying_face": "😢",
+ "crystal_ball": "🔮",
+ "cucumber": "🥒",
+ "cupcake": "🧁",
+ "cup_with_straw": "🥤",
+ "curling_stone": "🥌",
+ "curly_hair": "🦱",
+ "curly-haired_man": "👨\u200d🦱",
+ "curly-haired_woman": "👩\u200d🦱",
+ "curly_loop": "➰",
+ "currency_exchange": "💱",
+ "curry_rice": "🍛",
+ "custard": "🍮",
+ "customs": "🛃",
+ "cut_of_meat": "🥩",
+ "cyclone": "🌀",
+ "dagger": "🗡",
+ "dango": "🍡",
+ "dashing_away": "💨",
+ "deaf_person": "🧏",
+ "deciduous_tree": "🌳",
+ "deer": "🦌",
+ "delivery_truck": "🚚",
+ "department_store": "🏬",
+ "derelict_house": "🏚",
+ "desert": "🏜",
+ "desert_island": "🏝",
+ "desktop_computer": "🖥",
+ "detective": "🕵",
+ "detective_dark_skin_tone": "🕵🏿",
+ "detective_light_skin_tone": "🕵🏻",
+ "detective_medium-dark_skin_tone": "🕵🏾",
+ "detective_medium-light_skin_tone": "🕵🏼",
+ "detective_medium_skin_tone": "🕵🏽",
+ "diamond_suit": "♦",
+ "diamond_with_a_dot": "💠",
+ "dim_button": "🔅",
+ "direct_hit": "🎯",
+ "disappointed_face": "😞",
+ "diving_mask": "🤿",
+ "diya_lamp": "🪔",
+ "dizzy": "💫",
+ "dizzy_face": "😵",
+ "dna": "🧬",
+ "dog": "🐶",
+ "dog_face": "🐶",
+ "dollar_banknote": "💵",
+ "dolphin": "🐬",
+ "door": "🚪",
+ "dotted_six-pointed_star": "🔯",
+ "double_curly_loop": "➿",
+ "double_exclamation_mark": "‼",
+ "doughnut": "🍩",
+ "dove": "🕊",
+ "down-left_arrow": "↙",
+ "down-right_arrow": "↘",
+ "down_arrow": "⬇",
+ "downcast_face_with_sweat": "😓",
+ "downwards_button": "🔽",
+ "dragon": "🐉",
+ "dragon_face": "🐲",
+ "dress": "👗",
+ "drooling_face": "🤤",
+ "drop_of_blood": "🩸",
+ "droplet": "💧",
+ "drum": "🥁",
+ "duck": "🦆",
+ "dumpling": "🥟",
+ "dvd": "📀",
+ "e-mail": "📧",
+ "eagle": "🦅",
+ "ear": "👂",
+ "ear_dark_skin_tone": "👂🏿",
+ "ear_light_skin_tone": "👂🏻",
+ "ear_medium-dark_skin_tone": "👂🏾",
+ "ear_medium-light_skin_tone": "👂🏼",
+ "ear_medium_skin_tone": "👂🏽",
+ "ear_of_corn": "🌽",
+ "ear_with_hearing_aid": "🦻",
+ "egg": "🍳",
+ "eggplant": "🍆",
+ "eight-pointed_star": "✴",
+ "eight-spoked_asterisk": "✳",
+ "eight-thirty": "🕣",
+ "eight_o’clock": "🕗",
+ "eject_button": "⏏",
+ "electric_plug": "🔌",
+ "elephant": "🐘",
+ "eleven-thirty": "🕦",
+ "eleven_o’clock": "🕚",
+ "elf": "🧝",
+ "elf_dark_skin_tone": "🧝🏿",
+ "elf_light_skin_tone": "🧝🏻",
+ "elf_medium-dark_skin_tone": "🧝🏾",
+ "elf_medium-light_skin_tone": "🧝🏼",
+ "elf_medium_skin_tone": "🧝🏽",
+ "envelope": "✉",
+ "envelope_with_arrow": "📩",
+ "euro_banknote": "💶",
+ "evergreen_tree": "🌲",
+ "ewe": "🐑",
+ "exclamation_mark": "❗",
+ "exclamation_question_mark": "⁉",
+ "exploding_head": "🤯",
+ "expressionless_face": "😑",
+ "eye": "👁",
+ "eye_in_speech_bubble": "👁️\u200d🗨️",
+ "eyes": "👀",
+ "face_blowing_a_kiss": "😘",
+ "face_savoring_food": "😋",
+ "face_screaming_in_fear": "😱",
+ "face_vomiting": "🤮",
+ "face_with_hand_over_mouth": "🤭",
+ "face_with_head-bandage": "🤕",
+ "face_with_medical_mask": "😷",
+ "face_with_monocle": "🧐",
+ "face_with_open_mouth": "😮",
+ "face_with_raised_eyebrow": "🤨",
+ "face_with_rolling_eyes": "🙄",
+ "face_with_steam_from_nose": "😤",
+ "face_with_symbols_on_mouth": "🤬",
+ "face_with_tears_of_joy": "😂",
+ "face_with_thermometer": "🤒",
+ "face_with_tongue": "😛",
+ "face_without_mouth": "😶",
+ "factory": "🏭",
+ "fairy": "🧚",
+ "fairy_dark_skin_tone": "🧚🏿",
+ "fairy_light_skin_tone": "🧚🏻",
+ "fairy_medium-dark_skin_tone": "🧚🏾",
+ "fairy_medium-light_skin_tone": "🧚🏼",
+ "fairy_medium_skin_tone": "🧚🏽",
+ "falafel": "🧆",
+ "fallen_leaf": "🍂",
+ "family": "👪",
+ "family_man_boy": "👨\u200d👦",
+ "family_man_boy_boy": "👨\u200d👦\u200d👦",
+ "family_man_girl": "👨\u200d👧",
+ "family_man_girl_boy": "👨\u200d👧\u200d👦",
+ "family_man_girl_girl": "👨\u200d👧\u200d👧",
+ "family_man_man_boy": "👨\u200d👨\u200d👦",
+ "family_man_man_boy_boy": "👨\u200d👨\u200d👦\u200d👦",
+ "family_man_man_girl": "👨\u200d👨\u200d👧",
+ "family_man_man_girl_boy": "👨\u200d👨\u200d👧\u200d👦",
+ "family_man_man_girl_girl": "👨\u200d👨\u200d👧\u200d👧",
+ "family_man_woman_boy": "👨\u200d👩\u200d👦",
+ "family_man_woman_boy_boy": "👨\u200d👩\u200d👦\u200d👦",
+ "family_man_woman_girl": "👨\u200d👩\u200d👧",
+ "family_man_woman_girl_boy": "👨\u200d👩\u200d👧\u200d👦",
+ "family_man_woman_girl_girl": "👨\u200d👩\u200d👧\u200d👧",
+ "family_woman_boy": "👩\u200d👦",
+ "family_woman_boy_boy": "👩\u200d👦\u200d👦",
+ "family_woman_girl": "👩\u200d👧",
+ "family_woman_girl_boy": "👩\u200d👧\u200d👦",
+ "family_woman_girl_girl": "👩\u200d👧\u200d👧",
+ "family_woman_woman_boy": "👩\u200d👩\u200d👦",
+ "family_woman_woman_boy_boy": "👩\u200d👩\u200d👦\u200d👦",
+ "family_woman_woman_girl": "👩\u200d👩\u200d👧",
+ "family_woman_woman_girl_boy": "👩\u200d👩\u200d👧\u200d👦",
+ "family_woman_woman_girl_girl": "👩\u200d👩\u200d👧\u200d👧",
+ "fast-forward_button": "⏩",
+ "fast_down_button": "⏬",
+ "fast_reverse_button": "⏪",
+ "fast_up_button": "⏫",
+ "fax_machine": "📠",
+ "fearful_face": "😨",
+ "female_sign": "♀",
+ "ferris_wheel": "🎡",
+ "ferry": "⛴",
+ "field_hockey": "🏑",
+ "file_cabinet": "🗄",
+ "file_folder": "📁",
+ "film_frames": "🎞",
+ "film_projector": "📽",
+ "fire": "🔥",
+ "fire_extinguisher": "🧯",
+ "firecracker": "🧨",
+ "fire_engine": "🚒",
+ "fireworks": "🎆",
+ "first_quarter_moon": "🌓",
+ "first_quarter_moon_face": "🌛",
+ "fish": "🐟",
+ "fish_cake_with_swirl": "🍥",
+ "fishing_pole": "🎣",
+ "five-thirty": "🕠",
+ "five_o’clock": "🕔",
+ "flag_in_hole": "⛳",
+ "flamingo": "🦩",
+ "flashlight": "🔦",
+ "flat_shoe": "🥿",
+ "fleur-de-lis": "⚜",
+ "flexed_biceps": "💪",
+ "flexed_biceps_dark_skin_tone": "💪🏿",
+ "flexed_biceps_light_skin_tone": "💪🏻",
+ "flexed_biceps_medium-dark_skin_tone": "💪🏾",
+ "flexed_biceps_medium-light_skin_tone": "💪🏼",
+ "flexed_biceps_medium_skin_tone": "💪🏽",
+ "floppy_disk": "💾",
+ "flower_playing_cards": "🎴",
+ "flushed_face": "😳",
+ "flying_disc": "🥏",
+ "flying_saucer": "🛸",
+ "fog": "🌫",
+ "foggy": "🌁",
+ "folded_hands": "🙏",
+ "folded_hands_dark_skin_tone": "🙏🏿",
+ "folded_hands_light_skin_tone": "🙏🏻",
+ "folded_hands_medium-dark_skin_tone": "🙏🏾",
+ "folded_hands_medium-light_skin_tone": "🙏🏼",
+ "folded_hands_medium_skin_tone": "🙏🏽",
+ "foot": "🦶",
+ "footprints": "👣",
+ "fork_and_knife": "🍴",
+ "fork_and_knife_with_plate": "🍽",
+ "fortune_cookie": "🥠",
+ "fountain": "⛲",
+ "fountain_pen": "🖋",
+ "four-thirty": "🕟",
+ "four_leaf_clover": "🍀",
+ "four_o’clock": "🕓",
+ "fox_face": "🦊",
+ "framed_picture": "🖼",
+ "french_fries": "🍟",
+ "fried_shrimp": "🍤",
+ "frog_face": "🐸",
+ "front-facing_baby_chick": "🐥",
+ "frowning_face": "☹",
+ "frowning_face_with_open_mouth": "😦",
+ "fuel_pump": "⛽",
+ "full_moon": "🌕",
+ "full_moon_face": "🌝",
+ "funeral_urn": "⚱",
+ "game_die": "🎲",
+ "garlic": "🧄",
+ "gear": "⚙",
+ "gem_stone": "💎",
+ "genie": "🧞",
+ "ghost": "👻",
+ "giraffe": "🦒",
+ "girl": "👧",
+ "girl_dark_skin_tone": "👧🏿",
+ "girl_light_skin_tone": "👧🏻",
+ "girl_medium-dark_skin_tone": "👧🏾",
+ "girl_medium-light_skin_tone": "👧🏼",
+ "girl_medium_skin_tone": "👧🏽",
+ "glass_of_milk": "🥛",
+ "glasses": "👓",
+ "globe_showing_americas": "🌎",
+ "globe_showing_asia-australia": "🌏",
+ "globe_showing_europe-africa": "🌍",
+ "globe_with_meridians": "🌐",
+ "gloves": "🧤",
+ "glowing_star": "🌟",
+ "goal_net": "🥅",
+ "goat": "🐐",
+ "goblin": "👺",
+ "goggles": "🥽",
+ "gorilla": "🦍",
+ "graduation_cap": "🎓",
+ "grapes": "🍇",
+ "green_apple": "🍏",
+ "green_book": "📗",
+ "green_circle": "🟢",
+ "green_heart": "💚",
+ "green_salad": "🥗",
+ "green_square": "🟩",
+ "grimacing_face": "😬",
+ "grinning_cat_face": "😺",
+ "grinning_cat_face_with_smiling_eyes": "😸",
+ "grinning_face": "😀",
+ "grinning_face_with_big_eyes": "😃",
+ "grinning_face_with_smiling_eyes": "😄",
+ "grinning_face_with_sweat": "😅",
+ "grinning_squinting_face": "😆",
+ "growing_heart": "💗",
+ "guard": "💂",
+ "guard_dark_skin_tone": "💂🏿",
+ "guard_light_skin_tone": "💂🏻",
+ "guard_medium-dark_skin_tone": "💂🏾",
+ "guard_medium-light_skin_tone": "💂🏼",
+ "guard_medium_skin_tone": "💂🏽",
+ "guide_dog": "🦮",
+ "guitar": "🎸",
+ "hamburger": "🍔",
+ "hammer": "🔨",
+ "hammer_and_pick": "⚒",
+ "hammer_and_wrench": "🛠",
+ "hamster_face": "🐹",
+ "hand_with_fingers_splayed": "🖐",
+ "hand_with_fingers_splayed_dark_skin_tone": "🖐🏿",
+ "hand_with_fingers_splayed_light_skin_tone": "🖐🏻",
+ "hand_with_fingers_splayed_medium-dark_skin_tone": "🖐🏾",
+ "hand_with_fingers_splayed_medium-light_skin_tone": "🖐🏼",
+ "hand_with_fingers_splayed_medium_skin_tone": "🖐🏽",
+ "handbag": "👜",
+ "handshake": "🤝",
+ "hatching_chick": "🐣",
+ "headphone": "🎧",
+ "hear-no-evil_monkey": "🙉",
+ "heart_decoration": "💟",
+ "heart_suit": "♥",
+ "heart_with_arrow": "💘",
+ "heart_with_ribbon": "💝",
+ "heavy_check_mark": "✔",
+ "heavy_division_sign": "➗",
+ "heavy_dollar_sign": "💲",
+ "heavy_heart_exclamation": "❣",
+ "heavy_large_circle": "⭕",
+ "heavy_minus_sign": "➖",
+ "heavy_multiplication_x": "✖",
+ "heavy_plus_sign": "➕",
+ "hedgehog": "🦔",
+ "helicopter": "🚁",
+ "herb": "🌿",
+ "hibiscus": "🌺",
+ "high-heeled_shoe": "👠",
+ "high-speed_train": "🚄",
+ "high_voltage": "⚡",
+ "hiking_boot": "🥾",
+ "hindu_temple": "🛕",
+ "hippopotamus": "🦛",
+ "hole": "🕳",
+ "honey_pot": "🍯",
+ "honeybee": "🐝",
+ "horizontal_traffic_light": "🚥",
+ "horse": "🐴",
+ "horse_face": "🐴",
+ "horse_racing": "🏇",
+ "horse_racing_dark_skin_tone": "🏇🏿",
+ "horse_racing_light_skin_tone": "🏇🏻",
+ "horse_racing_medium-dark_skin_tone": "🏇🏾",
+ "horse_racing_medium-light_skin_tone": "🏇🏼",
+ "horse_racing_medium_skin_tone": "🏇🏽",
+ "hospital": "🏥",
+ "hot_beverage": "☕",
+ "hot_dog": "🌭",
+ "hot_face": "🥵",
+ "hot_pepper": "🌶",
+ "hot_springs": "♨",
+ "hotel": "🏨",
+ "hourglass_done": "⌛",
+ "hourglass_not_done": "⏳",
+ "house": "🏠",
+ "house_with_garden": "🏡",
+ "houses": "🏘",
+ "hugging_face": "🤗",
+ "hundred_points": "💯",
+ "hushed_face": "😯",
+ "ice": "🧊",
+ "ice_cream": "🍨",
+ "ice_hockey": "🏒",
+ "ice_skate": "⛸",
+ "inbox_tray": "📥",
+ "incoming_envelope": "📨",
+ "index_pointing_up": "☝",
+ "index_pointing_up_dark_skin_tone": "☝🏿",
+ "index_pointing_up_light_skin_tone": "☝🏻",
+ "index_pointing_up_medium-dark_skin_tone": "☝🏾",
+ "index_pointing_up_medium-light_skin_tone": "☝🏼",
+ "index_pointing_up_medium_skin_tone": "☝🏽",
+ "infinity": "♾",
+ "information": "ℹ",
+ "input_latin_letters": "🔤",
+ "input_latin_lowercase": "🔡",
+ "input_latin_uppercase": "🔠",
+ "input_numbers": "🔢",
+ "input_symbols": "🔣",
+ "jack-o-lantern": "🎃",
+ "jeans": "👖",
+ "jigsaw": "🧩",
+ "joker": "🃏",
+ "joystick": "🕹",
+ "kaaba": "🕋",
+ "kangaroo": "🦘",
+ "key": "🔑",
+ "keyboard": "⌨",
+ "keycap_#": "#️⃣",
+ "keycap_*": "*️⃣",
+ "keycap_0": "0️⃣",
+ "keycap_1": "1️⃣",
+ "keycap_10": "🔟",
+ "keycap_2": "2️⃣",
+ "keycap_3": "3️⃣",
+ "keycap_4": "4️⃣",
+ "keycap_5": "5️⃣",
+ "keycap_6": "6️⃣",
+ "keycap_7": "7️⃣",
+ "keycap_8": "8️⃣",
+ "keycap_9": "9️⃣",
+ "kick_scooter": "🛴",
+ "kimono": "👘",
+ "kiss": "💋",
+ "kiss_man_man": "👨\u200d❤️\u200d💋\u200d👨",
+ "kiss_mark": "💋",
+ "kiss_woman_man": "👩\u200d❤️\u200d💋\u200d👨",
+ "kiss_woman_woman": "👩\u200d❤️\u200d💋\u200d👩",
+ "kissing_cat_face": "😽",
+ "kissing_face": "😗",
+ "kissing_face_with_closed_eyes": "😚",
+ "kissing_face_with_smiling_eyes": "😙",
+ "kitchen_knife": "🔪",
+ "kite": "🪁",
+ "kiwi_fruit": "🥝",
+ "koala": "🐨",
+ "lab_coat": "🥼",
+ "label": "🏷",
+ "lacrosse": "🥍",
+ "lady_beetle": "🐞",
+ "laptop_computer": "💻",
+ "large_blue_diamond": "🔷",
+ "large_orange_diamond": "🔶",
+ "last_quarter_moon": "🌗",
+ "last_quarter_moon_face": "🌜",
+ "last_track_button": "⏮",
+ "latin_cross": "✝",
+ "leaf_fluttering_in_wind": "🍃",
+ "leafy_green": "🥬",
+ "ledger": "📒",
+ "left-facing_fist": "🤛",
+ "left-facing_fist_dark_skin_tone": "🤛🏿",
+ "left-facing_fist_light_skin_tone": "🤛🏻",
+ "left-facing_fist_medium-dark_skin_tone": "🤛🏾",
+ "left-facing_fist_medium-light_skin_tone": "🤛🏼",
+ "left-facing_fist_medium_skin_tone": "🤛🏽",
+ "left-right_arrow": "↔",
+ "left_arrow": "⬅",
+ "left_arrow_curving_right": "↪",
+ "left_luggage": "🛅",
+ "left_speech_bubble": "🗨",
+ "leg": "🦵",
+ "lemon": "🍋",
+ "leopard": "🐆",
+ "level_slider": "🎚",
+ "light_bulb": "💡",
+ "light_rail": "🚈",
+ "link": "🔗",
+ "linked_paperclips": "🖇",
+ "lion_face": "🦁",
+ "lipstick": "💄",
+ "litter_in_bin_sign": "🚮",
+ "lizard": "🦎",
+ "llama": "🦙",
+ "lobster": "🦞",
+ "locked": "🔒",
+ "locked_with_key": "🔐",
+ "locked_with_pen": "🔏",
+ "locomotive": "🚂",
+ "lollipop": "🍭",
+ "lotion_bottle": "🧴",
+ "loudly_crying_face": "😭",
+ "loudspeaker": "📢",
+ "love-you_gesture": "🤟",
+ "love-you_gesture_dark_skin_tone": "🤟🏿",
+ "love-you_gesture_light_skin_tone": "🤟🏻",
+ "love-you_gesture_medium-dark_skin_tone": "🤟🏾",
+ "love-you_gesture_medium-light_skin_tone": "🤟🏼",
+ "love-you_gesture_medium_skin_tone": "🤟🏽",
+ "love_hotel": "🏩",
+ "love_letter": "💌",
+ "luggage": "🧳",
+ "lying_face": "🤥",
+ "mage": "🧙",
+ "mage_dark_skin_tone": "🧙🏿",
+ "mage_light_skin_tone": "🧙🏻",
+ "mage_medium-dark_skin_tone": "🧙🏾",
+ "mage_medium-light_skin_tone": "🧙🏼",
+ "mage_medium_skin_tone": "🧙🏽",
+ "magnet": "🧲",
+ "magnifying_glass_tilted_left": "🔍",
+ "magnifying_glass_tilted_right": "🔎",
+ "mahjong_red_dragon": "🀄",
+ "male_sign": "♂",
+ "man": "👨",
+ "man_and_woman_holding_hands": "👫",
+ "man_artist": "👨\u200d🎨",
+ "man_artist_dark_skin_tone": "👨🏿\u200d🎨",
+ "man_artist_light_skin_tone": "👨🏻\u200d🎨",
+ "man_artist_medium-dark_skin_tone": "👨🏾\u200d🎨",
+ "man_artist_medium-light_skin_tone": "👨🏼\u200d🎨",
+ "man_artist_medium_skin_tone": "👨🏽\u200d🎨",
+ "man_astronaut": "👨\u200d🚀",
+ "man_astronaut_dark_skin_tone": "👨🏿\u200d🚀",
+ "man_astronaut_light_skin_tone": "👨🏻\u200d🚀",
+ "man_astronaut_medium-dark_skin_tone": "👨🏾\u200d🚀",
+ "man_astronaut_medium-light_skin_tone": "👨🏼\u200d🚀",
+ "man_astronaut_medium_skin_tone": "👨🏽\u200d🚀",
+ "man_biking": "🚴\u200d♂️",
+ "man_biking_dark_skin_tone": "🚴🏿\u200d♂️",
+ "man_biking_light_skin_tone": "🚴🏻\u200d♂️",
+ "man_biking_medium-dark_skin_tone": "🚴🏾\u200d♂️",
+ "man_biking_medium-light_skin_tone": "🚴🏼\u200d♂️",
+ "man_biking_medium_skin_tone": "🚴🏽\u200d♂️",
+ "man_bouncing_ball": "⛹️\u200d♂️",
+ "man_bouncing_ball_dark_skin_tone": "⛹🏿\u200d♂️",
+ "man_bouncing_ball_light_skin_tone": "⛹🏻\u200d♂️",
+ "man_bouncing_ball_medium-dark_skin_tone": "⛹🏾\u200d♂️",
+ "man_bouncing_ball_medium-light_skin_tone": "⛹🏼\u200d♂️",
+ "man_bouncing_ball_medium_skin_tone": "⛹🏽\u200d♂️",
+ "man_bowing": "🙇\u200d♂️",
+ "man_bowing_dark_skin_tone": "🙇🏿\u200d♂️",
+ "man_bowing_light_skin_tone": "🙇🏻\u200d♂️",
+ "man_bowing_medium-dark_skin_tone": "🙇🏾\u200d♂️",
+ "man_bowing_medium-light_skin_tone": "🙇🏼\u200d♂️",
+ "man_bowing_medium_skin_tone": "🙇🏽\u200d♂️",
+ "man_cartwheeling": "🤸\u200d♂️",
+ "man_cartwheeling_dark_skin_tone": "🤸🏿\u200d♂️",
+ "man_cartwheeling_light_skin_tone": "🤸🏻\u200d♂️",
+ "man_cartwheeling_medium-dark_skin_tone": "🤸🏾\u200d♂️",
+ "man_cartwheeling_medium-light_skin_tone": "🤸🏼\u200d♂️",
+ "man_cartwheeling_medium_skin_tone": "🤸🏽\u200d♂️",
+ "man_climbing": "🧗\u200d♂️",
+ "man_climbing_dark_skin_tone": "🧗🏿\u200d♂️",
+ "man_climbing_light_skin_tone": "🧗🏻\u200d♂️",
+ "man_climbing_medium-dark_skin_tone": "🧗🏾\u200d♂️",
+ "man_climbing_medium-light_skin_tone": "🧗🏼\u200d♂️",
+ "man_climbing_medium_skin_tone": "🧗🏽\u200d♂️",
+ "man_construction_worker": "👷\u200d♂️",
+ "man_construction_worker_dark_skin_tone": "👷🏿\u200d♂️",
+ "man_construction_worker_light_skin_tone": "👷🏻\u200d♂️",
+ "man_construction_worker_medium-dark_skin_tone": "👷🏾\u200d♂️",
+ "man_construction_worker_medium-light_skin_tone": "👷🏼\u200d♂️",
+ "man_construction_worker_medium_skin_tone": "👷🏽\u200d♂️",
+ "man_cook": "👨\u200d🍳",
+ "man_cook_dark_skin_tone": "👨🏿\u200d🍳",
+ "man_cook_light_skin_tone": "👨🏻\u200d🍳",
+ "man_cook_medium-dark_skin_tone": "👨🏾\u200d🍳",
+ "man_cook_medium-light_skin_tone": "👨🏼\u200d🍳",
+ "man_cook_medium_skin_tone": "👨🏽\u200d🍳",
+ "man_dancing": "🕺",
+ "man_dancing_dark_skin_tone": "🕺🏿",
+ "man_dancing_light_skin_tone": "🕺🏻",
+ "man_dancing_medium-dark_skin_tone": "🕺🏾",
+ "man_dancing_medium-light_skin_tone": "🕺🏼",
+ "man_dancing_medium_skin_tone": "🕺🏽",
+ "man_dark_skin_tone": "👨🏿",
+ "man_detective": "🕵️\u200d♂️",
+ "man_detective_dark_skin_tone": "🕵🏿\u200d♂️",
+ "man_detective_light_skin_tone": "🕵🏻\u200d♂️",
+ "man_detective_medium-dark_skin_tone": "🕵🏾\u200d♂️",
+ "man_detective_medium-light_skin_tone": "🕵🏼\u200d♂️",
+ "man_detective_medium_skin_tone": "🕵🏽\u200d♂️",
+ "man_elf": "🧝\u200d♂️",
+ "man_elf_dark_skin_tone": "🧝🏿\u200d♂️",
+ "man_elf_light_skin_tone": "🧝🏻\u200d♂️",
+ "man_elf_medium-dark_skin_tone": "🧝🏾\u200d♂️",
+ "man_elf_medium-light_skin_tone": "🧝🏼\u200d♂️",
+ "man_elf_medium_skin_tone": "🧝🏽\u200d♂️",
+ "man_facepalming": "🤦\u200d♂️",
+ "man_facepalming_dark_skin_tone": "🤦🏿\u200d♂️",
+ "man_facepalming_light_skin_tone": "🤦🏻\u200d♂️",
+ "man_facepalming_medium-dark_skin_tone": "🤦🏾\u200d♂️",
+ "man_facepalming_medium-light_skin_tone": "🤦🏼\u200d♂️",
+ "man_facepalming_medium_skin_tone": "🤦🏽\u200d♂️",
+ "man_factory_worker": "👨\u200d🏭",
+ "man_factory_worker_dark_skin_tone": "👨🏿\u200d🏭",
+ "man_factory_worker_light_skin_tone": "👨🏻\u200d🏭",
+ "man_factory_worker_medium-dark_skin_tone": "👨🏾\u200d🏭",
+ "man_factory_worker_medium-light_skin_tone": "👨🏼\u200d🏭",
+ "man_factory_worker_medium_skin_tone": "👨🏽\u200d🏭",
+ "man_fairy": "🧚\u200d♂️",
+ "man_fairy_dark_skin_tone": "🧚🏿\u200d♂️",
+ "man_fairy_light_skin_tone": "🧚🏻\u200d♂️",
+ "man_fairy_medium-dark_skin_tone": "🧚🏾\u200d♂️",
+ "man_fairy_medium-light_skin_tone": "🧚🏼\u200d♂️",
+ "man_fairy_medium_skin_tone": "🧚🏽\u200d♂️",
+ "man_farmer": "👨\u200d🌾",
+ "man_farmer_dark_skin_tone": "👨🏿\u200d🌾",
+ "man_farmer_light_skin_tone": "👨🏻\u200d🌾",
+ "man_farmer_medium-dark_skin_tone": "👨🏾\u200d🌾",
+ "man_farmer_medium-light_skin_tone": "👨🏼\u200d🌾",
+ "man_farmer_medium_skin_tone": "👨🏽\u200d🌾",
+ "man_firefighter": "👨\u200d🚒",
+ "man_firefighter_dark_skin_tone": "👨🏿\u200d🚒",
+ "man_firefighter_light_skin_tone": "👨🏻\u200d🚒",
+ "man_firefighter_medium-dark_skin_tone": "👨🏾\u200d🚒",
+ "man_firefighter_medium-light_skin_tone": "👨🏼\u200d🚒",
+ "man_firefighter_medium_skin_tone": "👨🏽\u200d🚒",
+ "man_frowning": "🙍\u200d♂️",
+ "man_frowning_dark_skin_tone": "🙍🏿\u200d♂️",
+ "man_frowning_light_skin_tone": "🙍🏻\u200d♂️",
+ "man_frowning_medium-dark_skin_tone": "🙍🏾\u200d♂️",
+ "man_frowning_medium-light_skin_tone": "🙍🏼\u200d♂️",
+ "man_frowning_medium_skin_tone": "🙍🏽\u200d♂️",
+ "man_genie": "🧞\u200d♂️",
+ "man_gesturing_no": "🙅\u200d♂️",
+ "man_gesturing_no_dark_skin_tone": "🙅🏿\u200d♂️",
+ "man_gesturing_no_light_skin_tone": "🙅🏻\u200d♂️",
+ "man_gesturing_no_medium-dark_skin_tone": "🙅🏾\u200d♂️",
+ "man_gesturing_no_medium-light_skin_tone": "🙅🏼\u200d♂️",
+ "man_gesturing_no_medium_skin_tone": "🙅🏽\u200d♂️",
+ "man_gesturing_ok": "🙆\u200d♂️",
+ "man_gesturing_ok_dark_skin_tone": "🙆🏿\u200d♂️",
+ "man_gesturing_ok_light_skin_tone": "🙆🏻\u200d♂️",
+ "man_gesturing_ok_medium-dark_skin_tone": "🙆🏾\u200d♂️",
+ "man_gesturing_ok_medium-light_skin_tone": "🙆🏼\u200d♂️",
+ "man_gesturing_ok_medium_skin_tone": "🙆🏽\u200d♂️",
+ "man_getting_haircut": "💇\u200d♂️",
+ "man_getting_haircut_dark_skin_tone": "💇🏿\u200d♂️",
+ "man_getting_haircut_light_skin_tone": "💇🏻\u200d♂️",
+ "man_getting_haircut_medium-dark_skin_tone": "💇🏾\u200d♂️",
+ "man_getting_haircut_medium-light_skin_tone": "💇🏼\u200d♂️",
+ "man_getting_haircut_medium_skin_tone": "💇🏽\u200d♂️",
+ "man_getting_massage": "💆\u200d♂️",
+ "man_getting_massage_dark_skin_tone": "💆🏿\u200d♂️",
+ "man_getting_massage_light_skin_tone": "💆🏻\u200d♂️",
+ "man_getting_massage_medium-dark_skin_tone": "💆🏾\u200d♂️",
+ "man_getting_massage_medium-light_skin_tone": "💆🏼\u200d♂️",
+ "man_getting_massage_medium_skin_tone": "💆🏽\u200d♂️",
+ "man_golfing": "🏌️\u200d♂️",
+ "man_golfing_dark_skin_tone": "🏌🏿\u200d♂️",
+ "man_golfing_light_skin_tone": "🏌🏻\u200d♂️",
+ "man_golfing_medium-dark_skin_tone": "🏌🏾\u200d♂️",
+ "man_golfing_medium-light_skin_tone": "🏌🏼\u200d♂️",
+ "man_golfing_medium_skin_tone": "🏌🏽\u200d♂️",
+ "man_guard": "💂\u200d♂️",
+ "man_guard_dark_skin_tone": "💂🏿\u200d♂️",
+ "man_guard_light_skin_tone": "💂🏻\u200d♂️",
+ "man_guard_medium-dark_skin_tone": "💂🏾\u200d♂️",
+ "man_guard_medium-light_skin_tone": "💂🏼\u200d♂️",
+ "man_guard_medium_skin_tone": "💂🏽\u200d♂️",
+ "man_health_worker": "👨\u200d⚕️",
+ "man_health_worker_dark_skin_tone": "👨🏿\u200d⚕️",
+ "man_health_worker_light_skin_tone": "👨🏻\u200d⚕️",
+ "man_health_worker_medium-dark_skin_tone": "👨🏾\u200d⚕️",
+ "man_health_worker_medium-light_skin_tone": "👨🏼\u200d⚕️",
+ "man_health_worker_medium_skin_tone": "👨🏽\u200d⚕️",
+ "man_in_lotus_position": "🧘\u200d♂️",
+ "man_in_lotus_position_dark_skin_tone": "🧘🏿\u200d♂️",
+ "man_in_lotus_position_light_skin_tone": "🧘🏻\u200d♂️",
+ "man_in_lotus_position_medium-dark_skin_tone": "🧘🏾\u200d♂️",
+ "man_in_lotus_position_medium-light_skin_tone": "🧘🏼\u200d♂️",
+ "man_in_lotus_position_medium_skin_tone": "🧘🏽\u200d♂️",
+ "man_in_manual_wheelchair": "👨\u200d🦽",
+ "man_in_motorized_wheelchair": "👨\u200d🦼",
+ "man_in_steamy_room": "🧖\u200d♂️",
+ "man_in_steamy_room_dark_skin_tone": "🧖🏿\u200d♂️",
+ "man_in_steamy_room_light_skin_tone": "🧖🏻\u200d♂️",
+ "man_in_steamy_room_medium-dark_skin_tone": "🧖🏾\u200d♂️",
+ "man_in_steamy_room_medium-light_skin_tone": "🧖🏼\u200d♂️",
+ "man_in_steamy_room_medium_skin_tone": "🧖🏽\u200d♂️",
+ "man_in_suit_levitating": "🕴",
+ "man_in_suit_levitating_dark_skin_tone": "🕴🏿",
+ "man_in_suit_levitating_light_skin_tone": "🕴🏻",
+ "man_in_suit_levitating_medium-dark_skin_tone": "🕴🏾",
+ "man_in_suit_levitating_medium-light_skin_tone": "🕴🏼",
+ "man_in_suit_levitating_medium_skin_tone": "🕴🏽",
+ "man_in_tuxedo": "🤵",
+ "man_in_tuxedo_dark_skin_tone": "🤵🏿",
+ "man_in_tuxedo_light_skin_tone": "🤵🏻",
+ "man_in_tuxedo_medium-dark_skin_tone": "🤵🏾",
+ "man_in_tuxedo_medium-light_skin_tone": "🤵🏼",
+ "man_in_tuxedo_medium_skin_tone": "🤵🏽",
+ "man_judge": "👨\u200d⚖️",
+ "man_judge_dark_skin_tone": "👨🏿\u200d⚖️",
+ "man_judge_light_skin_tone": "👨🏻\u200d⚖️",
+ "man_judge_medium-dark_skin_tone": "👨🏾\u200d⚖️",
+ "man_judge_medium-light_skin_tone": "👨🏼\u200d⚖️",
+ "man_judge_medium_skin_tone": "👨🏽\u200d⚖️",
+ "man_juggling": "🤹\u200d♂️",
+ "man_juggling_dark_skin_tone": "🤹🏿\u200d♂️",
+ "man_juggling_light_skin_tone": "🤹🏻\u200d♂️",
+ "man_juggling_medium-dark_skin_tone": "🤹🏾\u200d♂️",
+ "man_juggling_medium-light_skin_tone": "🤹🏼\u200d♂️",
+ "man_juggling_medium_skin_tone": "🤹🏽\u200d♂️",
+ "man_lifting_weights": "🏋️\u200d♂️",
+ "man_lifting_weights_dark_skin_tone": "🏋🏿\u200d♂️",
+ "man_lifting_weights_light_skin_tone": "🏋🏻\u200d♂️",
+ "man_lifting_weights_medium-dark_skin_tone": "🏋🏾\u200d♂️",
+ "man_lifting_weights_medium-light_skin_tone": "🏋🏼\u200d♂️",
+ "man_lifting_weights_medium_skin_tone": "🏋🏽\u200d♂️",
+ "man_light_skin_tone": "👨🏻",
+ "man_mage": "🧙\u200d♂️",
+ "man_mage_dark_skin_tone": "🧙🏿\u200d♂️",
+ "man_mage_light_skin_tone": "🧙🏻\u200d♂️",
+ "man_mage_medium-dark_skin_tone": "🧙🏾\u200d♂️",
+ "man_mage_medium-light_skin_tone": "🧙🏼\u200d♂️",
+ "man_mage_medium_skin_tone": "🧙🏽\u200d♂️",
+ "man_mechanic": "👨\u200d🔧",
+ "man_mechanic_dark_skin_tone": "👨🏿\u200d🔧",
+ "man_mechanic_light_skin_tone": "👨🏻\u200d🔧",
+ "man_mechanic_medium-dark_skin_tone": "👨🏾\u200d🔧",
+ "man_mechanic_medium-light_skin_tone": "👨🏼\u200d🔧",
+ "man_mechanic_medium_skin_tone": "👨🏽\u200d🔧",
+ "man_medium-dark_skin_tone": "👨🏾",
+ "man_medium-light_skin_tone": "👨🏼",
+ "man_medium_skin_tone": "👨🏽",
+ "man_mountain_biking": "🚵\u200d♂️",
+ "man_mountain_biking_dark_skin_tone": "🚵🏿\u200d♂️",
+ "man_mountain_biking_light_skin_tone": "🚵🏻\u200d♂️",
+ "man_mountain_biking_medium-dark_skin_tone": "🚵🏾\u200d♂️",
+ "man_mountain_biking_medium-light_skin_tone": "🚵🏼\u200d♂️",
+ "man_mountain_biking_medium_skin_tone": "🚵🏽\u200d♂️",
+ "man_office_worker": "👨\u200d💼",
+ "man_office_worker_dark_skin_tone": "👨🏿\u200d💼",
+ "man_office_worker_light_skin_tone": "👨🏻\u200d💼",
+ "man_office_worker_medium-dark_skin_tone": "👨🏾\u200d💼",
+ "man_office_worker_medium-light_skin_tone": "👨🏼\u200d💼",
+ "man_office_worker_medium_skin_tone": "👨🏽\u200d💼",
+ "man_pilot": "👨\u200d✈️",
+ "man_pilot_dark_skin_tone": "👨🏿\u200d✈️",
+ "man_pilot_light_skin_tone": "👨🏻\u200d✈️",
+ "man_pilot_medium-dark_skin_tone": "👨🏾\u200d✈️",
+ "man_pilot_medium-light_skin_tone": "👨🏼\u200d✈️",
+ "man_pilot_medium_skin_tone": "👨🏽\u200d✈️",
+ "man_playing_handball": "🤾\u200d♂️",
+ "man_playing_handball_dark_skin_tone": "🤾🏿\u200d♂️",
+ "man_playing_handball_light_skin_tone": "🤾🏻\u200d♂️",
+ "man_playing_handball_medium-dark_skin_tone": "🤾🏾\u200d♂️",
+ "man_playing_handball_medium-light_skin_tone": "🤾🏼\u200d♂️",
+ "man_playing_handball_medium_skin_tone": "🤾🏽\u200d♂️",
+ "man_playing_water_polo": "🤽\u200d♂️",
+ "man_playing_water_polo_dark_skin_tone": "🤽🏿\u200d♂️",
+ "man_playing_water_polo_light_skin_tone": "🤽🏻\u200d♂️",
+ "man_playing_water_polo_medium-dark_skin_tone": "🤽🏾\u200d♂️",
+ "man_playing_water_polo_medium-light_skin_tone": "🤽🏼\u200d♂️",
+ "man_playing_water_polo_medium_skin_tone": "🤽🏽\u200d♂️",
+ "man_police_officer": "👮\u200d♂️",
+ "man_police_officer_dark_skin_tone": "👮🏿\u200d♂️",
+ "man_police_officer_light_skin_tone": "👮🏻\u200d♂️",
+ "man_police_officer_medium-dark_skin_tone": "👮🏾\u200d♂️",
+ "man_police_officer_medium-light_skin_tone": "👮🏼\u200d♂️",
+ "man_police_officer_medium_skin_tone": "👮🏽\u200d♂️",
+ "man_pouting": "🙎\u200d♂️",
+ "man_pouting_dark_skin_tone": "🙎🏿\u200d♂️",
+ "man_pouting_light_skin_tone": "🙎🏻\u200d♂️",
+ "man_pouting_medium-dark_skin_tone": "🙎🏾\u200d♂️",
+ "man_pouting_medium-light_skin_tone": "🙎🏼\u200d♂️",
+ "man_pouting_medium_skin_tone": "🙎🏽\u200d♂️",
+ "man_raising_hand": "🙋\u200d♂️",
+ "man_raising_hand_dark_skin_tone": "🙋🏿\u200d♂️",
+ "man_raising_hand_light_skin_tone": "🙋🏻\u200d♂️",
+ "man_raising_hand_medium-dark_skin_tone": "🙋🏾\u200d♂️",
+ "man_raising_hand_medium-light_skin_tone": "🙋🏼\u200d♂️",
+ "man_raising_hand_medium_skin_tone": "🙋🏽\u200d♂️",
+ "man_rowing_boat": "🚣\u200d♂️",
+ "man_rowing_boat_dark_skin_tone": "🚣🏿\u200d♂️",
+ "man_rowing_boat_light_skin_tone": "🚣🏻\u200d♂️",
+ "man_rowing_boat_medium-dark_skin_tone": "🚣🏾\u200d♂️",
+ "man_rowing_boat_medium-light_skin_tone": "🚣🏼\u200d♂️",
+ "man_rowing_boat_medium_skin_tone": "🚣🏽\u200d♂️",
+ "man_running": "🏃\u200d♂️",
+ "man_running_dark_skin_tone": "🏃🏿\u200d♂️",
+ "man_running_light_skin_tone": "🏃🏻\u200d♂️",
+ "man_running_medium-dark_skin_tone": "🏃🏾\u200d♂️",
+ "man_running_medium-light_skin_tone": "🏃🏼\u200d♂️",
+ "man_running_medium_skin_tone": "🏃🏽\u200d♂️",
+ "man_scientist": "👨\u200d🔬",
+ "man_scientist_dark_skin_tone": "👨🏿\u200d🔬",
+ "man_scientist_light_skin_tone": "👨🏻\u200d🔬",
+ "man_scientist_medium-dark_skin_tone": "👨🏾\u200d🔬",
+ "man_scientist_medium-light_skin_tone": "👨🏼\u200d🔬",
+ "man_scientist_medium_skin_tone": "👨🏽\u200d🔬",
+ "man_shrugging": "🤷\u200d♂️",
+ "man_shrugging_dark_skin_tone": "🤷🏿\u200d♂️",
+ "man_shrugging_light_skin_tone": "🤷🏻\u200d♂️",
+ "man_shrugging_medium-dark_skin_tone": "🤷🏾\u200d♂️",
+ "man_shrugging_medium-light_skin_tone": "🤷🏼\u200d♂️",
+ "man_shrugging_medium_skin_tone": "🤷🏽\u200d♂️",
+ "man_singer": "👨\u200d🎤",
+ "man_singer_dark_skin_tone": "👨🏿\u200d🎤",
+ "man_singer_light_skin_tone": "👨🏻\u200d🎤",
+ "man_singer_medium-dark_skin_tone": "👨🏾\u200d🎤",
+ "man_singer_medium-light_skin_tone": "👨🏼\u200d🎤",
+ "man_singer_medium_skin_tone": "👨🏽\u200d🎤",
+ "man_student": "👨\u200d🎓",
+ "man_student_dark_skin_tone": "👨🏿\u200d🎓",
+ "man_student_light_skin_tone": "👨🏻\u200d🎓",
+ "man_student_medium-dark_skin_tone": "👨🏾\u200d🎓",
+ "man_student_medium-light_skin_tone": "👨🏼\u200d🎓",
+ "man_student_medium_skin_tone": "👨🏽\u200d🎓",
+ "man_surfing": "🏄\u200d♂️",
+ "man_surfing_dark_skin_tone": "🏄🏿\u200d♂️",
+ "man_surfing_light_skin_tone": "🏄🏻\u200d♂️",
+ "man_surfing_medium-dark_skin_tone": "🏄🏾\u200d♂️",
+ "man_surfing_medium-light_skin_tone": "🏄🏼\u200d♂️",
+ "man_surfing_medium_skin_tone": "🏄🏽\u200d♂️",
+ "man_swimming": "🏊\u200d♂️",
+ "man_swimming_dark_skin_tone": "🏊🏿\u200d♂️",
+ "man_swimming_light_skin_tone": "🏊🏻\u200d♂️",
+ "man_swimming_medium-dark_skin_tone": "🏊🏾\u200d♂️",
+ "man_swimming_medium-light_skin_tone": "🏊🏼\u200d♂️",
+ "man_swimming_medium_skin_tone": "🏊🏽\u200d♂️",
+ "man_teacher": "👨\u200d🏫",
+ "man_teacher_dark_skin_tone": "👨🏿\u200d🏫",
+ "man_teacher_light_skin_tone": "👨🏻\u200d🏫",
+ "man_teacher_medium-dark_skin_tone": "👨🏾\u200d🏫",
+ "man_teacher_medium-light_skin_tone": "👨🏼\u200d🏫",
+ "man_teacher_medium_skin_tone": "👨🏽\u200d🏫",
+ "man_technologist": "👨\u200d💻",
+ "man_technologist_dark_skin_tone": "👨🏿\u200d💻",
+ "man_technologist_light_skin_tone": "👨🏻\u200d💻",
+ "man_technologist_medium-dark_skin_tone": "👨🏾\u200d💻",
+ "man_technologist_medium-light_skin_tone": "👨🏼\u200d💻",
+ "man_technologist_medium_skin_tone": "👨🏽\u200d💻",
+ "man_tipping_hand": "💁\u200d♂️",
+ "man_tipping_hand_dark_skin_tone": "💁🏿\u200d♂️",
+ "man_tipping_hand_light_skin_tone": "💁🏻\u200d♂️",
+ "man_tipping_hand_medium-dark_skin_tone": "💁🏾\u200d♂️",
+ "man_tipping_hand_medium-light_skin_tone": "💁🏼\u200d♂️",
+ "man_tipping_hand_medium_skin_tone": "💁🏽\u200d♂️",
+ "man_vampire": "🧛\u200d♂️",
+ "man_vampire_dark_skin_tone": "🧛🏿\u200d♂️",
+ "man_vampire_light_skin_tone": "🧛🏻\u200d♂️",
+ "man_vampire_medium-dark_skin_tone": "🧛🏾\u200d♂️",
+ "man_vampire_medium-light_skin_tone": "🧛🏼\u200d♂️",
+ "man_vampire_medium_skin_tone": "🧛🏽\u200d♂️",
+ "man_walking": "🚶\u200d♂️",
+ "man_walking_dark_skin_tone": "🚶🏿\u200d♂️",
+ "man_walking_light_skin_tone": "🚶🏻\u200d♂️",
+ "man_walking_medium-dark_skin_tone": "🚶🏾\u200d♂️",
+ "man_walking_medium-light_skin_tone": "🚶🏼\u200d♂️",
+ "man_walking_medium_skin_tone": "🚶🏽\u200d♂️",
+ "man_wearing_turban": "👳\u200d♂️",
+ "man_wearing_turban_dark_skin_tone": "👳🏿\u200d♂️",
+ "man_wearing_turban_light_skin_tone": "👳🏻\u200d♂️",
+ "man_wearing_turban_medium-dark_skin_tone": "👳🏾\u200d♂️",
+ "man_wearing_turban_medium-light_skin_tone": "👳🏼\u200d♂️",
+ "man_wearing_turban_medium_skin_tone": "👳🏽\u200d♂️",
+ "man_with_probing_cane": "👨\u200d🦯",
+ "man_with_chinese_cap": "👲",
+ "man_with_chinese_cap_dark_skin_tone": "👲🏿",
+ "man_with_chinese_cap_light_skin_tone": "👲🏻",
+ "man_with_chinese_cap_medium-dark_skin_tone": "👲🏾",
+ "man_with_chinese_cap_medium-light_skin_tone": "👲🏼",
+ "man_with_chinese_cap_medium_skin_tone": "👲🏽",
+ "man_zombie": "🧟\u200d♂️",
+ "mango": "🥭",
+ "mantelpiece_clock": "🕰",
+ "manual_wheelchair": "🦽",
+ "man’s_shoe": "👞",
+ "map_of_japan": "🗾",
+ "maple_leaf": "🍁",
+ "martial_arts_uniform": "🥋",
+ "mate": "🧉",
+ "meat_on_bone": "🍖",
+ "mechanical_arm": "🦾",
+ "mechanical_leg": "🦿",
+ "medical_symbol": "⚕",
+ "megaphone": "📣",
+ "melon": "🍈",
+ "memo": "📝",
+ "men_with_bunny_ears": "👯\u200d♂️",
+ "men_wrestling": "🤼\u200d♂️",
+ "menorah": "🕎",
+ "men’s_room": "🚹",
+ "mermaid": "🧜\u200d♀️",
+ "mermaid_dark_skin_tone": "🧜🏿\u200d♀️",
+ "mermaid_light_skin_tone": "🧜🏻\u200d♀️",
+ "mermaid_medium-dark_skin_tone": "🧜🏾\u200d♀️",
+ "mermaid_medium-light_skin_tone": "🧜🏼\u200d♀️",
+ "mermaid_medium_skin_tone": "🧜🏽\u200d♀️",
+ "merman": "🧜\u200d♂️",
+ "merman_dark_skin_tone": "🧜🏿\u200d♂️",
+ "merman_light_skin_tone": "🧜🏻\u200d♂️",
+ "merman_medium-dark_skin_tone": "🧜🏾\u200d♂️",
+ "merman_medium-light_skin_tone": "🧜🏼\u200d♂️",
+ "merman_medium_skin_tone": "🧜🏽\u200d♂️",
+ "merperson": "🧜",
+ "merperson_dark_skin_tone": "🧜🏿",
+ "merperson_light_skin_tone": "🧜🏻",
+ "merperson_medium-dark_skin_tone": "🧜🏾",
+ "merperson_medium-light_skin_tone": "🧜🏼",
+ "merperson_medium_skin_tone": "🧜🏽",
+ "metro": "🚇",
+ "microbe": "🦠",
+ "microphone": "🎤",
+ "microscope": "🔬",
+ "middle_finger": "🖕",
+ "middle_finger_dark_skin_tone": "🖕🏿",
+ "middle_finger_light_skin_tone": "🖕🏻",
+ "middle_finger_medium-dark_skin_tone": "🖕🏾",
+ "middle_finger_medium-light_skin_tone": "🖕🏼",
+ "middle_finger_medium_skin_tone": "🖕🏽",
+ "military_medal": "🎖",
+ "milky_way": "🌌",
+ "minibus": "🚐",
+ "moai": "🗿",
+ "mobile_phone": "📱",
+ "mobile_phone_off": "📴",
+ "mobile_phone_with_arrow": "📲",
+ "money-mouth_face": "🤑",
+ "money_bag": "💰",
+ "money_with_wings": "💸",
+ "monkey": "🐒",
+ "monkey_face": "🐵",
+ "monorail": "🚝",
+ "moon_cake": "🥮",
+ "moon_viewing_ceremony": "🎑",
+ "mosque": "🕌",
+ "mosquito": "🦟",
+ "motor_boat": "🛥",
+ "motor_scooter": "🛵",
+ "motorcycle": "🏍",
+ "motorized_wheelchair": "🦼",
+ "motorway": "🛣",
+ "mount_fuji": "🗻",
+ "mountain": "⛰",
+ "mountain_cableway": "🚠",
+ "mountain_railway": "🚞",
+ "mouse": "🐭",
+ "mouse_face": "🐭",
+ "mouth": "👄",
+ "movie_camera": "🎥",
+ "mushroom": "🍄",
+ "musical_keyboard": "🎹",
+ "musical_note": "🎵",
+ "musical_notes": "🎶",
+ "musical_score": "🎼",
+ "muted_speaker": "🔇",
+ "nail_polish": "💅",
+ "nail_polish_dark_skin_tone": "💅🏿",
+ "nail_polish_light_skin_tone": "💅🏻",
+ "nail_polish_medium-dark_skin_tone": "💅🏾",
+ "nail_polish_medium-light_skin_tone": "💅🏼",
+ "nail_polish_medium_skin_tone": "💅🏽",
+ "name_badge": "📛",
+ "national_park": "🏞",
+ "nauseated_face": "🤢",
+ "nazar_amulet": "🧿",
+ "necktie": "👔",
+ "nerd_face": "🤓",
+ "neutral_face": "😐",
+ "new_moon": "🌑",
+ "new_moon_face": "🌚",
+ "newspaper": "📰",
+ "next_track_button": "⏭",
+ "night_with_stars": "🌃",
+ "nine-thirty": "🕤",
+ "nine_o’clock": "🕘",
+ "no_bicycles": "🚳",
+ "no_entry": "⛔",
+ "no_littering": "🚯",
+ "no_mobile_phones": "📵",
+ "no_one_under_eighteen": "🔞",
+ "no_pedestrians": "🚷",
+ "no_smoking": "🚭",
+ "non-potable_water": "🚱",
+ "nose": "👃",
+ "nose_dark_skin_tone": "👃🏿",
+ "nose_light_skin_tone": "👃🏻",
+ "nose_medium-dark_skin_tone": "👃🏾",
+ "nose_medium-light_skin_tone": "👃🏼",
+ "nose_medium_skin_tone": "👃🏽",
+ "notebook": "📓",
+ "notebook_with_decorative_cover": "📔",
+ "nut_and_bolt": "🔩",
+ "octopus": "🐙",
+ "oden": "🍢",
+ "office_building": "🏢",
+ "ogre": "👹",
+ "oil_drum": "🛢",
+ "old_key": "🗝",
+ "old_man": "👴",
+ "old_man_dark_skin_tone": "👴🏿",
+ "old_man_light_skin_tone": "👴🏻",
+ "old_man_medium-dark_skin_tone": "👴🏾",
+ "old_man_medium-light_skin_tone": "👴🏼",
+ "old_man_medium_skin_tone": "👴🏽",
+ "old_woman": "👵",
+ "old_woman_dark_skin_tone": "👵🏿",
+ "old_woman_light_skin_tone": "👵🏻",
+ "old_woman_medium-dark_skin_tone": "👵🏾",
+ "old_woman_medium-light_skin_tone": "👵🏼",
+ "old_woman_medium_skin_tone": "👵🏽",
+ "older_adult": "🧓",
+ "older_adult_dark_skin_tone": "🧓🏿",
+ "older_adult_light_skin_tone": "🧓🏻",
+ "older_adult_medium-dark_skin_tone": "🧓🏾",
+ "older_adult_medium-light_skin_tone": "🧓🏼",
+ "older_adult_medium_skin_tone": "🧓🏽",
+ "om": "🕉",
+ "oncoming_automobile": "🚘",
+ "oncoming_bus": "🚍",
+ "oncoming_fist": "👊",
+ "oncoming_fist_dark_skin_tone": "👊🏿",
+ "oncoming_fist_light_skin_tone": "👊🏻",
+ "oncoming_fist_medium-dark_skin_tone": "👊🏾",
+ "oncoming_fist_medium-light_skin_tone": "👊🏼",
+ "oncoming_fist_medium_skin_tone": "👊🏽",
+ "oncoming_police_car": "🚔",
+ "oncoming_taxi": "🚖",
+ "one-piece_swimsuit": "🩱",
+ "one-thirty": "🕜",
+ "one_o’clock": "🕐",
+ "onion": "🧅",
+ "open_book": "📖",
+ "open_file_folder": "📂",
+ "open_hands": "👐",
+ "open_hands_dark_skin_tone": "👐🏿",
+ "open_hands_light_skin_tone": "👐🏻",
+ "open_hands_medium-dark_skin_tone": "👐🏾",
+ "open_hands_medium-light_skin_tone": "👐🏼",
+ "open_hands_medium_skin_tone": "👐🏽",
+ "open_mailbox_with_lowered_flag": "📭",
+ "open_mailbox_with_raised_flag": "📬",
+ "optical_disk": "💿",
+ "orange_book": "📙",
+ "orange_circle": "🟠",
+ "orange_heart": "🧡",
+ "orange_square": "🟧",
+ "orangutan": "🦧",
+ "orthodox_cross": "☦",
+ "otter": "🦦",
+ "outbox_tray": "📤",
+ "owl": "🦉",
+ "ox": "🐂",
+ "oyster": "🦪",
+ "package": "📦",
+ "page_facing_up": "📄",
+ "page_with_curl": "📃",
+ "pager": "📟",
+ "paintbrush": "🖌",
+ "palm_tree": "🌴",
+ "palms_up_together": "🤲",
+ "palms_up_together_dark_skin_tone": "🤲🏿",
+ "palms_up_together_light_skin_tone": "🤲🏻",
+ "palms_up_together_medium-dark_skin_tone": "🤲🏾",
+ "palms_up_together_medium-light_skin_tone": "🤲🏼",
+ "palms_up_together_medium_skin_tone": "🤲🏽",
+ "pancakes": "🥞",
+ "panda_face": "🐼",
+ "paperclip": "📎",
+ "parrot": "🦜",
+ "part_alternation_mark": "〽",
+ "party_popper": "🎉",
+ "partying_face": "🥳",
+ "passenger_ship": "🛳",
+ "passport_control": "🛂",
+ "pause_button": "⏸",
+ "paw_prints": "🐾",
+ "peace_symbol": "☮",
+ "peach": "🍑",
+ "peacock": "🦚",
+ "peanuts": "🥜",
+ "pear": "🍐",
+ "pen": "🖊",
+ "pencil": "📝",
+ "penguin": "🐧",
+ "pensive_face": "😔",
+ "people_holding_hands": "🧑\u200d🤝\u200d🧑",
+ "people_with_bunny_ears": "👯",
+ "people_wrestling": "🤼",
+ "performing_arts": "🎭",
+ "persevering_face": "😣",
+ "person_biking": "🚴",
+ "person_biking_dark_skin_tone": "🚴🏿",
+ "person_biking_light_skin_tone": "🚴🏻",
+ "person_biking_medium-dark_skin_tone": "🚴🏾",
+ "person_biking_medium-light_skin_tone": "🚴🏼",
+ "person_biking_medium_skin_tone": "🚴🏽",
+ "person_bouncing_ball": "⛹",
+ "person_bouncing_ball_dark_skin_tone": "⛹🏿",
+ "person_bouncing_ball_light_skin_tone": "⛹🏻",
+ "person_bouncing_ball_medium-dark_skin_tone": "⛹🏾",
+ "person_bouncing_ball_medium-light_skin_tone": "⛹🏼",
+ "person_bouncing_ball_medium_skin_tone": "⛹🏽",
+ "person_bowing": "🙇",
+ "person_bowing_dark_skin_tone": "🙇🏿",
+ "person_bowing_light_skin_tone": "🙇🏻",
+ "person_bowing_medium-dark_skin_tone": "🙇🏾",
+ "person_bowing_medium-light_skin_tone": "🙇🏼",
+ "person_bowing_medium_skin_tone": "🙇🏽",
+ "person_cartwheeling": "🤸",
+ "person_cartwheeling_dark_skin_tone": "🤸🏿",
+ "person_cartwheeling_light_skin_tone": "🤸🏻",
+ "person_cartwheeling_medium-dark_skin_tone": "🤸🏾",
+ "person_cartwheeling_medium-light_skin_tone": "🤸🏼",
+ "person_cartwheeling_medium_skin_tone": "🤸🏽",
+ "person_climbing": "🧗",
+ "person_climbing_dark_skin_tone": "🧗🏿",
+ "person_climbing_light_skin_tone": "🧗🏻",
+ "person_climbing_medium-dark_skin_tone": "🧗🏾",
+ "person_climbing_medium-light_skin_tone": "🧗🏼",
+ "person_climbing_medium_skin_tone": "🧗🏽",
+ "person_facepalming": "🤦",
+ "person_facepalming_dark_skin_tone": "🤦🏿",
+ "person_facepalming_light_skin_tone": "🤦🏻",
+ "person_facepalming_medium-dark_skin_tone": "🤦🏾",
+ "person_facepalming_medium-light_skin_tone": "🤦🏼",
+ "person_facepalming_medium_skin_tone": "🤦🏽",
+ "person_fencing": "🤺",
+ "person_frowning": "🙍",
+ "person_frowning_dark_skin_tone": "🙍🏿",
+ "person_frowning_light_skin_tone": "🙍🏻",
+ "person_frowning_medium-dark_skin_tone": "🙍🏾",
+ "person_frowning_medium-light_skin_tone": "🙍🏼",
+ "person_frowning_medium_skin_tone": "🙍🏽",
+ "person_gesturing_no": "🙅",
+ "person_gesturing_no_dark_skin_tone": "🙅🏿",
+ "person_gesturing_no_light_skin_tone": "🙅🏻",
+ "person_gesturing_no_medium-dark_skin_tone": "🙅🏾",
+ "person_gesturing_no_medium-light_skin_tone": "🙅🏼",
+ "person_gesturing_no_medium_skin_tone": "🙅🏽",
+ "person_gesturing_ok": "🙆",
+ "person_gesturing_ok_dark_skin_tone": "🙆🏿",
+ "person_gesturing_ok_light_skin_tone": "🙆🏻",
+ "person_gesturing_ok_medium-dark_skin_tone": "🙆🏾",
+ "person_gesturing_ok_medium-light_skin_tone": "🙆🏼",
+ "person_gesturing_ok_medium_skin_tone": "🙆🏽",
+ "person_getting_haircut": "💇",
+ "person_getting_haircut_dark_skin_tone": "💇🏿",
+ "person_getting_haircut_light_skin_tone": "💇🏻",
+ "person_getting_haircut_medium-dark_skin_tone": "💇🏾",
+ "person_getting_haircut_medium-light_skin_tone": "💇🏼",
+ "person_getting_haircut_medium_skin_tone": "💇🏽",
+ "person_getting_massage": "💆",
+ "person_getting_massage_dark_skin_tone": "💆🏿",
+ "person_getting_massage_light_skin_tone": "💆🏻",
+ "person_getting_massage_medium-dark_skin_tone": "💆🏾",
+ "person_getting_massage_medium-light_skin_tone": "💆🏼",
+ "person_getting_massage_medium_skin_tone": "💆🏽",
+ "person_golfing": "🏌",
+ "person_golfing_dark_skin_tone": "🏌🏿",
+ "person_golfing_light_skin_tone": "🏌🏻",
+ "person_golfing_medium-dark_skin_tone": "🏌🏾",
+ "person_golfing_medium-light_skin_tone": "🏌🏼",
+ "person_golfing_medium_skin_tone": "🏌🏽",
+ "person_in_bed": "🛌",
+ "person_in_bed_dark_skin_tone": "🛌🏿",
+ "person_in_bed_light_skin_tone": "🛌🏻",
+ "person_in_bed_medium-dark_skin_tone": "🛌🏾",
+ "person_in_bed_medium-light_skin_tone": "🛌🏼",
+ "person_in_bed_medium_skin_tone": "🛌🏽",
+ "person_in_lotus_position": "🧘",
+ "person_in_lotus_position_dark_skin_tone": "🧘🏿",
+ "person_in_lotus_position_light_skin_tone": "🧘🏻",
+ "person_in_lotus_position_medium-dark_skin_tone": "🧘🏾",
+ "person_in_lotus_position_medium-light_skin_tone": "🧘🏼",
+ "person_in_lotus_position_medium_skin_tone": "🧘🏽",
+ "person_in_steamy_room": "🧖",
+ "person_in_steamy_room_dark_skin_tone": "🧖🏿",
+ "person_in_steamy_room_light_skin_tone": "🧖🏻",
+ "person_in_steamy_room_medium-dark_skin_tone": "🧖🏾",
+ "person_in_steamy_room_medium-light_skin_tone": "🧖🏼",
+ "person_in_steamy_room_medium_skin_tone": "🧖🏽",
+ "person_juggling": "🤹",
+ "person_juggling_dark_skin_tone": "🤹🏿",
+ "person_juggling_light_skin_tone": "🤹🏻",
+ "person_juggling_medium-dark_skin_tone": "🤹🏾",
+ "person_juggling_medium-light_skin_tone": "🤹🏼",
+ "person_juggling_medium_skin_tone": "🤹🏽",
+ "person_kneeling": "🧎",
+ "person_lifting_weights": "🏋",
+ "person_lifting_weights_dark_skin_tone": "🏋🏿",
+ "person_lifting_weights_light_skin_tone": "🏋🏻",
+ "person_lifting_weights_medium-dark_skin_tone": "🏋🏾",
+ "person_lifting_weights_medium-light_skin_tone": "🏋🏼",
+ "person_lifting_weights_medium_skin_tone": "🏋🏽",
+ "person_mountain_biking": "🚵",
+ "person_mountain_biking_dark_skin_tone": "🚵🏿",
+ "person_mountain_biking_light_skin_tone": "🚵🏻",
+ "person_mountain_biking_medium-dark_skin_tone": "🚵🏾",
+ "person_mountain_biking_medium-light_skin_tone": "🚵🏼",
+ "person_mountain_biking_medium_skin_tone": "🚵🏽",
+ "person_playing_handball": "🤾",
+ "person_playing_handball_dark_skin_tone": "🤾🏿",
+ "person_playing_handball_light_skin_tone": "🤾🏻",
+ "person_playing_handball_medium-dark_skin_tone": "🤾🏾",
+ "person_playing_handball_medium-light_skin_tone": "🤾🏼",
+ "person_playing_handball_medium_skin_tone": "🤾🏽",
+ "person_playing_water_polo": "🤽",
+ "person_playing_water_polo_dark_skin_tone": "🤽🏿",
+ "person_playing_water_polo_light_skin_tone": "🤽🏻",
+ "person_playing_water_polo_medium-dark_skin_tone": "🤽🏾",
+ "person_playing_water_polo_medium-light_skin_tone": "🤽🏼",
+ "person_playing_water_polo_medium_skin_tone": "🤽🏽",
+ "person_pouting": "🙎",
+ "person_pouting_dark_skin_tone": "🙎🏿",
+ "person_pouting_light_skin_tone": "🙎🏻",
+ "person_pouting_medium-dark_skin_tone": "🙎🏾",
+ "person_pouting_medium-light_skin_tone": "🙎🏼",
+ "person_pouting_medium_skin_tone": "🙎🏽",
+ "person_raising_hand": "🙋",
+ "person_raising_hand_dark_skin_tone": "🙋🏿",
+ "person_raising_hand_light_skin_tone": "🙋🏻",
+ "person_raising_hand_medium-dark_skin_tone": "🙋🏾",
+ "person_raising_hand_medium-light_skin_tone": "🙋🏼",
+ "person_raising_hand_medium_skin_tone": "🙋🏽",
+ "person_rowing_boat": "🚣",
+ "person_rowing_boat_dark_skin_tone": "🚣🏿",
+ "person_rowing_boat_light_skin_tone": "🚣🏻",
+ "person_rowing_boat_medium-dark_skin_tone": "🚣🏾",
+ "person_rowing_boat_medium-light_skin_tone": "🚣🏼",
+ "person_rowing_boat_medium_skin_tone": "🚣🏽",
+ "person_running": "🏃",
+ "person_running_dark_skin_tone": "🏃🏿",
+ "person_running_light_skin_tone": "🏃🏻",
+ "person_running_medium-dark_skin_tone": "🏃🏾",
+ "person_running_medium-light_skin_tone": "🏃🏼",
+ "person_running_medium_skin_tone": "🏃🏽",
+ "person_shrugging": "🤷",
+ "person_shrugging_dark_skin_tone": "🤷🏿",
+ "person_shrugging_light_skin_tone": "🤷🏻",
+ "person_shrugging_medium-dark_skin_tone": "🤷🏾",
+ "person_shrugging_medium-light_skin_tone": "🤷🏼",
+ "person_shrugging_medium_skin_tone": "🤷🏽",
+ "person_standing": "🧍",
+ "person_surfing": "🏄",
+ "person_surfing_dark_skin_tone": "🏄🏿",
+ "person_surfing_light_skin_tone": "🏄🏻",
+ "person_surfing_medium-dark_skin_tone": "🏄🏾",
+ "person_surfing_medium-light_skin_tone": "🏄🏼",
+ "person_surfing_medium_skin_tone": "🏄🏽",
+ "person_swimming": "🏊",
+ "person_swimming_dark_skin_tone": "🏊🏿",
+ "person_swimming_light_skin_tone": "🏊🏻",
+ "person_swimming_medium-dark_skin_tone": "🏊🏾",
+ "person_swimming_medium-light_skin_tone": "🏊🏼",
+ "person_swimming_medium_skin_tone": "🏊🏽",
+ "person_taking_bath": "🛀",
+ "person_taking_bath_dark_skin_tone": "🛀🏿",
+ "person_taking_bath_light_skin_tone": "🛀🏻",
+ "person_taking_bath_medium-dark_skin_tone": "🛀🏾",
+ "person_taking_bath_medium-light_skin_tone": "🛀🏼",
+ "person_taking_bath_medium_skin_tone": "🛀🏽",
+ "person_tipping_hand": "💁",
+ "person_tipping_hand_dark_skin_tone": "💁🏿",
+ "person_tipping_hand_light_skin_tone": "💁🏻",
+ "person_tipping_hand_medium-dark_skin_tone": "💁🏾",
+ "person_tipping_hand_medium-light_skin_tone": "💁🏼",
+ "person_tipping_hand_medium_skin_tone": "💁🏽",
+ "person_walking": "🚶",
+ "person_walking_dark_skin_tone": "🚶🏿",
+ "person_walking_light_skin_tone": "🚶🏻",
+ "person_walking_medium-dark_skin_tone": "🚶🏾",
+ "person_walking_medium-light_skin_tone": "🚶🏼",
+ "person_walking_medium_skin_tone": "🚶🏽",
+ "person_wearing_turban": "👳",
+ "person_wearing_turban_dark_skin_tone": "👳🏿",
+ "person_wearing_turban_light_skin_tone": "👳🏻",
+ "person_wearing_turban_medium-dark_skin_tone": "👳🏾",
+ "person_wearing_turban_medium-light_skin_tone": "👳🏼",
+ "person_wearing_turban_medium_skin_tone": "👳🏽",
+ "petri_dish": "🧫",
+ "pick": "⛏",
+ "pie": "🥧",
+ "pig": "🐷",
+ "pig_face": "🐷",
+ "pig_nose": "🐽",
+ "pile_of_poo": "💩",
+ "pill": "💊",
+ "pinching_hand": "🤏",
+ "pine_decoration": "🎍",
+ "pineapple": "🍍",
+ "ping_pong": "🏓",
+ "pirate_flag": "🏴\u200d☠️",
+ "pistol": "🔫",
+ "pizza": "🍕",
+ "place_of_worship": "🛐",
+ "play_button": "▶",
+ "play_or_pause_button": "⏯",
+ "pleading_face": "🥺",
+ "police_car": "🚓",
+ "police_car_light": "🚨",
+ "police_officer": "👮",
+ "police_officer_dark_skin_tone": "👮🏿",
+ "police_officer_light_skin_tone": "👮🏻",
+ "police_officer_medium-dark_skin_tone": "👮🏾",
+ "police_officer_medium-light_skin_tone": "👮🏼",
+ "police_officer_medium_skin_tone": "👮🏽",
+ "poodle": "🐩",
+ "pool_8_ball": "🎱",
+ "popcorn": "🍿",
+ "post_office": "🏣",
+ "postal_horn": "📯",
+ "postbox": "📮",
+ "pot_of_food": "🍲",
+ "potable_water": "🚰",
+ "potato": "🥔",
+ "poultry_leg": "🍗",
+ "pound_banknote": "💷",
+ "pouting_cat_face": "😾",
+ "pouting_face": "😡",
+ "prayer_beads": "📿",
+ "pregnant_woman": "🤰",
+ "pregnant_woman_dark_skin_tone": "🤰🏿",
+ "pregnant_woman_light_skin_tone": "🤰🏻",
+ "pregnant_woman_medium-dark_skin_tone": "🤰🏾",
+ "pregnant_woman_medium-light_skin_tone": "🤰🏼",
+ "pregnant_woman_medium_skin_tone": "🤰🏽",
+ "pretzel": "🥨",
+ "probing_cane": "🦯",
+ "prince": "🤴",
+ "prince_dark_skin_tone": "🤴🏿",
+ "prince_light_skin_tone": "🤴🏻",
+ "prince_medium-dark_skin_tone": "🤴🏾",
+ "prince_medium-light_skin_tone": "🤴🏼",
+ "prince_medium_skin_tone": "🤴🏽",
+ "princess": "👸",
+ "princess_dark_skin_tone": "👸🏿",
+ "princess_light_skin_tone": "👸🏻",
+ "princess_medium-dark_skin_tone": "👸🏾",
+ "princess_medium-light_skin_tone": "👸🏼",
+ "princess_medium_skin_tone": "👸🏽",
+ "printer": "🖨",
+ "prohibited": "🚫",
+ "purple_circle": "🟣",
+ "purple_heart": "💜",
+ "purple_square": "🟪",
+ "purse": "👛",
+ "pushpin": "📌",
+ "question_mark": "❓",
+ "rabbit": "🐰",
+ "rabbit_face": "🐰",
+ "raccoon": "🦝",
+ "racing_car": "🏎",
+ "radio": "📻",
+ "radio_button": "🔘",
+ "radioactive": "☢",
+ "railway_car": "🚃",
+ "railway_track": "🛤",
+ "rainbow": "🌈",
+ "rainbow_flag": "🏳️\u200d🌈",
+ "raised_back_of_hand": "🤚",
+ "raised_back_of_hand_dark_skin_tone": "🤚🏿",
+ "raised_back_of_hand_light_skin_tone": "🤚🏻",
+ "raised_back_of_hand_medium-dark_skin_tone": "🤚🏾",
+ "raised_back_of_hand_medium-light_skin_tone": "🤚🏼",
+ "raised_back_of_hand_medium_skin_tone": "🤚🏽",
+ "raised_fist": "✊",
+ "raised_fist_dark_skin_tone": "✊🏿",
+ "raised_fist_light_skin_tone": "✊🏻",
+ "raised_fist_medium-dark_skin_tone": "✊🏾",
+ "raised_fist_medium-light_skin_tone": "✊🏼",
+ "raised_fist_medium_skin_tone": "✊🏽",
+ "raised_hand": "✋",
+ "raised_hand_dark_skin_tone": "✋🏿",
+ "raised_hand_light_skin_tone": "✋🏻",
+ "raised_hand_medium-dark_skin_tone": "✋🏾",
+ "raised_hand_medium-light_skin_tone": "✋🏼",
+ "raised_hand_medium_skin_tone": "✋🏽",
+ "raising_hands": "🙌",
+ "raising_hands_dark_skin_tone": "🙌🏿",
+ "raising_hands_light_skin_tone": "🙌🏻",
+ "raising_hands_medium-dark_skin_tone": "🙌🏾",
+ "raising_hands_medium-light_skin_tone": "🙌🏼",
+ "raising_hands_medium_skin_tone": "🙌🏽",
+ "ram": "🐏",
+ "rat": "🐀",
+ "razor": "🪒",
+ "ringed_planet": "🪐",
+ "receipt": "🧾",
+ "record_button": "⏺",
+ "recycling_symbol": "♻",
+ "red_apple": "🍎",
+ "red_circle": "🔴",
+ "red_envelope": "🧧",
+ "red_hair": "🦰",
+ "red-haired_man": "👨\u200d🦰",
+ "red-haired_woman": "👩\u200d🦰",
+ "red_heart": "❤",
+ "red_paper_lantern": "🏮",
+ "red_square": "🟥",
+ "red_triangle_pointed_down": "🔻",
+ "red_triangle_pointed_up": "🔺",
+ "registered": "®",
+ "relieved_face": "😌",
+ "reminder_ribbon": "🎗",
+ "repeat_button": "🔁",
+ "repeat_single_button": "🔂",
+ "rescue_worker’s_helmet": "⛑",
+ "restroom": "🚻",
+ "reverse_button": "◀",
+ "revolving_hearts": "💞",
+ "rhinoceros": "🦏",
+ "ribbon": "🎀",
+ "rice_ball": "🍙",
+ "rice_cracker": "🍘",
+ "right-facing_fist": "🤜",
+ "right-facing_fist_dark_skin_tone": "🤜🏿",
+ "right-facing_fist_light_skin_tone": "🤜🏻",
+ "right-facing_fist_medium-dark_skin_tone": "🤜🏾",
+ "right-facing_fist_medium-light_skin_tone": "🤜🏼",
+ "right-facing_fist_medium_skin_tone": "🤜🏽",
+ "right_anger_bubble": "🗯",
+ "right_arrow": "➡",
+ "right_arrow_curving_down": "⤵",
+ "right_arrow_curving_left": "↩",
+ "right_arrow_curving_up": "⤴",
+ "ring": "💍",
+ "roasted_sweet_potato": "🍠",
+ "robot_face": "🤖",
+ "rocket": "🚀",
+ "roll_of_paper": "🧻",
+ "rolled-up_newspaper": "🗞",
+ "roller_coaster": "🎢",
+ "rolling_on_the_floor_laughing": "🤣",
+ "rooster": "🐓",
+ "rose": "🌹",
+ "rosette": "🏵",
+ "round_pushpin": "📍",
+ "rugby_football": "🏉",
+ "running_shirt": "🎽",
+ "running_shoe": "👟",
+ "sad_but_relieved_face": "😥",
+ "safety_pin": "🧷",
+ "safety_vest": "🦺",
+ "salt": "🧂",
+ "sailboat": "⛵",
+ "sake": "🍶",
+ "sandwich": "🥪",
+ "sari": "🥻",
+ "satellite": "📡",
+ "satellite_antenna": "📡",
+ "sauropod": "🦕",
+ "saxophone": "🎷",
+ "scarf": "🧣",
+ "school": "🏫",
+ "school_backpack": "🎒",
+ "scissors": "✂",
+ "scorpion": "🦂",
+ "scroll": "📜",
+ "seat": "💺",
+ "see-no-evil_monkey": "🙈",
+ "seedling": "🌱",
+ "selfie": "🤳",
+ "selfie_dark_skin_tone": "🤳🏿",
+ "selfie_light_skin_tone": "🤳🏻",
+ "selfie_medium-dark_skin_tone": "🤳🏾",
+ "selfie_medium-light_skin_tone": "🤳🏼",
+ "selfie_medium_skin_tone": "🤳🏽",
+ "service_dog": "🐕\u200d🦺",
+ "seven-thirty": "🕢",
+ "seven_o’clock": "🕖",
+ "shallow_pan_of_food": "🥘",
+ "shamrock": "☘",
+ "shark": "🦈",
+ "shaved_ice": "🍧",
+ "sheaf_of_rice": "🌾",
+ "shield": "🛡",
+ "shinto_shrine": "⛩",
+ "ship": "🚢",
+ "shooting_star": "🌠",
+ "shopping_bags": "🛍",
+ "shopping_cart": "🛒",
+ "shortcake": "🍰",
+ "shorts": "🩳",
+ "shower": "🚿",
+ "shrimp": "🦐",
+ "shuffle_tracks_button": "🔀",
+ "shushing_face": "🤫",
+ "sign_of_the_horns": "🤘",
+ "sign_of_the_horns_dark_skin_tone": "🤘🏿",
+ "sign_of_the_horns_light_skin_tone": "🤘🏻",
+ "sign_of_the_horns_medium-dark_skin_tone": "🤘🏾",
+ "sign_of_the_horns_medium-light_skin_tone": "🤘🏼",
+ "sign_of_the_horns_medium_skin_tone": "🤘🏽",
+ "six-thirty": "🕡",
+ "six_o’clock": "🕕",
+ "skateboard": "🛹",
+ "skier": "⛷",
+ "skis": "🎿",
+ "skull": "💀",
+ "skull_and_crossbones": "☠",
+ "skunk": "🦨",
+ "sled": "🛷",
+ "sleeping_face": "😴",
+ "sleepy_face": "😪",
+ "slightly_frowning_face": "🙁",
+ "slightly_smiling_face": "🙂",
+ "slot_machine": "🎰",
+ "sloth": "🦥",
+ "small_airplane": "🛩",
+ "small_blue_diamond": "🔹",
+ "small_orange_diamond": "🔸",
+ "smiling_cat_face_with_heart-eyes": "😻",
+ "smiling_face": "☺",
+ "smiling_face_with_halo": "😇",
+ "smiling_face_with_3_hearts": "🥰",
+ "smiling_face_with_heart-eyes": "😍",
+ "smiling_face_with_horns": "😈",
+ "smiling_face_with_smiling_eyes": "😊",
+ "smiling_face_with_sunglasses": "😎",
+ "smirking_face": "😏",
+ "snail": "🐌",
+ "snake": "🐍",
+ "sneezing_face": "🤧",
+ "snow-capped_mountain": "🏔",
+ "snowboarder": "🏂",
+ "snowboarder_dark_skin_tone": "🏂🏿",
+ "snowboarder_light_skin_tone": "🏂🏻",
+ "snowboarder_medium-dark_skin_tone": "🏂🏾",
+ "snowboarder_medium-light_skin_tone": "🏂🏼",
+ "snowboarder_medium_skin_tone": "🏂🏽",
+ "snowflake": "❄",
+ "snowman": "☃",
+ "snowman_without_snow": "⛄",
+ "soap": "🧼",
+ "soccer_ball": "⚽",
+ "socks": "🧦",
+ "softball": "🥎",
+ "soft_ice_cream": "🍦",
+ "spade_suit": "♠",
+ "spaghetti": "🍝",
+ "sparkle": "❇",
+ "sparkler": "🎇",
+ "sparkles": "✨",
+ "sparkling_heart": "💖",
+ "speak-no-evil_monkey": "🙊",
+ "speaker_high_volume": "🔊",
+ "speaker_low_volume": "🔈",
+ "speaker_medium_volume": "🔉",
+ "speaking_head": "🗣",
+ "speech_balloon": "💬",
+ "speedboat": "🚤",
+ "spider": "🕷",
+ "spider_web": "🕸",
+ "spiral_calendar": "🗓",
+ "spiral_notepad": "🗒",
+ "spiral_shell": "🐚",
+ "spoon": "🥄",
+ "sponge": "🧽",
+ "sport_utility_vehicle": "🚙",
+ "sports_medal": "🏅",
+ "spouting_whale": "🐳",
+ "squid": "🦑",
+ "squinting_face_with_tongue": "😝",
+ "stadium": "🏟",
+ "star-struck": "🤩",
+ "star_and_crescent": "☪",
+ "star_of_david": "✡",
+ "station": "🚉",
+ "steaming_bowl": "🍜",
+ "stethoscope": "🩺",
+ "stop_button": "⏹",
+ "stop_sign": "🛑",
+ "stopwatch": "⏱",
+ "straight_ruler": "📏",
+ "strawberry": "🍓",
+ "studio_microphone": "🎙",
+ "stuffed_flatbread": "🥙",
+ "sun": "☀",
+ "sun_behind_cloud": "⛅",
+ "sun_behind_large_cloud": "🌥",
+ "sun_behind_rain_cloud": "🌦",
+ "sun_behind_small_cloud": "🌤",
+ "sun_with_face": "🌞",
+ "sunflower": "🌻",
+ "sunglasses": "😎",
+ "sunrise": "🌅",
+ "sunrise_over_mountains": "🌄",
+ "sunset": "🌇",
+ "superhero": "🦸",
+ "supervillain": "🦹",
+ "sushi": "🍣",
+ "suspension_railway": "🚟",
+ "swan": "🦢",
+ "sweat_droplets": "💦",
+ "synagogue": "🕍",
+ "syringe": "💉",
+ "t-shirt": "👕",
+ "taco": "🌮",
+ "takeout_box": "🥡",
+ "tanabata_tree": "🎋",
+ "tangerine": "🍊",
+ "taxi": "🚕",
+ "teacup_without_handle": "🍵",
+ "tear-off_calendar": "📆",
+ "teddy_bear": "🧸",
+ "telephone": "☎",
+ "telephone_receiver": "📞",
+ "telescope": "🔭",
+ "television": "📺",
+ "ten-thirty": "🕥",
+ "ten_o’clock": "🕙",
+ "tennis": "🎾",
+ "tent": "⛺",
+ "test_tube": "🧪",
+ "thermometer": "🌡",
+ "thinking_face": "🤔",
+ "thought_balloon": "💭",
+ "thread": "🧵",
+ "three-thirty": "🕞",
+ "three_o’clock": "🕒",
+ "thumbs_down": "👎",
+ "thumbs_down_dark_skin_tone": "👎🏿",
+ "thumbs_down_light_skin_tone": "👎🏻",
+ "thumbs_down_medium-dark_skin_tone": "👎🏾",
+ "thumbs_down_medium-light_skin_tone": "👎🏼",
+ "thumbs_down_medium_skin_tone": "👎🏽",
+ "thumbs_up": "👍",
+ "thumbs_up_dark_skin_tone": "👍🏿",
+ "thumbs_up_light_skin_tone": "👍🏻",
+ "thumbs_up_medium-dark_skin_tone": "👍🏾",
+ "thumbs_up_medium-light_skin_tone": "👍🏼",
+ "thumbs_up_medium_skin_tone": "👍🏽",
+ "ticket": "🎫",
+ "tiger": "🐯",
+ "tiger_face": "🐯",
+ "timer_clock": "⏲",
+ "tired_face": "😫",
+ "toolbox": "🧰",
+ "toilet": "🚽",
+ "tomato": "🍅",
+ "tongue": "👅",
+ "tooth": "🦷",
+ "top_hat": "🎩",
+ "tornado": "🌪",
+ "trackball": "🖲",
+ "tractor": "🚜",
+ "trade_mark": "™",
+ "train": "🚋",
+ "tram": "🚊",
+ "tram_car": "🚋",
+ "triangular_flag": "🚩",
+ "triangular_ruler": "📐",
+ "trident_emblem": "🔱",
+ "trolleybus": "🚎",
+ "trophy": "🏆",
+ "tropical_drink": "🍹",
+ "tropical_fish": "🐠",
+ "trumpet": "🎺",
+ "tulip": "🌷",
+ "tumbler_glass": "🥃",
+ "turtle": "🐢",
+ "twelve-thirty": "🕧",
+ "twelve_o’clock": "🕛",
+ "two-hump_camel": "🐫",
+ "two-thirty": "🕝",
+ "two_hearts": "💕",
+ "two_men_holding_hands": "👬",
+ "two_o’clock": "🕑",
+ "two_women_holding_hands": "👭",
+ "umbrella": "☂",
+ "umbrella_on_ground": "⛱",
+ "umbrella_with_rain_drops": "☔",
+ "unamused_face": "😒",
+ "unicorn_face": "🦄",
+ "unlocked": "🔓",
+ "up-down_arrow": "↕",
+ "up-left_arrow": "↖",
+ "up-right_arrow": "↗",
+ "up_arrow": "⬆",
+ "upside-down_face": "🙃",
+ "upwards_button": "🔼",
+ "vampire": "🧛",
+ "vampire_dark_skin_tone": "🧛🏿",
+ "vampire_light_skin_tone": "🧛🏻",
+ "vampire_medium-dark_skin_tone": "🧛🏾",
+ "vampire_medium-light_skin_tone": "🧛🏼",
+ "vampire_medium_skin_tone": "🧛🏽",
+ "vertical_traffic_light": "🚦",
+ "vibration_mode": "📳",
+ "victory_hand": "✌",
+ "victory_hand_dark_skin_tone": "✌🏿",
+ "victory_hand_light_skin_tone": "✌🏻",
+ "victory_hand_medium-dark_skin_tone": "✌🏾",
+ "victory_hand_medium-light_skin_tone": "✌🏼",
+ "victory_hand_medium_skin_tone": "✌🏽",
+ "video_camera": "📹",
+ "video_game": "🎮",
+ "videocassette": "📼",
+ "violin": "🎻",
+ "volcano": "🌋",
+ "volleyball": "🏐",
+ "vulcan_salute": "🖖",
+ "vulcan_salute_dark_skin_tone": "🖖🏿",
+ "vulcan_salute_light_skin_tone": "🖖🏻",
+ "vulcan_salute_medium-dark_skin_tone": "🖖🏾",
+ "vulcan_salute_medium-light_skin_tone": "🖖🏼",
+ "vulcan_salute_medium_skin_tone": "🖖🏽",
+ "waffle": "🧇",
+ "waning_crescent_moon": "🌘",
+ "waning_gibbous_moon": "🌖",
+ "warning": "⚠",
+ "wastebasket": "🗑",
+ "watch": "⌚",
+ "water_buffalo": "🐃",
+ "water_closet": "🚾",
+ "water_wave": "🌊",
+ "watermelon": "🍉",
+ "waving_hand": "👋",
+ "waving_hand_dark_skin_tone": "👋🏿",
+ "waving_hand_light_skin_tone": "👋🏻",
+ "waving_hand_medium-dark_skin_tone": "👋🏾",
+ "waving_hand_medium-light_skin_tone": "👋🏼",
+ "waving_hand_medium_skin_tone": "👋🏽",
+ "wavy_dash": "〰",
+ "waxing_crescent_moon": "🌒",
+ "waxing_gibbous_moon": "🌔",
+ "weary_cat_face": "🙀",
+ "weary_face": "😩",
+ "wedding": "💒",
+ "whale": "🐳",
+ "wheel_of_dharma": "☸",
+ "wheelchair_symbol": "♿",
+ "white_circle": "⚪",
+ "white_exclamation_mark": "❕",
+ "white_flag": "🏳",
+ "white_flower": "💮",
+ "white_hair": "🦳",
+ "white-haired_man": "👨\u200d🦳",
+ "white-haired_woman": "👩\u200d🦳",
+ "white_heart": "🤍",
+ "white_heavy_check_mark": "✅",
+ "white_large_square": "⬜",
+ "white_medium-small_square": "◽",
+ "white_medium_square": "◻",
+ "white_medium_star": "⭐",
+ "white_question_mark": "❔",
+ "white_small_square": "▫",
+ "white_square_button": "🔳",
+ "wilted_flower": "🥀",
+ "wind_chime": "🎐",
+ "wind_face": "🌬",
+ "wine_glass": "🍷",
+ "winking_face": "😉",
+ "winking_face_with_tongue": "😜",
+ "wolf_face": "🐺",
+ "woman": "👩",
+ "woman_artist": "👩\u200d🎨",
+ "woman_artist_dark_skin_tone": "👩🏿\u200d🎨",
+ "woman_artist_light_skin_tone": "👩🏻\u200d🎨",
+ "woman_artist_medium-dark_skin_tone": "👩🏾\u200d🎨",
+ "woman_artist_medium-light_skin_tone": "👩🏼\u200d🎨",
+ "woman_artist_medium_skin_tone": "👩🏽\u200d🎨",
+ "woman_astronaut": "👩\u200d🚀",
+ "woman_astronaut_dark_skin_tone": "👩🏿\u200d🚀",
+ "woman_astronaut_light_skin_tone": "👩🏻\u200d🚀",
+ "woman_astronaut_medium-dark_skin_tone": "👩🏾\u200d🚀",
+ "woman_astronaut_medium-light_skin_tone": "👩🏼\u200d🚀",
+ "woman_astronaut_medium_skin_tone": "👩🏽\u200d🚀",
+ "woman_biking": "🚴\u200d♀️",
+ "woman_biking_dark_skin_tone": "🚴🏿\u200d♀️",
+ "woman_biking_light_skin_tone": "🚴🏻\u200d♀️",
+ "woman_biking_medium-dark_skin_tone": "🚴🏾\u200d♀️",
+ "woman_biking_medium-light_skin_tone": "🚴🏼\u200d♀️",
+ "woman_biking_medium_skin_tone": "🚴🏽\u200d♀️",
+ "woman_bouncing_ball": "⛹️\u200d♀️",
+ "woman_bouncing_ball_dark_skin_tone": "⛹🏿\u200d♀️",
+ "woman_bouncing_ball_light_skin_tone": "⛹🏻\u200d♀️",
+ "woman_bouncing_ball_medium-dark_skin_tone": "⛹🏾\u200d♀️",
+ "woman_bouncing_ball_medium-light_skin_tone": "⛹🏼\u200d♀️",
+ "woman_bouncing_ball_medium_skin_tone": "⛹🏽\u200d♀️",
+ "woman_bowing": "🙇\u200d♀️",
+ "woman_bowing_dark_skin_tone": "🙇🏿\u200d♀️",
+ "woman_bowing_light_skin_tone": "🙇🏻\u200d♀️",
+ "woman_bowing_medium-dark_skin_tone": "🙇🏾\u200d♀️",
+ "woman_bowing_medium-light_skin_tone": "🙇🏼\u200d♀️",
+ "woman_bowing_medium_skin_tone": "🙇🏽\u200d♀️",
+ "woman_cartwheeling": "🤸\u200d♀️",
+ "woman_cartwheeling_dark_skin_tone": "🤸🏿\u200d♀️",
+ "woman_cartwheeling_light_skin_tone": "🤸🏻\u200d♀️",
+ "woman_cartwheeling_medium-dark_skin_tone": "🤸🏾\u200d♀️",
+ "woman_cartwheeling_medium-light_skin_tone": "🤸🏼\u200d♀️",
+ "woman_cartwheeling_medium_skin_tone": "🤸🏽\u200d♀️",
+ "woman_climbing": "🧗\u200d♀️",
+ "woman_climbing_dark_skin_tone": "🧗🏿\u200d♀️",
+ "woman_climbing_light_skin_tone": "🧗🏻\u200d♀️",
+ "woman_climbing_medium-dark_skin_tone": "🧗🏾\u200d♀️",
+ "woman_climbing_medium-light_skin_tone": "🧗🏼\u200d♀️",
+ "woman_climbing_medium_skin_tone": "🧗🏽\u200d♀️",
+ "woman_construction_worker": "👷\u200d♀️",
+ "woman_construction_worker_dark_skin_tone": "👷🏿\u200d♀️",
+ "woman_construction_worker_light_skin_tone": "👷🏻\u200d♀️",
+ "woman_construction_worker_medium-dark_skin_tone": "👷🏾\u200d♀️",
+ "woman_construction_worker_medium-light_skin_tone": "👷🏼\u200d♀️",
+ "woman_construction_worker_medium_skin_tone": "👷🏽\u200d♀️",
+ "woman_cook": "👩\u200d🍳",
+ "woman_cook_dark_skin_tone": "👩🏿\u200d🍳",
+ "woman_cook_light_skin_tone": "👩🏻\u200d🍳",
+ "woman_cook_medium-dark_skin_tone": "👩🏾\u200d🍳",
+ "woman_cook_medium-light_skin_tone": "👩🏼\u200d🍳",
+ "woman_cook_medium_skin_tone": "👩🏽\u200d🍳",
+ "woman_dancing": "💃",
+ "woman_dancing_dark_skin_tone": "💃🏿",
+ "woman_dancing_light_skin_tone": "💃🏻",
+ "woman_dancing_medium-dark_skin_tone": "💃🏾",
+ "woman_dancing_medium-light_skin_tone": "💃🏼",
+ "woman_dancing_medium_skin_tone": "💃🏽",
+ "woman_dark_skin_tone": "👩🏿",
+ "woman_detective": "🕵️\u200d♀️",
+ "woman_detective_dark_skin_tone": "🕵🏿\u200d♀️",
+ "woman_detective_light_skin_tone": "🕵🏻\u200d♀️",
+ "woman_detective_medium-dark_skin_tone": "🕵🏾\u200d♀️",
+ "woman_detective_medium-light_skin_tone": "🕵🏼\u200d♀️",
+ "woman_detective_medium_skin_tone": "🕵🏽\u200d♀️",
+ "woman_elf": "🧝\u200d♀️",
+ "woman_elf_dark_skin_tone": "🧝🏿\u200d♀️",
+ "woman_elf_light_skin_tone": "🧝🏻\u200d♀️",
+ "woman_elf_medium-dark_skin_tone": "🧝🏾\u200d♀️",
+ "woman_elf_medium-light_skin_tone": "🧝🏼\u200d♀️",
+ "woman_elf_medium_skin_tone": "🧝🏽\u200d♀️",
+ "woman_facepalming": "🤦\u200d♀️",
+ "woman_facepalming_dark_skin_tone": "🤦🏿\u200d♀️",
+ "woman_facepalming_light_skin_tone": "🤦🏻\u200d♀️",
+ "woman_facepalming_medium-dark_skin_tone": "🤦🏾\u200d♀️",
+ "woman_facepalming_medium-light_skin_tone": "🤦🏼\u200d♀️",
+ "woman_facepalming_medium_skin_tone": "🤦🏽\u200d♀️",
+ "woman_factory_worker": "👩\u200d🏭",
+ "woman_factory_worker_dark_skin_tone": "👩🏿\u200d🏭",
+ "woman_factory_worker_light_skin_tone": "👩🏻\u200d🏭",
+ "woman_factory_worker_medium-dark_skin_tone": "👩🏾\u200d🏭",
+ "woman_factory_worker_medium-light_skin_tone": "👩🏼\u200d🏭",
+ "woman_factory_worker_medium_skin_tone": "👩🏽\u200d🏭",
+ "woman_fairy": "🧚\u200d♀️",
+ "woman_fairy_dark_skin_tone": "🧚🏿\u200d♀️",
+ "woman_fairy_light_skin_tone": "🧚🏻\u200d♀️",
+ "woman_fairy_medium-dark_skin_tone": "🧚🏾\u200d♀️",
+ "woman_fairy_medium-light_skin_tone": "🧚🏼\u200d♀️",
+ "woman_fairy_medium_skin_tone": "🧚🏽\u200d♀️",
+ "woman_farmer": "👩\u200d🌾",
+ "woman_farmer_dark_skin_tone": "👩🏿\u200d🌾",
+ "woman_farmer_light_skin_tone": "👩🏻\u200d🌾",
+ "woman_farmer_medium-dark_skin_tone": "👩🏾\u200d🌾",
+ "woman_farmer_medium-light_skin_tone": "👩🏼\u200d🌾",
+ "woman_farmer_medium_skin_tone": "👩🏽\u200d🌾",
+ "woman_firefighter": "👩\u200d🚒",
+ "woman_firefighter_dark_skin_tone": "👩🏿\u200d🚒",
+ "woman_firefighter_light_skin_tone": "👩🏻\u200d🚒",
+ "woman_firefighter_medium-dark_skin_tone": "👩🏾\u200d🚒",
+ "woman_firefighter_medium-light_skin_tone": "👩🏼\u200d🚒",
+ "woman_firefighter_medium_skin_tone": "👩🏽\u200d🚒",
+ "woman_frowning": "🙍\u200d♀️",
+ "woman_frowning_dark_skin_tone": "🙍🏿\u200d♀️",
+ "woman_frowning_light_skin_tone": "🙍🏻\u200d♀️",
+ "woman_frowning_medium-dark_skin_tone": "🙍🏾\u200d♀️",
+ "woman_frowning_medium-light_skin_tone": "🙍🏼\u200d♀️",
+ "woman_frowning_medium_skin_tone": "🙍🏽\u200d♀️",
+ "woman_genie": "🧞\u200d♀️",
+ "woman_gesturing_no": "🙅\u200d♀️",
+ "woman_gesturing_no_dark_skin_tone": "🙅🏿\u200d♀️",
+ "woman_gesturing_no_light_skin_tone": "🙅🏻\u200d♀️",
+ "woman_gesturing_no_medium-dark_skin_tone": "🙅🏾\u200d♀️",
+ "woman_gesturing_no_medium-light_skin_tone": "🙅🏼\u200d♀️",
+ "woman_gesturing_no_medium_skin_tone": "🙅🏽\u200d♀️",
+ "woman_gesturing_ok": "🙆\u200d♀️",
+ "woman_gesturing_ok_dark_skin_tone": "🙆🏿\u200d♀️",
+ "woman_gesturing_ok_light_skin_tone": "🙆🏻\u200d♀️",
+ "woman_gesturing_ok_medium-dark_skin_tone": "🙆🏾\u200d♀️",
+ "woman_gesturing_ok_medium-light_skin_tone": "🙆🏼\u200d♀️",
+ "woman_gesturing_ok_medium_skin_tone": "🙆🏽\u200d♀️",
+ "woman_getting_haircut": "💇\u200d♀️",
+ "woman_getting_haircut_dark_skin_tone": "💇🏿\u200d♀️",
+ "woman_getting_haircut_light_skin_tone": "💇🏻\u200d♀️",
+ "woman_getting_haircut_medium-dark_skin_tone": "💇🏾\u200d♀️",
+ "woman_getting_haircut_medium-light_skin_tone": "💇🏼\u200d♀️",
+ "woman_getting_haircut_medium_skin_tone": "💇🏽\u200d♀️",
+ "woman_getting_massage": "💆\u200d♀️",
+ "woman_getting_massage_dark_skin_tone": "💆🏿\u200d♀️",
+ "woman_getting_massage_light_skin_tone": "💆🏻\u200d♀️",
+ "woman_getting_massage_medium-dark_skin_tone": "💆🏾\u200d♀️",
+ "woman_getting_massage_medium-light_skin_tone": "💆🏼\u200d♀️",
+ "woman_getting_massage_medium_skin_tone": "💆🏽\u200d♀️",
+ "woman_golfing": "🏌️\u200d♀️",
+ "woman_golfing_dark_skin_tone": "🏌🏿\u200d♀️",
+ "woman_golfing_light_skin_tone": "🏌🏻\u200d♀️",
+ "woman_golfing_medium-dark_skin_tone": "🏌🏾\u200d♀️",
+ "woman_golfing_medium-light_skin_tone": "🏌🏼\u200d♀️",
+ "woman_golfing_medium_skin_tone": "🏌🏽\u200d♀️",
+ "woman_guard": "💂\u200d♀️",
+ "woman_guard_dark_skin_tone": "💂🏿\u200d♀️",
+ "woman_guard_light_skin_tone": "💂🏻\u200d♀️",
+ "woman_guard_medium-dark_skin_tone": "💂🏾\u200d♀️",
+ "woman_guard_medium-light_skin_tone": "💂🏼\u200d♀️",
+ "woman_guard_medium_skin_tone": "💂🏽\u200d♀️",
+ "woman_health_worker": "👩\u200d⚕️",
+ "woman_health_worker_dark_skin_tone": "👩🏿\u200d⚕️",
+ "woman_health_worker_light_skin_tone": "👩🏻\u200d⚕️",
+ "woman_health_worker_medium-dark_skin_tone": "👩🏾\u200d⚕️",
+ "woman_health_worker_medium-light_skin_tone": "👩🏼\u200d⚕️",
+ "woman_health_worker_medium_skin_tone": "👩🏽\u200d⚕️",
+ "woman_in_lotus_position": "🧘\u200d♀️",
+ "woman_in_lotus_position_dark_skin_tone": "🧘🏿\u200d♀️",
+ "woman_in_lotus_position_light_skin_tone": "🧘🏻\u200d♀️",
+ "woman_in_lotus_position_medium-dark_skin_tone": "🧘🏾\u200d♀️",
+ "woman_in_lotus_position_medium-light_skin_tone": "🧘🏼\u200d♀️",
+ "woman_in_lotus_position_medium_skin_tone": "🧘🏽\u200d♀️",
+ "woman_in_manual_wheelchair": "👩\u200d🦽",
+ "woman_in_motorized_wheelchair": "👩\u200d🦼",
+ "woman_in_steamy_room": "🧖\u200d♀️",
+ "woman_in_steamy_room_dark_skin_tone": "🧖🏿\u200d♀️",
+ "woman_in_steamy_room_light_skin_tone": "🧖🏻\u200d♀️",
+ "woman_in_steamy_room_medium-dark_skin_tone": "🧖🏾\u200d♀️",
+ "woman_in_steamy_room_medium-light_skin_tone": "🧖🏼\u200d♀️",
+ "woman_in_steamy_room_medium_skin_tone": "🧖🏽\u200d♀️",
+ "woman_judge": "👩\u200d⚖️",
+ "woman_judge_dark_skin_tone": "👩🏿\u200d⚖️",
+ "woman_judge_light_skin_tone": "👩🏻\u200d⚖️",
+ "woman_judge_medium-dark_skin_tone": "👩🏾\u200d⚖️",
+ "woman_judge_medium-light_skin_tone": "👩🏼\u200d⚖️",
+ "woman_judge_medium_skin_tone": "👩🏽\u200d⚖️",
+ "woman_juggling": "🤹\u200d♀️",
+ "woman_juggling_dark_skin_tone": "🤹🏿\u200d♀️",
+ "woman_juggling_light_skin_tone": "🤹🏻\u200d♀️",
+ "woman_juggling_medium-dark_skin_tone": "🤹🏾\u200d♀️",
+ "woman_juggling_medium-light_skin_tone": "🤹🏼\u200d♀️",
+ "woman_juggling_medium_skin_tone": "🤹🏽\u200d♀️",
+ "woman_lifting_weights": "🏋️\u200d♀️",
+ "woman_lifting_weights_dark_skin_tone": "🏋🏿\u200d♀️",
+ "woman_lifting_weights_light_skin_tone": "🏋🏻\u200d♀️",
+ "woman_lifting_weights_medium-dark_skin_tone": "🏋🏾\u200d♀️",
+ "woman_lifting_weights_medium-light_skin_tone": "🏋🏼\u200d♀️",
+ "woman_lifting_weights_medium_skin_tone": "🏋🏽\u200d♀️",
+ "woman_light_skin_tone": "👩🏻",
+ "woman_mage": "🧙\u200d♀️",
+ "woman_mage_dark_skin_tone": "🧙🏿\u200d♀️",
+ "woman_mage_light_skin_tone": "🧙🏻\u200d♀️",
+ "woman_mage_medium-dark_skin_tone": "🧙🏾\u200d♀️",
+ "woman_mage_medium-light_skin_tone": "🧙🏼\u200d♀️",
+ "woman_mage_medium_skin_tone": "🧙🏽\u200d♀️",
+ "woman_mechanic": "👩\u200d🔧",
+ "woman_mechanic_dark_skin_tone": "👩🏿\u200d🔧",
+ "woman_mechanic_light_skin_tone": "👩🏻\u200d🔧",
+ "woman_mechanic_medium-dark_skin_tone": "👩🏾\u200d🔧",
+ "woman_mechanic_medium-light_skin_tone": "👩🏼\u200d🔧",
+ "woman_mechanic_medium_skin_tone": "👩🏽\u200d🔧",
+ "woman_medium-dark_skin_tone": "👩🏾",
+ "woman_medium-light_skin_tone": "👩🏼",
+ "woman_medium_skin_tone": "👩🏽",
+ "woman_mountain_biking": "🚵\u200d♀️",
+ "woman_mountain_biking_dark_skin_tone": "🚵🏿\u200d♀️",
+ "woman_mountain_biking_light_skin_tone": "🚵🏻\u200d♀️",
+ "woman_mountain_biking_medium-dark_skin_tone": "🚵🏾\u200d♀️",
+ "woman_mountain_biking_medium-light_skin_tone": "🚵🏼\u200d♀️",
+ "woman_mountain_biking_medium_skin_tone": "🚵🏽\u200d♀️",
+ "woman_office_worker": "👩\u200d💼",
+ "woman_office_worker_dark_skin_tone": "👩🏿\u200d💼",
+ "woman_office_worker_light_skin_tone": "👩🏻\u200d💼",
+ "woman_office_worker_medium-dark_skin_tone": "👩🏾\u200d💼",
+ "woman_office_worker_medium-light_skin_tone": "👩🏼\u200d💼",
+ "woman_office_worker_medium_skin_tone": "👩🏽\u200d💼",
+ "woman_pilot": "👩\u200d✈️",
+ "woman_pilot_dark_skin_tone": "👩🏿\u200d✈️",
+ "woman_pilot_light_skin_tone": "👩🏻\u200d✈️",
+ "woman_pilot_medium-dark_skin_tone": "👩🏾\u200d✈️",
+ "woman_pilot_medium-light_skin_tone": "👩🏼\u200d✈️",
+ "woman_pilot_medium_skin_tone": "👩🏽\u200d✈️",
+ "woman_playing_handball": "🤾\u200d♀️",
+ "woman_playing_handball_dark_skin_tone": "🤾🏿\u200d♀️",
+ "woman_playing_handball_light_skin_tone": "🤾🏻\u200d♀️",
+ "woman_playing_handball_medium-dark_skin_tone": "🤾🏾\u200d♀️",
+ "woman_playing_handball_medium-light_skin_tone": "🤾🏼\u200d♀️",
+ "woman_playing_handball_medium_skin_tone": "🤾🏽\u200d♀️",
+ "woman_playing_water_polo": "🤽\u200d♀️",
+ "woman_playing_water_polo_dark_skin_tone": "🤽🏿\u200d♀️",
+ "woman_playing_water_polo_light_skin_tone": "🤽🏻\u200d♀️",
+ "woman_playing_water_polo_medium-dark_skin_tone": "🤽🏾\u200d♀️",
+ "woman_playing_water_polo_medium-light_skin_tone": "🤽🏼\u200d♀️",
+ "woman_playing_water_polo_medium_skin_tone": "🤽🏽\u200d♀️",
+ "woman_police_officer": "👮\u200d♀️",
+ "woman_police_officer_dark_skin_tone": "👮🏿\u200d♀️",
+ "woman_police_officer_light_skin_tone": "👮🏻\u200d♀️",
+ "woman_police_officer_medium-dark_skin_tone": "👮🏾\u200d♀️",
+ "woman_police_officer_medium-light_skin_tone": "👮🏼\u200d♀️",
+ "woman_police_officer_medium_skin_tone": "👮🏽\u200d♀️",
+ "woman_pouting": "🙎\u200d♀️",
+ "woman_pouting_dark_skin_tone": "🙎🏿\u200d♀️",
+ "woman_pouting_light_skin_tone": "🙎🏻\u200d♀️",
+ "woman_pouting_medium-dark_skin_tone": "🙎🏾\u200d♀️",
+ "woman_pouting_medium-light_skin_tone": "🙎🏼\u200d♀️",
+ "woman_pouting_medium_skin_tone": "🙎🏽\u200d♀️",
+ "woman_raising_hand": "🙋\u200d♀️",
+ "woman_raising_hand_dark_skin_tone": "🙋🏿\u200d♀️",
+ "woman_raising_hand_light_skin_tone": "🙋🏻\u200d♀️",
+ "woman_raising_hand_medium-dark_skin_tone": "🙋🏾\u200d♀️",
+ "woman_raising_hand_medium-light_skin_tone": "🙋🏼\u200d♀️",
+ "woman_raising_hand_medium_skin_tone": "🙋🏽\u200d♀️",
+ "woman_rowing_boat": "🚣\u200d♀️",
+ "woman_rowing_boat_dark_skin_tone": "🚣🏿\u200d♀️",
+ "woman_rowing_boat_light_skin_tone": "🚣🏻\u200d♀️",
+ "woman_rowing_boat_medium-dark_skin_tone": "🚣🏾\u200d♀️",
+ "woman_rowing_boat_medium-light_skin_tone": "🚣🏼\u200d♀️",
+ "woman_rowing_boat_medium_skin_tone": "🚣🏽\u200d♀️",
+ "woman_running": "🏃\u200d♀️",
+ "woman_running_dark_skin_tone": "🏃🏿\u200d♀️",
+ "woman_running_light_skin_tone": "🏃🏻\u200d♀️",
+ "woman_running_medium-dark_skin_tone": "🏃🏾\u200d♀️",
+ "woman_running_medium-light_skin_tone": "🏃🏼\u200d♀️",
+ "woman_running_medium_skin_tone": "🏃🏽\u200d♀️",
+ "woman_scientist": "👩\u200d🔬",
+ "woman_scientist_dark_skin_tone": "👩🏿\u200d🔬",
+ "woman_scientist_light_skin_tone": "👩🏻\u200d🔬",
+ "woman_scientist_medium-dark_skin_tone": "👩🏾\u200d🔬",
+ "woman_scientist_medium-light_skin_tone": "👩🏼\u200d🔬",
+ "woman_scientist_medium_skin_tone": "👩🏽\u200d🔬",
+ "woman_shrugging": "🤷\u200d♀️",
+ "woman_shrugging_dark_skin_tone": "🤷🏿\u200d♀️",
+ "woman_shrugging_light_skin_tone": "🤷🏻\u200d♀️",
+ "woman_shrugging_medium-dark_skin_tone": "🤷🏾\u200d♀️",
+ "woman_shrugging_medium-light_skin_tone": "🤷🏼\u200d♀️",
+ "woman_shrugging_medium_skin_tone": "🤷🏽\u200d♀️",
+ "woman_singer": "👩\u200d🎤",
+ "woman_singer_dark_skin_tone": "👩🏿\u200d🎤",
+ "woman_singer_light_skin_tone": "👩🏻\u200d🎤",
+ "woman_singer_medium-dark_skin_tone": "👩🏾\u200d🎤",
+ "woman_singer_medium-light_skin_tone": "👩🏼\u200d🎤",
+ "woman_singer_medium_skin_tone": "👩🏽\u200d🎤",
+ "woman_student": "👩\u200d🎓",
+ "woman_student_dark_skin_tone": "👩🏿\u200d🎓",
+ "woman_student_light_skin_tone": "👩🏻\u200d🎓",
+ "woman_student_medium-dark_skin_tone": "👩🏾\u200d🎓",
+ "woman_student_medium-light_skin_tone": "👩🏼\u200d🎓",
+ "woman_student_medium_skin_tone": "👩🏽\u200d🎓",
+ "woman_surfing": "🏄\u200d♀️",
+ "woman_surfing_dark_skin_tone": "🏄🏿\u200d♀️",
+ "woman_surfing_light_skin_tone": "🏄🏻\u200d♀️",
+ "woman_surfing_medium-dark_skin_tone": "🏄🏾\u200d♀️",
+ "woman_surfing_medium-light_skin_tone": "🏄🏼\u200d♀️",
+ "woman_surfing_medium_skin_tone": "🏄🏽\u200d♀️",
+ "woman_swimming": "🏊\u200d♀️",
+ "woman_swimming_dark_skin_tone": "🏊🏿\u200d♀️",
+ "woman_swimming_light_skin_tone": "🏊🏻\u200d♀️",
+ "woman_swimming_medium-dark_skin_tone": "🏊🏾\u200d♀️",
+ "woman_swimming_medium-light_skin_tone": "🏊🏼\u200d♀️",
+ "woman_swimming_medium_skin_tone": "🏊🏽\u200d♀️",
+ "woman_teacher": "👩\u200d🏫",
+ "woman_teacher_dark_skin_tone": "👩🏿\u200d🏫",
+ "woman_teacher_light_skin_tone": "👩🏻\u200d🏫",
+ "woman_teacher_medium-dark_skin_tone": "👩🏾\u200d🏫",
+ "woman_teacher_medium-light_skin_tone": "👩🏼\u200d🏫",
+ "woman_teacher_medium_skin_tone": "👩🏽\u200d🏫",
+ "woman_technologist": "👩\u200d💻",
+ "woman_technologist_dark_skin_tone": "👩🏿\u200d💻",
+ "woman_technologist_light_skin_tone": "👩🏻\u200d💻",
+ "woman_technologist_medium-dark_skin_tone": "👩🏾\u200d💻",
+ "woman_technologist_medium-light_skin_tone": "👩🏼\u200d💻",
+ "woman_technologist_medium_skin_tone": "👩🏽\u200d💻",
+ "woman_tipping_hand": "💁\u200d♀️",
+ "woman_tipping_hand_dark_skin_tone": "💁🏿\u200d♀️",
+ "woman_tipping_hand_light_skin_tone": "💁🏻\u200d♀️",
+ "woman_tipping_hand_medium-dark_skin_tone": "💁🏾\u200d♀️",
+ "woman_tipping_hand_medium-light_skin_tone": "💁🏼\u200d♀️",
+ "woman_tipping_hand_medium_skin_tone": "💁🏽\u200d♀️",
+ "woman_vampire": "🧛\u200d♀️",
+ "woman_vampire_dark_skin_tone": "🧛🏿\u200d♀️",
+ "woman_vampire_light_skin_tone": "🧛🏻\u200d♀️",
+ "woman_vampire_medium-dark_skin_tone": "🧛🏾\u200d♀️",
+ "woman_vampire_medium-light_skin_tone": "🧛🏼\u200d♀️",
+ "woman_vampire_medium_skin_tone": "🧛🏽\u200d♀️",
+ "woman_walking": "🚶\u200d♀️",
+ "woman_walking_dark_skin_tone": "🚶🏿\u200d♀️",
+ "woman_walking_light_skin_tone": "🚶🏻\u200d♀️",
+ "woman_walking_medium-dark_skin_tone": "🚶🏾\u200d♀️",
+ "woman_walking_medium-light_skin_tone": "🚶🏼\u200d♀️",
+ "woman_walking_medium_skin_tone": "🚶🏽\u200d♀️",
+ "woman_wearing_turban": "👳\u200d♀️",
+ "woman_wearing_turban_dark_skin_tone": "👳🏿\u200d♀️",
+ "woman_wearing_turban_light_skin_tone": "👳🏻\u200d♀️",
+ "woman_wearing_turban_medium-dark_skin_tone": "👳🏾\u200d♀️",
+ "woman_wearing_turban_medium-light_skin_tone": "👳🏼\u200d♀️",
+ "woman_wearing_turban_medium_skin_tone": "👳🏽\u200d♀️",
+ "woman_with_headscarf": "🧕",
+ "woman_with_headscarf_dark_skin_tone": "🧕🏿",
+ "woman_with_headscarf_light_skin_tone": "🧕🏻",
+ "woman_with_headscarf_medium-dark_skin_tone": "🧕🏾",
+ "woman_with_headscarf_medium-light_skin_tone": "🧕🏼",
+ "woman_with_headscarf_medium_skin_tone": "🧕🏽",
+ "woman_with_probing_cane": "👩\u200d🦯",
+ "woman_zombie": "🧟\u200d♀️",
+ "woman’s_boot": "👢",
+ "woman’s_clothes": "👚",
+ "woman’s_hat": "👒",
+ "woman’s_sandal": "👡",
+ "women_with_bunny_ears": "👯\u200d♀️",
+ "women_wrestling": "🤼\u200d♀️",
+ "women’s_room": "🚺",
+ "woozy_face": "🥴",
+ "world_map": "🗺",
+ "worried_face": "😟",
+ "wrapped_gift": "🎁",
+ "wrench": "🔧",
+ "writing_hand": "✍",
+ "writing_hand_dark_skin_tone": "✍🏿",
+ "writing_hand_light_skin_tone": "✍🏻",
+ "writing_hand_medium-dark_skin_tone": "✍🏾",
+ "writing_hand_medium-light_skin_tone": "✍🏼",
+ "writing_hand_medium_skin_tone": "✍🏽",
+ "yarn": "🧶",
+ "yawning_face": "🥱",
+ "yellow_circle": "🟡",
+ "yellow_heart": "💛",
+ "yellow_square": "🟨",
+ "yen_banknote": "💴",
+ "yo-yo": "🪀",
+ "yin_yang": "☯",
+ "zany_face": "🤪",
+ "zebra": "🦓",
+ "zipper-mouth_face": "🤐",
+ "zombie": "🧟",
+ "zzz": "💤",
+ "åland_islands": "🇦🇽",
+ "keycap_asterisk": "*⃣",
+ "keycap_digit_eight": "8⃣",
+ "keycap_digit_five": "5⃣",
+ "keycap_digit_four": "4⃣",
+ "keycap_digit_nine": "9⃣",
+ "keycap_digit_one": "1⃣",
+ "keycap_digit_seven": "7⃣",
+ "keycap_digit_six": "6⃣",
+ "keycap_digit_three": "3⃣",
+ "keycap_digit_two": "2⃣",
+ "keycap_digit_zero": "0⃣",
+ "keycap_number_sign": "#⃣",
+ "light_skin_tone": "🏻",
+ "medium_light_skin_tone": "🏼",
+ "medium_skin_tone": "🏽",
+ "medium_dark_skin_tone": "🏾",
+ "dark_skin_tone": "🏿",
+ "regional_indicator_symbol_letter_a": "🇦",
+ "regional_indicator_symbol_letter_b": "🇧",
+ "regional_indicator_symbol_letter_c": "🇨",
+ "regional_indicator_symbol_letter_d": "🇩",
+ "regional_indicator_symbol_letter_e": "🇪",
+ "regional_indicator_symbol_letter_f": "🇫",
+ "regional_indicator_symbol_letter_g": "🇬",
+ "regional_indicator_symbol_letter_h": "🇭",
+ "regional_indicator_symbol_letter_i": "🇮",
+ "regional_indicator_symbol_letter_j": "🇯",
+ "regional_indicator_symbol_letter_k": "🇰",
+ "regional_indicator_symbol_letter_l": "🇱",
+ "regional_indicator_symbol_letter_m": "🇲",
+ "regional_indicator_symbol_letter_n": "🇳",
+ "regional_indicator_symbol_letter_o": "🇴",
+ "regional_indicator_symbol_letter_p": "🇵",
+ "regional_indicator_symbol_letter_q": "🇶",
+ "regional_indicator_symbol_letter_r": "🇷",
+ "regional_indicator_symbol_letter_s": "🇸",
+ "regional_indicator_symbol_letter_t": "🇹",
+ "regional_indicator_symbol_letter_u": "🇺",
+ "regional_indicator_symbol_letter_v": "🇻",
+ "regional_indicator_symbol_letter_w": "🇼",
+ "regional_indicator_symbol_letter_x": "🇽",
+ "regional_indicator_symbol_letter_y": "🇾",
+ "regional_indicator_symbol_letter_z": "🇿",
+ "airplane_arriving": "🛬",
+ "space_invader": "👾",
+ "football": "🏈",
+ "anger": "💢",
+ "angry": "😠",
+ "anguished": "😧",
+ "signal_strength": "📶",
+ "arrows_counterclockwise": "🔄",
+ "arrow_heading_down": "⤵",
+ "arrow_heading_up": "⤴",
+ "art": "🎨",
+ "astonished": "😲",
+ "athletic_shoe": "👟",
+ "atm": "🏧",
+ "car": "🚗",
+ "red_car": "🚗",
+ "angel": "👼",
+ "back": "🔙",
+ "badminton_racquet_and_shuttlecock": "🏸",
+ "dollar": "💵",
+ "euro": "💶",
+ "pound": "💷",
+ "yen": "💴",
+ "barber": "💈",
+ "bath": "🛀",
+ "bear": "🐻",
+ "heartbeat": "💓",
+ "beer": "🍺",
+ "no_bell": "🔕",
+ "bento": "🍱",
+ "bike": "🚲",
+ "bicyclist": "🚴",
+ "8ball": "🎱",
+ "biohazard_sign": "☣",
+ "birthday": "🎂",
+ "black_circle_for_record": "⏺",
+ "clubs": "♣",
+ "diamonds": "♦",
+ "arrow_double_down": "⏬",
+ "hearts": "♥",
+ "rewind": "⏪",
+ "black_left__pointing_double_triangle_with_vertical_bar": "⏮",
+ "arrow_backward": "◀",
+ "black_medium_small_square": "◾",
+ "question": "❓",
+ "fast_forward": "⏩",
+ "black_right__pointing_double_triangle_with_vertical_bar": "⏭",
+ "arrow_forward": "▶",
+ "black_right__pointing_triangle_with_double_vertical_bar": "⏯",
+ "arrow_right": "➡",
+ "spades": "♠",
+ "black_square_for_stop": "⏹",
+ "sunny": "☀",
+ "phone": "☎",
+ "recycle": "♻",
+ "arrow_double_up": "⏫",
+ "busstop": "🚏",
+ "date": "📅",
+ "flags": "🎏",
+ "cat2": "🐈",
+ "joy_cat": "😹",
+ "smirk_cat": "😼",
+ "chart_with_downwards_trend": "📉",
+ "chart_with_upwards_trend": "📈",
+ "chart": "💹",
+ "mega": "📣",
+ "checkered_flag": "🏁",
+ "accept": "🉑",
+ "ideograph_advantage": "🉐",
+ "congratulations": "㊗",
+ "secret": "㊙",
+ "m": "Ⓜ",
+ "city_sunset": "🌆",
+ "clapper": "🎬",
+ "clap": "👏",
+ "beers": "🍻",
+ "clock830": "🕣",
+ "clock8": "🕗",
+ "clock1130": "🕦",
+ "clock11": "🕚",
+ "clock530": "🕠",
+ "clock5": "🕔",
+ "clock430": "🕟",
+ "clock4": "🕓",
+ "clock930": "🕤",
+ "clock9": "🕘",
+ "clock130": "🕜",
+ "clock1": "🕐",
+ "clock730": "🕢",
+ "clock7": "🕖",
+ "clock630": "🕡",
+ "clock6": "🕕",
+ "clock1030": "🕥",
+ "clock10": "🕙",
+ "clock330": "🕞",
+ "clock3": "🕒",
+ "clock1230": "🕧",
+ "clock12": "🕛",
+ "clock230": "🕝",
+ "clock2": "🕑",
+ "arrows_clockwise": "🔃",
+ "repeat": "🔁",
+ "repeat_one": "🔂",
+ "closed_lock_with_key": "🔐",
+ "mailbox_closed": "📪",
+ "mailbox": "📫",
+ "cloud_with_tornado": "🌪",
+ "cocktail": "🍸",
+ "boom": "💥",
+ "compression": "🗜",
+ "confounded": "😖",
+ "confused": "😕",
+ "rice": "🍚",
+ "cow2": "🐄",
+ "cricket_bat_and_ball": "🏏",
+ "x": "❌",
+ "cry": "😢",
+ "curry": "🍛",
+ "dagger_knife": "🗡",
+ "dancer": "💃",
+ "dark_sunglasses": "🕶",
+ "dash": "💨",
+ "truck": "🚚",
+ "derelict_house_building": "🏚",
+ "diamond_shape_with_a_dot_inside": "💠",
+ "dart": "🎯",
+ "disappointed_relieved": "😥",
+ "disappointed": "😞",
+ "do_not_litter": "🚯",
+ "dog2": "🐕",
+ "flipper": "🐬",
+ "loop": "➿",
+ "bangbang": "‼",
+ "double_vertical_bar": "⏸",
+ "dove_of_peace": "🕊",
+ "small_red_triangle_down": "🔻",
+ "arrow_down_small": "🔽",
+ "arrow_down": "⬇",
+ "dromedary_camel": "🐪",
+ "e__mail": "📧",
+ "corn": "🌽",
+ "ear_of_rice": "🌾",
+ "earth_americas": "🌎",
+ "earth_asia": "🌏",
+ "earth_africa": "🌍",
+ "eight_pointed_black_star": "✴",
+ "eight_spoked_asterisk": "✳",
+ "eject_symbol": "⏏",
+ "bulb": "💡",
+ "emoji_modifier_fitzpatrick_type__1__2": "🏻",
+ "emoji_modifier_fitzpatrick_type__3": "🏼",
+ "emoji_modifier_fitzpatrick_type__4": "🏽",
+ "emoji_modifier_fitzpatrick_type__5": "🏾",
+ "emoji_modifier_fitzpatrick_type__6": "🏿",
+ "end": "🔚",
+ "email": "✉",
+ "european_castle": "🏰",
+ "european_post_office": "🏤",
+ "interrobang": "⁉",
+ "expressionless": "😑",
+ "eyeglasses": "👓",
+ "massage": "💆",
+ "yum": "😋",
+ "scream": "😱",
+ "kissing_heart": "😘",
+ "sweat": "😓",
+ "face_with_head__bandage": "🤕",
+ "triumph": "😤",
+ "mask": "😷",
+ "no_good": "🙅",
+ "ok_woman": "🙆",
+ "open_mouth": "😮",
+ "cold_sweat": "😰",
+ "stuck_out_tongue": "😛",
+ "stuck_out_tongue_closed_eyes": "😝",
+ "stuck_out_tongue_winking_eye": "😜",
+ "joy": "😂",
+ "no_mouth": "😶",
+ "santa": "🎅",
+ "fax": "📠",
+ "fearful": "😨",
+ "field_hockey_stick_and_ball": "🏑",
+ "first_quarter_moon_with_face": "🌛",
+ "fish_cake": "🍥",
+ "fishing_pole_and_fish": "🎣",
+ "facepunch": "👊",
+ "punch": "👊",
+ "flag_for_afghanistan": "🇦🇫",
+ "flag_for_albania": "🇦🇱",
+ "flag_for_algeria": "🇩🇿",
+ "flag_for_american_samoa": "🇦🇸",
+ "flag_for_andorra": "🇦🇩",
+ "flag_for_angola": "🇦🇴",
+ "flag_for_anguilla": "🇦🇮",
+ "flag_for_antarctica": "🇦🇶",
+ "flag_for_antigua_&_barbuda": "🇦🇬",
+ "flag_for_argentina": "🇦🇷",
+ "flag_for_armenia": "🇦🇲",
+ "flag_for_aruba": "🇦🇼",
+ "flag_for_ascension_island": "🇦🇨",
+ "flag_for_australia": "🇦🇺",
+ "flag_for_austria": "🇦🇹",
+ "flag_for_azerbaijan": "🇦🇿",
+ "flag_for_bahamas": "🇧🇸",
+ "flag_for_bahrain": "🇧🇭",
+ "flag_for_bangladesh": "🇧🇩",
+ "flag_for_barbados": "🇧🇧",
+ "flag_for_belarus": "🇧🇾",
+ "flag_for_belgium": "🇧🇪",
+ "flag_for_belize": "🇧🇿",
+ "flag_for_benin": "🇧🇯",
+ "flag_for_bermuda": "🇧🇲",
+ "flag_for_bhutan": "🇧🇹",
+ "flag_for_bolivia": "🇧🇴",
+ "flag_for_bosnia_&_herzegovina": "🇧🇦",
+ "flag_for_botswana": "🇧🇼",
+ "flag_for_bouvet_island": "🇧🇻",
+ "flag_for_brazil": "🇧🇷",
+ "flag_for_british_indian_ocean_territory": "🇮🇴",
+ "flag_for_british_virgin_islands": "🇻🇬",
+ "flag_for_brunei": "🇧🇳",
+ "flag_for_bulgaria": "🇧🇬",
+ "flag_for_burkina_faso": "🇧🇫",
+ "flag_for_burundi": "🇧🇮",
+ "flag_for_cambodia": "🇰🇭",
+ "flag_for_cameroon": "🇨🇲",
+ "flag_for_canada": "🇨🇦",
+ "flag_for_canary_islands": "🇮🇨",
+ "flag_for_cape_verde": "🇨🇻",
+ "flag_for_caribbean_netherlands": "🇧🇶",
+ "flag_for_cayman_islands": "🇰🇾",
+ "flag_for_central_african_republic": "🇨🇫",
+ "flag_for_ceuta_&_melilla": "🇪🇦",
+ "flag_for_chad": "🇹🇩",
+ "flag_for_chile": "🇨🇱",
+ "flag_for_china": "🇨🇳",
+ "flag_for_christmas_island": "🇨🇽",
+ "flag_for_clipperton_island": "🇨🇵",
+ "flag_for_cocos__islands": "🇨🇨",
+ "flag_for_colombia": "🇨🇴",
+ "flag_for_comoros": "🇰🇲",
+ "flag_for_congo____brazzaville": "🇨🇬",
+ "flag_for_congo____kinshasa": "🇨🇩",
+ "flag_for_cook_islands": "🇨🇰",
+ "flag_for_costa_rica": "🇨🇷",
+ "flag_for_croatia": "🇭🇷",
+ "flag_for_cuba": "🇨🇺",
+ "flag_for_curaçao": "🇨🇼",
+ "flag_for_cyprus": "🇨🇾",
+ "flag_for_czech_republic": "🇨🇿",
+ "flag_for_côte_d’ivoire": "🇨🇮",
+ "flag_for_denmark": "🇩🇰",
+ "flag_for_diego_garcia": "🇩🇬",
+ "flag_for_djibouti": "🇩🇯",
+ "flag_for_dominica": "🇩🇲",
+ "flag_for_dominican_republic": "🇩🇴",
+ "flag_for_ecuador": "🇪🇨",
+ "flag_for_egypt": "🇪🇬",
+ "flag_for_el_salvador": "🇸🇻",
+ "flag_for_equatorial_guinea": "🇬🇶",
+ "flag_for_eritrea": "🇪🇷",
+ "flag_for_estonia": "🇪🇪",
+ "flag_for_ethiopia": "🇪🇹",
+ "flag_for_european_union": "🇪🇺",
+ "flag_for_falkland_islands": "🇫🇰",
+ "flag_for_faroe_islands": "🇫🇴",
+ "flag_for_fiji": "🇫🇯",
+ "flag_for_finland": "🇫🇮",
+ "flag_for_france": "🇫🇷",
+ "flag_for_french_guiana": "🇬🇫",
+ "flag_for_french_polynesia": "🇵🇫",
+ "flag_for_french_southern_territories": "🇹🇫",
+ "flag_for_gabon": "🇬🇦",
+ "flag_for_gambia": "🇬🇲",
+ "flag_for_georgia": "🇬🇪",
+ "flag_for_germany": "🇩🇪",
+ "flag_for_ghana": "🇬🇭",
+ "flag_for_gibraltar": "🇬🇮",
+ "flag_for_greece": "🇬🇷",
+ "flag_for_greenland": "🇬🇱",
+ "flag_for_grenada": "🇬🇩",
+ "flag_for_guadeloupe": "🇬🇵",
+ "flag_for_guam": "🇬🇺",
+ "flag_for_guatemala": "🇬🇹",
+ "flag_for_guernsey": "🇬🇬",
+ "flag_for_guinea": "🇬🇳",
+ "flag_for_guinea__bissau": "🇬🇼",
+ "flag_for_guyana": "🇬🇾",
+ "flag_for_haiti": "🇭🇹",
+ "flag_for_heard_&_mcdonald_islands": "🇭🇲",
+ "flag_for_honduras": "🇭🇳",
+ "flag_for_hong_kong": "🇭🇰",
+ "flag_for_hungary": "🇭🇺",
+ "flag_for_iceland": "🇮🇸",
+ "flag_for_india": "🇮🇳",
+ "flag_for_indonesia": "🇮🇩",
+ "flag_for_iran": "🇮🇷",
+ "flag_for_iraq": "🇮🇶",
+ "flag_for_ireland": "🇮🇪",
+ "flag_for_isle_of_man": "🇮🇲",
+ "flag_for_israel": "🇮🇱",
+ "flag_for_italy": "🇮🇹",
+ "flag_for_jamaica": "🇯🇲",
+ "flag_for_japan": "🇯🇵",
+ "flag_for_jersey": "🇯🇪",
+ "flag_for_jordan": "🇯🇴",
+ "flag_for_kazakhstan": "🇰🇿",
+ "flag_for_kenya": "🇰🇪",
+ "flag_for_kiribati": "🇰🇮",
+ "flag_for_kosovo": "🇽🇰",
+ "flag_for_kuwait": "🇰🇼",
+ "flag_for_kyrgyzstan": "🇰🇬",
+ "flag_for_laos": "🇱🇦",
+ "flag_for_latvia": "🇱🇻",
+ "flag_for_lebanon": "🇱🇧",
+ "flag_for_lesotho": "🇱🇸",
+ "flag_for_liberia": "🇱🇷",
+ "flag_for_libya": "🇱🇾",
+ "flag_for_liechtenstein": "🇱🇮",
+ "flag_for_lithuania": "🇱🇹",
+ "flag_for_luxembourg": "🇱🇺",
+ "flag_for_macau": "🇲🇴",
+ "flag_for_macedonia": "🇲🇰",
+ "flag_for_madagascar": "🇲🇬",
+ "flag_for_malawi": "🇲🇼",
+ "flag_for_malaysia": "🇲🇾",
+ "flag_for_maldives": "🇲🇻",
+ "flag_for_mali": "🇲🇱",
+ "flag_for_malta": "🇲🇹",
+ "flag_for_marshall_islands": "🇲🇭",
+ "flag_for_martinique": "🇲🇶",
+ "flag_for_mauritania": "🇲🇷",
+ "flag_for_mauritius": "🇲🇺",
+ "flag_for_mayotte": "🇾🇹",
+ "flag_for_mexico": "🇲🇽",
+ "flag_for_micronesia": "🇫🇲",
+ "flag_for_moldova": "🇲🇩",
+ "flag_for_monaco": "🇲🇨",
+ "flag_for_mongolia": "🇲🇳",
+ "flag_for_montenegro": "🇲🇪",
+ "flag_for_montserrat": "🇲🇸",
+ "flag_for_morocco": "🇲🇦",
+ "flag_for_mozambique": "🇲🇿",
+ "flag_for_myanmar": "🇲🇲",
+ "flag_for_namibia": "🇳🇦",
+ "flag_for_nauru": "🇳🇷",
+ "flag_for_nepal": "🇳🇵",
+ "flag_for_netherlands": "🇳🇱",
+ "flag_for_new_caledonia": "🇳🇨",
+ "flag_for_new_zealand": "🇳🇿",
+ "flag_for_nicaragua": "🇳🇮",
+ "flag_for_niger": "🇳🇪",
+ "flag_for_nigeria": "🇳🇬",
+ "flag_for_niue": "🇳🇺",
+ "flag_for_norfolk_island": "🇳🇫",
+ "flag_for_north_korea": "🇰🇵",
+ "flag_for_northern_mariana_islands": "🇲🇵",
+ "flag_for_norway": "🇳🇴",
+ "flag_for_oman": "🇴🇲",
+ "flag_for_pakistan": "🇵🇰",
+ "flag_for_palau": "🇵🇼",
+ "flag_for_palestinian_territories": "🇵🇸",
+ "flag_for_panama": "🇵🇦",
+ "flag_for_papua_new_guinea": "🇵🇬",
+ "flag_for_paraguay": "🇵🇾",
+ "flag_for_peru": "🇵🇪",
+ "flag_for_philippines": "🇵🇭",
+ "flag_for_pitcairn_islands": "🇵🇳",
+ "flag_for_poland": "🇵🇱",
+ "flag_for_portugal": "🇵🇹",
+ "flag_for_puerto_rico": "🇵🇷",
+ "flag_for_qatar": "🇶🇦",
+ "flag_for_romania": "🇷🇴",
+ "flag_for_russia": "🇷🇺",
+ "flag_for_rwanda": "🇷🇼",
+ "flag_for_réunion": "🇷🇪",
+ "flag_for_samoa": "🇼🇸",
+ "flag_for_san_marino": "🇸🇲",
+ "flag_for_saudi_arabia": "🇸🇦",
+ "flag_for_senegal": "🇸🇳",
+ "flag_for_serbia": "🇷🇸",
+ "flag_for_seychelles": "🇸🇨",
+ "flag_for_sierra_leone": "🇸🇱",
+ "flag_for_singapore": "🇸🇬",
+ "flag_for_sint_maarten": "🇸🇽",
+ "flag_for_slovakia": "🇸🇰",
+ "flag_for_slovenia": "🇸🇮",
+ "flag_for_solomon_islands": "🇸🇧",
+ "flag_for_somalia": "🇸🇴",
+ "flag_for_south_africa": "🇿🇦",
+ "flag_for_south_georgia_&_south_sandwich_islands": "🇬🇸",
+ "flag_for_south_korea": "🇰🇷",
+ "flag_for_south_sudan": "🇸🇸",
+ "flag_for_spain": "🇪🇸",
+ "flag_for_sri_lanka": "🇱🇰",
+ "flag_for_st._barthélemy": "🇧🇱",
+ "flag_for_st._helena": "🇸🇭",
+ "flag_for_st._kitts_&_nevis": "🇰🇳",
+ "flag_for_st._lucia": "🇱🇨",
+ "flag_for_st._martin": "🇲🇫",
+ "flag_for_st._pierre_&_miquelon": "🇵🇲",
+ "flag_for_st._vincent_&_grenadines": "🇻🇨",
+ "flag_for_sudan": "🇸🇩",
+ "flag_for_suriname": "🇸🇷",
+ "flag_for_svalbard_&_jan_mayen": "🇸🇯",
+ "flag_for_swaziland": "🇸🇿",
+ "flag_for_sweden": "🇸🇪",
+ "flag_for_switzerland": "🇨🇭",
+ "flag_for_syria": "🇸🇾",
+ "flag_for_são_tomé_&_príncipe": "🇸🇹",
+ "flag_for_taiwan": "🇹🇼",
+ "flag_for_tajikistan": "🇹🇯",
+ "flag_for_tanzania": "🇹🇿",
+ "flag_for_thailand": "🇹🇭",
+ "flag_for_timor__leste": "🇹🇱",
+ "flag_for_togo": "🇹🇬",
+ "flag_for_tokelau": "🇹🇰",
+ "flag_for_tonga": "🇹🇴",
+ "flag_for_trinidad_&_tobago": "🇹🇹",
+ "flag_for_tristan_da_cunha": "🇹🇦",
+ "flag_for_tunisia": "🇹🇳",
+ "flag_for_turkey": "🇹🇷",
+ "flag_for_turkmenistan": "🇹🇲",
+ "flag_for_turks_&_caicos_islands": "🇹🇨",
+ "flag_for_tuvalu": "🇹🇻",
+ "flag_for_u.s._outlying_islands": "🇺🇲",
+ "flag_for_u.s._virgin_islands": "🇻🇮",
+ "flag_for_uganda": "🇺🇬",
+ "flag_for_ukraine": "🇺🇦",
+ "flag_for_united_arab_emirates": "🇦🇪",
+ "flag_for_united_kingdom": "🇬🇧",
+ "flag_for_united_states": "🇺🇸",
+ "flag_for_uruguay": "🇺🇾",
+ "flag_for_uzbekistan": "🇺🇿",
+ "flag_for_vanuatu": "🇻🇺",
+ "flag_for_vatican_city": "🇻🇦",
+ "flag_for_venezuela": "🇻🇪",
+ "flag_for_vietnam": "🇻🇳",
+ "flag_for_wallis_&_futuna": "🇼🇫",
+ "flag_for_western_sahara": "🇪🇭",
+ "flag_for_yemen": "🇾🇪",
+ "flag_for_zambia": "🇿🇲",
+ "flag_for_zimbabwe": "🇿🇼",
+ "flag_for_åland_islands": "🇦🇽",
+ "golf": "⛳",
+ "fleur__de__lis": "⚜",
+ "muscle": "💪",
+ "flushed": "😳",
+ "frame_with_picture": "🖼",
+ "fries": "🍟",
+ "frog": "🐸",
+ "hatched_chick": "🐥",
+ "frowning": "😦",
+ "fuelpump": "⛽",
+ "full_moon_with_face": "🌝",
+ "gem": "💎",
+ "star2": "🌟",
+ "golfer": "🏌",
+ "mortar_board": "🎓",
+ "grimacing": "😬",
+ "smile_cat": "😸",
+ "grinning": "😀",
+ "grin": "😁",
+ "heartpulse": "💗",
+ "guardsman": "💂",
+ "haircut": "💇",
+ "hamster": "🐹",
+ "raising_hand": "🙋",
+ "headphones": "🎧",
+ "hear_no_evil": "🙉",
+ "cupid": "💘",
+ "gift_heart": "💝",
+ "heart": "❤",
+ "exclamation": "❗",
+ "heavy_exclamation_mark": "❗",
+ "heavy_heart_exclamation_mark_ornament": "❣",
+ "o": "⭕",
+ "helm_symbol": "⎈",
+ "helmet_with_white_cross": "⛑",
+ "high_heel": "👠",
+ "bullettrain_side": "🚄",
+ "bullettrain_front": "🚅",
+ "high_brightness": "🔆",
+ "zap": "⚡",
+ "hocho": "🔪",
+ "knife": "🔪",
+ "bee": "🐝",
+ "traffic_light": "🚥",
+ "racehorse": "🐎",
+ "coffee": "☕",
+ "hotsprings": "♨",
+ "hourglass": "⌛",
+ "hourglass_flowing_sand": "⏳",
+ "house_buildings": "🏘",
+ "100": "💯",
+ "hushed": "😯",
+ "ice_hockey_stick_and_puck": "🏒",
+ "imp": "👿",
+ "information_desk_person": "💁",
+ "information_source": "ℹ",
+ "capital_abcd": "🔠",
+ "abc": "🔤",
+ "abcd": "🔡",
+ "1234": "🔢",
+ "symbols": "🔣",
+ "izakaya_lantern": "🏮",
+ "lantern": "🏮",
+ "jack_o_lantern": "🎃",
+ "dolls": "🎎",
+ "japanese_goblin": "👺",
+ "japanese_ogre": "👹",
+ "beginner": "🔰",
+ "zero": "0️⃣",
+ "one": "1️⃣",
+ "ten": "🔟",
+ "two": "2️⃣",
+ "three": "3️⃣",
+ "four": "4️⃣",
+ "five": "5️⃣",
+ "six": "6️⃣",
+ "seven": "7️⃣",
+ "eight": "8️⃣",
+ "nine": "9️⃣",
+ "couplekiss": "💏",
+ "kissing_cat": "😽",
+ "kissing": "😗",
+ "kissing_closed_eyes": "😚",
+ "kissing_smiling_eyes": "😙",
+ "beetle": "🐞",
+ "large_blue_circle": "🔵",
+ "last_quarter_moon_with_face": "🌜",
+ "leaves": "🍃",
+ "mag": "🔍",
+ "left_right_arrow": "↔",
+ "leftwards_arrow_with_hook": "↩",
+ "arrow_left": "⬅",
+ "lock": "🔒",
+ "lock_with_ink_pen": "🔏",
+ "sob": "😭",
+ "low_brightness": "🔅",
+ "lower_left_ballpoint_pen": "🖊",
+ "lower_left_crayon": "🖍",
+ "lower_left_fountain_pen": "🖋",
+ "lower_left_paintbrush": "🖌",
+ "mahjong": "🀄",
+ "couple": "👫",
+ "man_in_business_suit_levitating": "🕴",
+ "man_with_gua_pi_mao": "👲",
+ "man_with_turban": "👳",
+ "mans_shoe": "👞",
+ "shoe": "👞",
+ "menorah_with_nine_branches": "🕎",
+ "mens": "🚹",
+ "minidisc": "💽",
+ "iphone": "📱",
+ "calling": "📲",
+ "money__mouth_face": "🤑",
+ "moneybag": "💰",
+ "rice_scene": "🎑",
+ "mountain_bicyclist": "🚵",
+ "mouse2": "🐁",
+ "lips": "👄",
+ "moyai": "🗿",
+ "notes": "🎶",
+ "nail_care": "💅",
+ "ab": "🆎",
+ "negative_squared_cross_mark": "❎",
+ "a": "🅰",
+ "b": "🅱",
+ "o2": "🅾",
+ "parking": "🅿",
+ "new_moon_with_face": "🌚",
+ "no_entry_sign": "🚫",
+ "underage": "🔞",
+ "non__potable_water": "🚱",
+ "arrow_upper_right": "↗",
+ "arrow_upper_left": "↖",
+ "office": "🏢",
+ "older_man": "👴",
+ "older_woman": "👵",
+ "om_symbol": "🕉",
+ "on": "🔛",
+ "book": "📖",
+ "unlock": "🔓",
+ "mailbox_with_no_mail": "📭",
+ "mailbox_with_mail": "📬",
+ "cd": "💿",
+ "tada": "🎉",
+ "feet": "🐾",
+ "walking": "🚶",
+ "pencil2": "✏",
+ "pensive": "😔",
+ "persevere": "😣",
+ "bow": "🙇",
+ "raised_hands": "🙌",
+ "person_with_ball": "⛹",
+ "person_with_blond_hair": "👱",
+ "pray": "🙏",
+ "person_with_pouting_face": "🙎",
+ "computer": "💻",
+ "pig2": "🐖",
+ "hankey": "💩",
+ "poop": "💩",
+ "shit": "💩",
+ "bamboo": "🎍",
+ "gun": "🔫",
+ "black_joker": "🃏",
+ "rotating_light": "🚨",
+ "cop": "👮",
+ "stew": "🍲",
+ "pouch": "👝",
+ "pouting_cat": "😾",
+ "rage": "😡",
+ "put_litter_in_its_place": "🚮",
+ "rabbit2": "🐇",
+ "racing_motorcycle": "🏍",
+ "radioactive_sign": "☢",
+ "fist": "✊",
+ "hand": "✋",
+ "raised_hand_with_fingers_splayed": "🖐",
+ "raised_hand_with_part_between_middle_and_ring_fingers": "🖖",
+ "blue_car": "🚙",
+ "apple": "🍎",
+ "relieved": "😌",
+ "reversed_hand_with_middle_finger_extended": "🖕",
+ "mag_right": "🔎",
+ "arrow_right_hook": "↪",
+ "sweet_potato": "🍠",
+ "robot": "🤖",
+ "rolled__up_newspaper": "🗞",
+ "rowboat": "🚣",
+ "runner": "🏃",
+ "running": "🏃",
+ "running_shirt_with_sash": "🎽",
+ "boat": "⛵",
+ "scales": "⚖",
+ "school_satchel": "🎒",
+ "scorpius": "♏",
+ "see_no_evil": "🙈",
+ "sheep": "🐑",
+ "stars": "🌠",
+ "cake": "🍰",
+ "six_pointed_star": "🔯",
+ "ski": "🎿",
+ "sleeping_accommodation": "🛌",
+ "sleeping": "😴",
+ "sleepy": "😪",
+ "sleuth_or_spy": "🕵",
+ "heart_eyes_cat": "😻",
+ "smiley_cat": "😺",
+ "innocent": "😇",
+ "heart_eyes": "😍",
+ "smiling_imp": "😈",
+ "smiley": "😃",
+ "sweat_smile": "😅",
+ "smile": "😄",
+ "laughing": "😆",
+ "satisfied": "😆",
+ "blush": "😊",
+ "smirk": "😏",
+ "smoking": "🚬",
+ "snow_capped_mountain": "🏔",
+ "soccer": "⚽",
+ "icecream": "🍦",
+ "soon": "🔜",
+ "arrow_lower_right": "↘",
+ "arrow_lower_left": "↙",
+ "speak_no_evil": "🙊",
+ "speaker": "🔈",
+ "mute": "🔇",
+ "sound": "🔉",
+ "loud_sound": "🔊",
+ "speaking_head_in_silhouette": "🗣",
+ "spiral_calendar_pad": "🗓",
+ "spiral_note_pad": "🗒",
+ "shell": "🐚",
+ "sweat_drops": "💦",
+ "u5272": "🈹",
+ "u5408": "🈴",
+ "u55b6": "🈺",
+ "u6307": "🈯",
+ "u6708": "🈷",
+ "u6709": "🈶",
+ "u6e80": "🈵",
+ "u7121": "🈚",
+ "u7533": "🈸",
+ "u7981": "🈲",
+ "u7a7a": "🈳",
+ "cl": "🆑",
+ "cool": "🆒",
+ "free": "🆓",
+ "id": "🆔",
+ "koko": "🈁",
+ "sa": "🈂",
+ "new": "🆕",
+ "ng": "🆖",
+ "ok": "🆗",
+ "sos": "🆘",
+ "up": "🆙",
+ "vs": "🆚",
+ "steam_locomotive": "🚂",
+ "ramen": "🍜",
+ "partly_sunny": "⛅",
+ "city_sunrise": "🌇",
+ "surfer": "🏄",
+ "swimmer": "🏊",
+ "shirt": "👕",
+ "tshirt": "👕",
+ "table_tennis_paddle_and_ball": "🏓",
+ "tea": "🍵",
+ "tv": "📺",
+ "three_button_mouse": "🖱",
+ "+1": "👍",
+ "thumbsup": "👍",
+ "__1": "👎",
+ "-1": "👎",
+ "thumbsdown": "👎",
+ "thunder_cloud_and_rain": "⛈",
+ "tiger2": "🐅",
+ "tophat": "🎩",
+ "top": "🔝",
+ "tm": "™",
+ "train2": "🚆",
+ "triangular_flag_on_post": "🚩",
+ "trident": "🔱",
+ "twisted_rightwards_arrows": "🔀",
+ "unamused": "😒",
+ "small_red_triangle": "🔺",
+ "arrow_up_small": "🔼",
+ "arrow_up_down": "↕",
+ "upside__down_face": "🙃",
+ "arrow_up": "⬆",
+ "v": "✌",
+ "vhs": "📼",
+ "wc": "🚾",
+ "ocean": "🌊",
+ "waving_black_flag": "🏴",
+ "wave": "👋",
+ "waving_white_flag": "🏳",
+ "moon": "🌔",
+ "scream_cat": "🙀",
+ "weary": "😩",
+ "weight_lifter": "🏋",
+ "whale2": "🐋",
+ "wheelchair": "♿",
+ "point_down": "👇",
+ "grey_exclamation": "❕",
+ "white_frowning_face": "☹",
+ "white_check_mark": "✅",
+ "point_left": "👈",
+ "white_medium_small_square": "◽",
+ "star": "⭐",
+ "grey_question": "❔",
+ "point_right": "👉",
+ "relaxed": "☺",
+ "white_sun_behind_cloud": "🌥",
+ "white_sun_behind_cloud_with_rain": "🌦",
+ "white_sun_with_small_cloud": "🌤",
+ "point_up_2": "👆",
+ "point_up": "☝",
+ "wind_blowing_face": "🌬",
+ "wink": "😉",
+ "wolf": "🐺",
+ "dancers": "👯",
+ "boot": "👢",
+ "womans_clothes": "👚",
+ "womans_hat": "👒",
+ "sandal": "👡",
+ "womens": "🚺",
+ "worried": "😟",
+ "gift": "🎁",
+ "zipper__mouth_face": "🤐",
+ "regional_indicator_a": "🇦",
+ "regional_indicator_b": "🇧",
+ "regional_indicator_c": "🇨",
+ "regional_indicator_d": "🇩",
+ "regional_indicator_e": "🇪",
+ "regional_indicator_f": "🇫",
+ "regional_indicator_g": "🇬",
+ "regional_indicator_h": "🇭",
+ "regional_indicator_i": "🇮",
+ "regional_indicator_j": "🇯",
+ "regional_indicator_k": "🇰",
+ "regional_indicator_l": "🇱",
+ "regional_indicator_m": "🇲",
+ "regional_indicator_n": "🇳",
+ "regional_indicator_o": "🇴",
+ "regional_indicator_p": "🇵",
+ "regional_indicator_q": "🇶",
+ "regional_indicator_r": "🇷",
+ "regional_indicator_s": "🇸",
+ "regional_indicator_t": "🇹",
+ "regional_indicator_u": "🇺",
+ "regional_indicator_v": "🇻",
+ "regional_indicator_w": "🇼",
+ "regional_indicator_x": "🇽",
+ "regional_indicator_y": "🇾",
+ "regional_indicator_z": "🇿",
+}
diff --git a/rich/_emoji_replace.py b/rich/_emoji_replace.py
new file mode 100644
index 0000000..ee8cde0
--- /dev/null
+++ b/rich/_emoji_replace.py
@@ -0,0 +1,17 @@
+from typing import Match
+
+import re
+
+from ._emoji_codes import EMOJI
+
+
+def _emoji_replace(text: str, _emoji_sub=re.compile(r"(:(\S*?):)").sub) -> str:
+ """Replace emoji code in text."""
+ get_emoji = EMOJI.get
+
+ def do_replace(match: Match[str]) -> str:
+ """Called by re.sub to do the replacement."""
+ emoji_code, emoji_name = match.groups()
+ return get_emoji(emoji_name.lower(), emoji_code)
+
+ return _emoji_sub(do_replace, text)
diff --git a/rich/_inspect.py b/rich/_inspect.py
new file mode 100644
index 0000000..c1c9eec
--- /dev/null
+++ b/rich/_inspect.py
@@ -0,0 +1,251 @@
+from __future__ import absolute_import
+
+from inspect import cleandoc, getdoc, getfile, isclass, ismodule, signature
+from typing import Any, Iterable, Optional, Tuple
+
+from .console import RenderableType, RenderGroup
+from .highlighter import ReprHighlighter
+from .jupyter import JupyterMixin
+from .panel import Panel
+from .pretty import Pretty
+from .table import Table
+from .text import Text, TextType
+
+
+def _first_paragraph(doc: str) -> str:
+ """Get the first paragraph from a docstring."""
+ paragraph, _, _ = doc.partition("\n\n")
+ return paragraph
+
+
+def _reformat_doc(doc: str) -> str:
+ """Reformat docstring."""
+ doc = cleandoc(doc).strip()
+ return doc
+
+
+class Inspect(JupyterMixin):
+ """A renderable to inspect any Python Object.
+
+ Args:
+ obj (Any): An object to inspect.
+ title (str, optional): Title to display over inspect result, or None use type. Defaults to None.
+ help (bool, optional): Show full help text rather than just first paragraph. Defaults to False.
+ methods (bool, optional): Enable inspection of callables. Defaults to False.
+ docs (bool, optional): Also render doc strings. Defaults to True.
+ private (bool, optional): Show private attributes (beginning with underscore). Defaults to False.
+ dunder (bool, optional): Show attributes starting with double underscore. Defaults to False.
+ sort (bool, optional): Sort attributes alphabetically. Defaults to True.
+ all (bool, optional): Show all attributes. Defaults to False.
+ value (bool, optional): Pretty print value of object. Defaults to True.
+ """
+
+ def __init__(
+ self,
+ obj: Any,
+ *,
+ title: TextType = None,
+ help: bool = False,
+ methods: bool = False,
+ docs: bool = True,
+ private: bool = False,
+ dunder: bool = False,
+ sort: bool = True,
+ all: bool = True,
+ value: bool = True,
+ ) -> None:
+ self.highlighter = ReprHighlighter()
+ self.obj = obj
+ self.title = title or self._make_title(obj)
+ if all:
+ methods = private = dunder = True
+ self.help = help
+ self.methods = methods
+ self.docs = docs or help
+ self.private = private or dunder
+ self.dunder = dunder
+ self.sort = sort
+ self.value = value
+
+ def _make_title(self, obj: Any) -> Text:
+ """Make a default title."""
+ title_str = (
+ str(obj)
+ if (isclass(obj) or callable(obj) or ismodule(obj))
+ else str(type(obj))
+ )
+ title_text = self.highlighter(title_str)
+ return title_text
+
+ def __rich__(self) -> Panel:
+ return Panel.fit(
+ RenderGroup(*self._render()),
+ title=self.title,
+ border_style="scope.border",
+ padding=(0, 1),
+ )
+
+ def _get_signature(self, name: str, obj: Any) -> Optional[Text]:
+ """Get a signature for a callable."""
+ try:
+ _signature = str(signature(obj)) + ":"
+ except ValueError:
+ _signature = "(...)"
+ except TypeError:
+ return None
+
+ source_filename: Optional[str] = None
+ try:
+ source_filename = getfile(obj)
+ except TypeError:
+ pass
+
+ callable_name = Text(name, style="inspect.callable")
+ if source_filename:
+ callable_name.stylize(f"link file://{source_filename}")
+ signature_text = self.highlighter(_signature)
+
+ qualname = name or getattr(obj, "__qualname__", name)
+ qual_signature = Text.assemble(
+ ("def ", "inspect.def"), (qualname, "inspect.callable"), signature_text
+ )
+
+ return qual_signature
+
+ def _render(self) -> Iterable[RenderableType]:
+ """Render object."""
+
+ def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]:
+ key, (_error, value) = item
+ return (callable(value), key.strip("_").lower())
+
+ def safe_getattr(attr_name: str) -> Tuple[Any, Any]:
+ """Get attribute or any exception."""
+ try:
+ return (None, getattr(obj, attr_name))
+ except Exception as error:
+ return (error, None)
+
+ obj = self.obj
+ keys = dir(obj)
+ total_items = len(keys)
+ if not self.dunder:
+ keys = [key for key in keys if not key.startswith("__")]
+ if not self.private:
+ keys = [key for key in keys if not key.startswith("_")]
+ not_shown_count = total_items - len(keys)
+ items = [(key, safe_getattr(key)) for key in keys]
+ if self.sort:
+ items.sort(key=sort_items)
+
+ items_table = Table.grid(padding=(0, 1), expand=False)
+ items_table.add_column(justify="right")
+ add_row = items_table.add_row
+ highlighter = self.highlighter
+
+ if callable(obj):
+ signature = self._get_signature("", obj)
+ if signature is not None:
+ yield signature
+ yield ""
+
+ _doc = getdoc(obj)
+ if _doc is not None:
+ if not self.help:
+ _doc = _first_paragraph(_doc)
+ doc_text = Text(_reformat_doc(_doc), style="inspect.help")
+ doc_text = highlighter(doc_text)
+ yield doc_text
+ yield ""
+
+ if self.value and not (isclass(obj) or callable(obj) or ismodule(obj)):
+ yield Panel(
+ Pretty(obj, indent_guides=True, max_length=10, max_string=60),
+ border_style="inspect.value.border",
+ )
+ yield ""
+
+ for key, (error, value) in items:
+ key_text = Text.assemble(
+ (
+ key,
+ "inspect.attr.dunder" if key.startswith("__") else "inspect.attr",
+ ),
+ (" =", "inspect.equals"),
+ )
+ if error is not None:
+ warning = key_text.copy()
+ warning.stylize("inspect.error")
+ add_row(warning, highlighter(repr(error)))
+ continue
+
+ if callable(value):
+ if not self.methods:
+ continue
+
+ _signature_text = self._get_signature(key, value)
+ if _signature_text is None:
+ add_row(key_text, Pretty(value, highlighter=highlighter))
+ else:
+ if self.docs:
+ docs = getdoc(value)
+ if docs is not None:
+ _doc = _reformat_doc(str(docs))
+ if not self.help:
+ _doc = _first_paragraph(_doc)
+ _signature_text.append("\n" if "\n" in _doc else " ")
+ doc = highlighter(_doc)
+ doc.stylize("inspect.doc")
+ _signature_text.append(doc)
+
+ add_row(key_text, _signature_text)
+ else:
+ add_row(key_text, Pretty(value, highlighter=highlighter))
+ if items_table.row_count:
+ yield items_table
+ else:
+ yield self.highlighter(
+ Text.from_markup(
+ f"[i][b]{not_shown_count}[/b] attribute(s) not shown.[/i] Run [b][red]inspect[/red]([not b]inspect[/])[/b] for options."
+ )
+ )
+
+
+if __name__ == "__main__": # type: ignore
+ from rich import print
+
+ inspect = Inspect({}, docs=True, methods=True, dunder=True)
+ print(inspect)
+
+ t = Text("Hello, World")
+ print(Inspect(t))
+
+ from rich.style import Style
+ from rich.color import Color
+
+ print(Inspect(Style.parse("bold red on black"), methods=True, docs=True))
+ print(Inspect(Color.parse("#ffe326"), methods=True, docs=True))
+
+ from rich import get_console
+
+ print(Inspect(get_console(), methods=False))
+
+ print(Inspect(open("foo.txt", "wt"), methods=False))
+
+ print(Inspect("Hello", methods=False, dunder=True))
+ print(Inspect(inspect, methods=False, dunder=False, docs=False))
+
+ class Foo:
+ @property
+ def broken(self):
+ 1 / 0
+
+ f = Foo()
+ print(Inspect(f))
+
+ print(Inspect(object, dunder=True))
+
+ print(Inspect(None, dunder=False))
+
+ print(Inspect(str, help=True))
+ print(Inspect(1, help=False))
diff --git a/rich/_log_render.py b/rich/_log_render.py
new file mode 100644
index 0000000..2caad0b
--- /dev/null
+++ b/rich/_log_render.py
@@ -0,0 +1,88 @@
+from datetime import datetime
+from typing import Iterable, List, Optional, TYPE_CHECKING, Union, Callable
+
+
+from .text import Text, TextType
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleRenderable, RenderableType
+ from .table import Table
+
+FormatTimeCallable = Callable[[datetime], Text]
+
+
+class LogRender:
+ def __init__(
+ self,
+ show_time: bool = True,
+ show_level: bool = False,
+ show_path: bool = True,
+ time_format: Union[str, FormatTimeCallable] = "[%x %X]",
+ level_width: Optional[int] = 8,
+ ) -> None:
+ self.show_time = show_time
+ self.show_level = show_level
+ self.show_path = show_path
+ self.time_format = time_format
+ self.level_width = level_width
+ self._last_time: Optional[Text] = None
+
+ def __call__(
+ self,
+ console: "Console",
+ renderables: Iterable["ConsoleRenderable"],
+ log_time: datetime = None,
+ time_format: Union[str, FormatTimeCallable] = None,
+ level: TextType = "",
+ path: str = None,
+ line_no: int = None,
+ link_path: str = None,
+ ) -> "Table":
+ from .containers import Renderables
+ from .table import Table
+
+ output = Table.grid(padding=(0, 1))
+ output.expand = True
+ if self.show_time:
+ output.add_column(style="log.time")
+ if self.show_level:
+ output.add_column(style="log.level", width=self.level_width)
+ output.add_column(ratio=1, style="log.message", overflow="fold")
+ if self.show_path and path:
+ output.add_column(style="log.path")
+ row: List["RenderableType"] = []
+ if self.show_time:
+ log_time = log_time or console.get_datetime()
+ time_format = time_format or self.time_format
+ if callable(time_format):
+ log_time_display = time_format(log_time)
+ else:
+ log_time_display = Text(log_time.strftime(time_format))
+ if log_time_display == self._last_time:
+ row.append(Text(" " * len(log_time_display)))
+ else:
+ row.append(log_time_display)
+ self._last_time = log_time_display
+ if self.show_level:
+ row.append(level)
+
+ row.append(Renderables(renderables))
+ if self.show_path and path:
+ path_text = Text()
+ path_text.append(
+ path, style=f"link file://{link_path}" if link_path else ""
+ )
+ if line_no:
+ path_text.append(f":{line_no}")
+ row.append(path_text)
+
+ output.add_row(*row)
+ return output
+
+
+if __name__ == "__main__": # pragma: no cover
+ from rich.console import Console
+
+ c = Console()
+ c.print("[on blue]Hello", justify="right")
+ c.log("[on blue]hello", justify="right")
diff --git a/rich/_loop.py b/rich/_loop.py
new file mode 100644
index 0000000..01c6caf
--- /dev/null
+++ b/rich/_loop.py
@@ -0,0 +1,43 @@
+from typing import Iterable, Tuple, TypeVar
+
+T = TypeVar("T")
+
+
+def loop_first(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for first value."""
+ iter_values = iter(values)
+ try:
+ value = next(iter_values)
+ except StopIteration:
+ return
+ yield True, value
+ for value in iter_values:
+ yield False, value
+
+
+def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ for value in iter_values:
+ yield False, previous_value
+ previous_value = value
+ yield True, previous_value
+
+
+def loop_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]:
+ """Iterate and generate a tuple with a flag for first and last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ first = True
+ for value in iter_values:
+ yield first, False, previous_value
+ first = False
+ previous_value = value
+ yield first, True, previous_value
diff --git a/rich/_lru_cache.py b/rich/_lru_cache.py
new file mode 100644
index 0000000..b77c337
--- /dev/null
+++ b/rich/_lru_cache.py
@@ -0,0 +1,34 @@
+from collections import OrderedDict
+from typing import Dict, 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: Dict[CacheKey, CacheValue], 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
diff --git a/rich/_palettes.py b/rich/_palettes.py
new file mode 100644
index 0000000..3c748d3
--- /dev/null
+++ b/rich/_palettes.py
@@ -0,0 +1,309 @@
+from .palette import Palette
+
+
+# Taken from https://en.wikipedia.org/wiki/ANSI_escape_code (Windows 10 column)
+WINDOWS_PALETTE = Palette(
+ [
+ (12, 12, 12),
+ (197, 15, 31),
+ (19, 161, 14),
+ (193, 156, 0),
+ (0, 55, 218),
+ (136, 23, 152),
+ (58, 150, 221),
+ (204, 204, 204),
+ (118, 118, 118),
+ (231, 72, 86),
+ (22, 198, 12),
+ (249, 241, 165),
+ (59, 120, 255),
+ (180, 0, 158),
+ (97, 214, 214),
+ (242, 242, 242),
+ ]
+)
+
+# # The standard ansi colors (including bright variants)
+STANDARD_PALETTE = Palette(
+ [
+ (0, 0, 0),
+ (170, 0, 0),
+ (0, 170, 0),
+ (170, 85, 0),
+ (0, 0, 170),
+ (170, 0, 170),
+ (0, 170, 170),
+ (170, 170, 170),
+ (85, 85, 85),
+ (255, 85, 85),
+ (85, 255, 85),
+ (255, 255, 85),
+ (85, 85, 255),
+ (255, 85, 255),
+ (85, 255, 255),
+ (255, 255, 255),
+ ]
+)
+
+
+# The 256 color palette
+EIGHT_BIT_PALETTE = Palette(
+ [
+ (0, 0, 0),
+ (128, 0, 0),
+ (0, 128, 0),
+ (128, 128, 0),
+ (0, 0, 128),
+ (128, 0, 128),
+ (0, 128, 128),
+ (192, 192, 192),
+ (128, 128, 128),
+ (255, 0, 0),
+ (0, 255, 0),
+ (255, 255, 0),
+ (0, 0, 255),
+ (255, 0, 255),
+ (0, 255, 255),
+ (255, 255, 255),
+ (0, 0, 0),
+ (0, 0, 95),
+ (0, 0, 135),
+ (0, 0, 175),
+ (0, 0, 215),
+ (0, 0, 255),
+ (0, 95, 0),
+ (0, 95, 95),
+ (0, 95, 135),
+ (0, 95, 175),
+ (0, 95, 215),
+ (0, 95, 255),
+ (0, 135, 0),
+ (0, 135, 95),
+ (0, 135, 135),
+ (0, 135, 175),
+ (0, 135, 215),
+ (0, 135, 255),
+ (0, 175, 0),
+ (0, 175, 95),
+ (0, 175, 135),
+ (0, 175, 175),
+ (0, 175, 215),
+ (0, 175, 255),
+ (0, 215, 0),
+ (0, 215, 95),
+ (0, 215, 135),
+ (0, 215, 175),
+ (0, 215, 215),
+ (0, 215, 255),
+ (0, 255, 0),
+ (0, 255, 95),
+ (0, 255, 135),
+ (0, 255, 175),
+ (0, 255, 215),
+ (0, 255, 255),
+ (95, 0, 0),
+ (95, 0, 95),
+ (95, 0, 135),
+ (95, 0, 175),
+ (95, 0, 215),
+ (95, 0, 255),
+ (95, 95, 0),
+ (95, 95, 95),
+ (95, 95, 135),
+ (95, 95, 175),
+ (95, 95, 215),
+ (95, 95, 255),
+ (95, 135, 0),
+ (95, 135, 95),
+ (95, 135, 135),
+ (95, 135, 175),
+ (95, 135, 215),
+ (95, 135, 255),
+ (95, 175, 0),
+ (95, 175, 95),
+ (95, 175, 135),
+ (95, 175, 175),
+ (95, 175, 215),
+ (95, 175, 255),
+ (95, 215, 0),
+ (95, 215, 95),
+ (95, 215, 135),
+ (95, 215, 175),
+ (95, 215, 215),
+ (95, 215, 255),
+ (95, 255, 0),
+ (95, 255, 95),
+ (95, 255, 135),
+ (95, 255, 175),
+ (95, 255, 215),
+ (95, 255, 255),
+ (135, 0, 0),
+ (135, 0, 95),
+ (135, 0, 135),
+ (135, 0, 175),
+ (135, 0, 215),
+ (135, 0, 255),
+ (135, 95, 0),
+ (135, 95, 95),
+ (135, 95, 135),
+ (135, 95, 175),
+ (135, 95, 215),
+ (135, 95, 255),
+ (135, 135, 0),
+ (135, 135, 95),
+ (135, 135, 135),
+ (135, 135, 175),
+ (135, 135, 215),
+ (135, 135, 255),
+ (135, 175, 0),
+ (135, 175, 95),
+ (135, 175, 135),
+ (135, 175, 175),
+ (135, 175, 215),
+ (135, 175, 255),
+ (135, 215, 0),
+ (135, 215, 95),
+ (135, 215, 135),
+ (135, 215, 175),
+ (135, 215, 215),
+ (135, 215, 255),
+ (135, 255, 0),
+ (135, 255, 95),
+ (135, 255, 135),
+ (135, 255, 175),
+ (135, 255, 215),
+ (135, 255, 255),
+ (175, 0, 0),
+ (175, 0, 95),
+ (175, 0, 135),
+ (175, 0, 175),
+ (175, 0, 215),
+ (175, 0, 255),
+ (175, 95, 0),
+ (175, 95, 95),
+ (175, 95, 135),
+ (175, 95, 175),
+ (175, 95, 215),
+ (175, 95, 255),
+ (175, 135, 0),
+ (175, 135, 95),
+ (175, 135, 135),
+ (175, 135, 175),
+ (175, 135, 215),
+ (175, 135, 255),
+ (175, 175, 0),
+ (175, 175, 95),
+ (175, 175, 135),
+ (175, 175, 175),
+ (175, 175, 215),
+ (175, 175, 255),
+ (175, 215, 0),
+ (175, 215, 95),
+ (175, 215, 135),
+ (175, 215, 175),
+ (175, 215, 215),
+ (175, 215, 255),
+ (175, 255, 0),
+ (175, 255, 95),
+ (175, 255, 135),
+ (175, 255, 175),
+ (175, 255, 215),
+ (175, 255, 255),
+ (215, 0, 0),
+ (215, 0, 95),
+ (215, 0, 135),
+ (215, 0, 175),
+ (215, 0, 215),
+ (215, 0, 255),
+ (215, 95, 0),
+ (215, 95, 95),
+ (215, 95, 135),
+ (215, 95, 175),
+ (215, 95, 215),
+ (215, 95, 255),
+ (215, 135, 0),
+ (215, 135, 95),
+ (215, 135, 135),
+ (215, 135, 175),
+ (215, 135, 215),
+ (215, 135, 255),
+ (215, 175, 0),
+ (215, 175, 95),
+ (215, 175, 135),
+ (215, 175, 175),
+ (215, 175, 215),
+ (215, 175, 255),
+ (215, 215, 0),
+ (215, 215, 95),
+ (215, 215, 135),
+ (215, 215, 175),
+ (215, 215, 215),
+ (215, 215, 255),
+ (215, 255, 0),
+ (215, 255, 95),
+ (215, 255, 135),
+ (215, 255, 175),
+ (215, 255, 215),
+ (215, 255, 255),
+ (255, 0, 0),
+ (255, 0, 95),
+ (255, 0, 135),
+ (255, 0, 175),
+ (255, 0, 215),
+ (255, 0, 255),
+ (255, 95, 0),
+ (255, 95, 95),
+ (255, 95, 135),
+ (255, 95, 175),
+ (255, 95, 215),
+ (255, 95, 255),
+ (255, 135, 0),
+ (255, 135, 95),
+ (255, 135, 135),
+ (255, 135, 175),
+ (255, 135, 215),
+ (255, 135, 255),
+ (255, 175, 0),
+ (255, 175, 95),
+ (255, 175, 135),
+ (255, 175, 175),
+ (255, 175, 215),
+ (255, 175, 255),
+ (255, 215, 0),
+ (255, 215, 95),
+ (255, 215, 135),
+ (255, 215, 175),
+ (255, 215, 215),
+ (255, 215, 255),
+ (255, 255, 0),
+ (255, 255, 95),
+ (255, 255, 135),
+ (255, 255, 175),
+ (255, 255, 215),
+ (255, 255, 255),
+ (8, 8, 8),
+ (18, 18, 18),
+ (28, 28, 28),
+ (38, 38, 38),
+ (48, 48, 48),
+ (58, 58, 58),
+ (68, 68, 68),
+ (78, 78, 78),
+ (88, 88, 88),
+ (98, 98, 98),
+ (108, 108, 108),
+ (118, 118, 118),
+ (128, 128, 128),
+ (138, 138, 138),
+ (148, 148, 148),
+ (158, 158, 158),
+ (168, 168, 168),
+ (178, 178, 178),
+ (188, 188, 188),
+ (198, 198, 198),
+ (208, 208, 208),
+ (218, 218, 218),
+ (228, 228, 228),
+ (238, 238, 238),
+ ]
+)
diff --git a/rich/_pick.py b/rich/_pick.py
new file mode 100644
index 0000000..4f6d8b2
--- /dev/null
+++ b/rich/_pick.py
@@ -0,0 +1,17 @@
+from typing import Optional
+
+
+def pick_bool(*values: Optional[bool]) -> bool:
+ """Pick the first non-none bool or return the last value.
+
+ Args:
+ *values (bool): Any number of boolean or None values.
+
+ Returns:
+ bool: First non-none boolean.
+ """
+ assert values, "1 or more values required"
+ for value in values:
+ if value is not None:
+ return value
+ return bool(value)
diff --git a/rich/_ratio.py b/rich/_ratio.py
new file mode 100644
index 0000000..f293aba
--- /dev/null
+++ b/rich/_ratio.py
@@ -0,0 +1,134 @@
+from math import ceil, modf
+from typing import cast, List, Optional, Sequence
+from typing_extensions import Protocol
+
+
+class Edge(Protocol):
+ """Any object that defines an edge (such as Layout)."""
+
+ size: Optional[int] = None
+ ratio: int = 1
+ minimum_size: int = 1
+
+
+def ratio_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
+ """Divide total space to satisfy size, ratio, and minimum_size, constraints.
+
+ The returned list of integers should add up to total in most cases, unless it is
+ impossible to satisfy all the constraints. For instance, if there are two edges
+ with a minimum size of 20 each and `total` is 30 then the returned list will be
+ greater than total. In practice, this would mean that a Layout object would
+ clip the rows that would overflow the screen height.
+
+ Args:
+ total (int): Total number of characters.
+ edges (List[Edge]): Edges within total space.
+
+ Returns:
+ List[int]: Number of characters for each edge.
+ """
+ # Size of edge or None for yet to be determined
+ sizes = [(edge.size or None) for edge in edges]
+
+ # While any edges haven't been calculated
+ while None in sizes:
+ # Get flexible edges and index to map these back on to sizes list
+ flexible_edges = [
+ (index, edge)
+ for index, (size, edge) in enumerate(zip(sizes, edges))
+ if size is None
+ ]
+ # Remaining space in total
+ remaining = total - sum(size or 0 for size in sizes)
+ if remaining <= 0:
+ # No room for flexible edges
+ return [(size or 1) for size in sizes]
+ # Calculate number of characters in a ratio portion
+ portion = remaining / sum((edge.ratio or 1) for _, edge in flexible_edges)
+
+ # If any edges will be less than their minimum, replace size with the minimum
+ for index, edge in flexible_edges:
+ if portion * edge.ratio <= edge.minimum_size:
+ sizes[index] = edge.minimum_size
+ # New fixed size will invalidate calculations, so we need to repeat the process
+ break
+ else:
+ # Distribute flexible space and compensate for rounding error
+ # Since edge sizes can only be integers we need to add the remainder
+ # to the following line
+ _modf = modf
+ remainder = 0.0
+ for index, edge in flexible_edges:
+ remainder, size = _modf(portion * edge.ratio + remainder)
+ sizes[index] = int(size)
+ break
+ # Sizes now contains integers only
+ return cast(List[int], sizes)
+
+
+def ratio_reduce(
+ total: int, ratios: List[int], maximums: List[int], values: List[int]
+) -> List[int]:
+ """Divide an integer total in to parts based on ratios.
+
+ Args:
+ total (int): The total to divide.
+ ratios (List[int]): A list of integer ratios.
+ maximums (List[int]): List of maximums values for each slot.
+ values (List[int]): List of values
+
+ Returns:
+ List[int]: A list of integers guaranteed to sum to total.
+ """
+ ratios = [ratio if _max else 0 for ratio, _max in zip(ratios, maximums)]
+ total_ratio = sum(ratios)
+ if not total_ratio:
+ return values[:]
+ total_remaining = total
+ result: List[int] = []
+ append = result.append
+ for ratio, maximum, value in zip(ratios, maximums, values):
+ if ratio and total_ratio > 0:
+ distributed = min(maximum, round(ratio * total_remaining / total_ratio))
+ append(value - distributed)
+ total_remaining -= distributed
+ total_ratio -= ratio
+ else:
+ append(value)
+ return result
+
+
+def ratio_distribute(
+ total: int, ratios: List[int], minimums: List[int] = None
+) -> List[int]:
+ """Distribute an integer total in to parts based on ratios.
+
+ Args:
+ total (int): The total to divide.
+ ratios (List[int]): A list of integer ratios.
+ minimums (List[int]): List of minimum values for each slot.
+
+ Returns:
+ List[int]: A list of integers guaranteed to sum to total.
+ """
+ if minimums:
+ ratios = [ratio if _min else 0 for ratio, _min in zip(ratios, minimums)]
+ total_ratio = sum(ratios)
+ assert total_ratio > 0, "Sum of ratios must be > 0"
+
+ total_remaining = total
+ distributed_total: List[int] = []
+ append = distributed_total.append
+ if minimums is None:
+ _minimums = [0] * len(ratios)
+ else:
+ _minimums = minimums
+ for ratio, minimum in zip(ratios, _minimums):
+ if total_ratio > 0:
+ distributed = max(minimum, ceil(ratio * total_remaining / total_ratio))
+ else:
+ distributed = total_remaining
+ append(distributed)
+ total_ratio -= ratio
+ total_remaining -= distributed
+ return distributed_total
diff --git a/rich/_spinners.py b/rich/_spinners.py
new file mode 100644
index 0000000..dc1db07
--- /dev/null
+++ b/rich/_spinners.py
@@ -0,0 +1,848 @@
+"""
+Spinners are from:
+* cli-spinners:
+ MIT License
+ Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights to
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ the Software, and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+"""
+
+SPINNERS = {
+ "dots": {
+ "interval": 80,
+ "frames": ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
+ },
+ "dots2": {"interval": 80, "frames": ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]},
+ "dots3": {
+ "interval": 80,
+ "frames": ["⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"],
+ },
+ "dots4": {
+ "interval": 80,
+ "frames": [
+ "⠄",
+ "⠆",
+ "⠇",
+ "⠋",
+ "⠙",
+ "⠸",
+ "⠰",
+ "⠠",
+ "⠰",
+ "⠸",
+ "⠙",
+ "⠋",
+ "⠇",
+ "⠆",
+ ],
+ },
+ "dots5": {
+ "interval": 80,
+ "frames": [
+ "⠋",
+ "⠙",
+ "⠚",
+ "⠒",
+ "⠂",
+ "⠂",
+ "⠒",
+ "⠲",
+ "⠴",
+ "⠦",
+ "⠖",
+ "⠒",
+ "⠐",
+ "⠐",
+ "⠒",
+ "⠓",
+ "⠋",
+ ],
+ },
+ "dots6": {
+ "interval": 80,
+ "frames": [
+ "⠁",
+ "⠉",
+ "⠙",
+ "⠚",
+ "⠒",
+ "⠂",
+ "⠂",
+ "⠒",
+ "⠲",
+ "⠴",
+ "⠤",
+ "⠄",
+ "⠄",
+ "⠤",
+ "⠴",
+ "⠲",
+ "⠒",
+ "⠂",
+ "⠂",
+ "⠒",
+ "⠚",
+ "⠙",
+ "⠉",
+ "⠁",
+ ],
+ },
+ "dots7": {
+ "interval": 80,
+ "frames": [
+ "⠈",
+ "⠉",
+ "⠋",
+ "⠓",
+ "⠒",
+ "⠐",
+ "⠐",
+ "⠒",
+ "⠖",
+ "⠦",
+ "⠤",
+ "⠠",
+ "⠠",
+ "⠤",
+ "⠦",
+ "⠖",
+ "⠒",
+ "⠐",
+ "⠐",
+ "⠒",
+ "⠓",
+ "⠋",
+ "⠉",
+ "⠈",
+ ],
+ },
+ "dots8": {
+ "interval": 80,
+ "frames": [
+ "⠁",
+ "⠁",
+ "⠉",
+ "⠙",
+ "⠚",
+ "⠒",
+ "⠂",
+ "⠂",
+ "⠒",
+ "⠲",
+ "⠴",
+ "⠤",
+ "⠄",
+ "⠄",
+ "⠤",
+ "⠠",
+ "⠠",
+ "⠤",
+ "⠦",
+ "⠖",
+ "⠒",
+ "⠐",
+ "⠐",
+ "⠒",
+ "⠓",
+ "⠋",
+ "⠉",
+ "⠈",
+ "⠈",
+ ],
+ },
+ "dots9": {"interval": 80, "frames": ["⢹", "⢺", "⢼", "⣸", "⣇", "⡧", "⡗", "⡏"]},
+ "dots10": {"interval": 80, "frames": ["⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"]},
+ "dots11": {"interval": 100, "frames": ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]},
+ "dots12": {
+ "interval": 80,
+ "frames": [
+ "⢀⠀",
+ "⡀⠀",
+ "⠄⠀",
+ "⢂⠀",
+ "⡂⠀",
+ "⠅⠀",
+ "⢃⠀",
+ "⡃⠀",
+ "⠍⠀",
+ "⢋⠀",
+ "⡋⠀",
+ "⠍⠁",
+ "⢋⠁",
+ "⡋⠁",
+ "⠍⠉",
+ "⠋⠉",
+ "⠋⠉",
+ "⠉⠙",
+ "⠉⠙",
+ "⠉⠩",
+ "⠈⢙",
+ "⠈⡙",
+ "⢈⠩",
+ "⡀⢙",
+ "⠄⡙",
+ "⢂⠩",
+ "⡂⢘",
+ "⠅⡘",
+ "⢃⠨",
+ "⡃⢐",
+ "⠍⡐",
+ "⢋⠠",
+ "⡋⢀",
+ "⠍⡁",
+ "⢋⠁",
+ "⡋⠁",
+ "⠍⠉",
+ "⠋⠉",
+ "⠋⠉",
+ "⠉⠙",
+ "⠉⠙",
+ "⠉⠩",
+ "⠈⢙",
+ "⠈⡙",
+ "⠈⠩",
+ "⠀⢙",
+ "⠀⡙",
+ "⠀⠩",
+ "⠀⢘",
+ "⠀⡘",
+ "⠀⠨",
+ "⠀⢐",
+ "⠀⡐",
+ "⠀⠠",
+ "⠀⢀",
+ "⠀⡀",
+ ],
+ },
+ "dots8Bit": {
+ "interval": 80,
+ "frames": [
+ "⠀",
+ "⠁",
+ "⠂",
+ "⠃",
+ "⠄",
+ "⠅",
+ "⠆",
+ "⠇",
+ "⡀",
+ "⡁",
+ "⡂",
+ "⡃",
+ "⡄",
+ "⡅",
+ "⡆",
+ "⡇",
+ "⠈",
+ "⠉",
+ "⠊",
+ "⠋",
+ "⠌",
+ "⠍",
+ "⠎",
+ "⠏",
+ "⡈",
+ "⡉",
+ "⡊",
+ "⡋",
+ "⡌",
+ "⡍",
+ "⡎",
+ "⡏",
+ "⠐",
+ "⠑",
+ "⠒",
+ "⠓",
+ "⠔",
+ "⠕",
+ "⠖",
+ "⠗",
+ "⡐",
+ "⡑",
+ "⡒",
+ "⡓",
+ "⡔",
+ "⡕",
+ "⡖",
+ "⡗",
+ "⠘",
+ "⠙",
+ "⠚",
+ "⠛",
+ "⠜",
+ "⠝",
+ "⠞",
+ "⠟",
+ "⡘",
+ "⡙",
+ "⡚",
+ "⡛",
+ "⡜",
+ "⡝",
+ "⡞",
+ "⡟",
+ "⠠",
+ "⠡",
+ "⠢",
+ "⠣",
+ "⠤",
+ "⠥",
+ "⠦",
+ "⠧",
+ "⡠",
+ "⡡",
+ "⡢",
+ "⡣",
+ "⡤",
+ "⡥",
+ "⡦",
+ "⡧",
+ "⠨",
+ "⠩",
+ "⠪",
+ "⠫",
+ "⠬",
+ "⠭",
+ "⠮",
+ "⠯",
+ "⡨",
+ "⡩",
+ "⡪",
+ "⡫",
+ "⡬",
+ "⡭",
+ "⡮",
+ "⡯",
+ "⠰",
+ "⠱",
+ "⠲",
+ "⠳",
+ "⠴",
+ "⠵",
+ "⠶",
+ "⠷",
+ "⡰",
+ "⡱",
+ "⡲",
+ "⡳",
+ "⡴",
+ "⡵",
+ "⡶",
+ "⡷",
+ "⠸",
+ "⠹",
+ "⠺",
+ "⠻",
+ "⠼",
+ "⠽",
+ "⠾",
+ "⠿",
+ "⡸",
+ "⡹",
+ "⡺",
+ "⡻",
+ "⡼",
+ "⡽",
+ "⡾",
+ "⡿",
+ "⢀",
+ "⢁",
+ "⢂",
+ "⢃",
+ "⢄",
+ "⢅",
+ "⢆",
+ "⢇",
+ "⣀",
+ "⣁",
+ "⣂",
+ "⣃",
+ "⣄",
+ "⣅",
+ "⣆",
+ "⣇",
+ "⢈",
+ "⢉",
+ "⢊",
+ "⢋",
+ "⢌",
+ "⢍",
+ "⢎",
+ "⢏",
+ "⣈",
+ "⣉",
+ "⣊",
+ "⣋",
+ "⣌",
+ "⣍",
+ "⣎",
+ "⣏",
+ "⢐",
+ "⢑",
+ "⢒",
+ "⢓",
+ "⢔",
+ "⢕",
+ "⢖",
+ "⢗",
+ "⣐",
+ "⣑",
+ "⣒",
+ "⣓",
+ "⣔",
+ "⣕",
+ "⣖",
+ "⣗",
+ "⢘",
+ "⢙",
+ "⢚",
+ "⢛",
+ "⢜",
+ "⢝",
+ "⢞",
+ "⢟",
+ "⣘",
+ "⣙",
+ "⣚",
+ "⣛",
+ "⣜",
+ "⣝",
+ "⣞",
+ "⣟",
+ "⢠",
+ "⢡",
+ "⢢",
+ "⢣",
+ "⢤",
+ "⢥",
+ "⢦",
+ "⢧",
+ "⣠",
+ "⣡",
+ "⣢",
+ "⣣",
+ "⣤",
+ "⣥",
+ "⣦",
+ "⣧",
+ "⢨",
+ "⢩",
+ "⢪",
+ "⢫",
+ "⢬",
+ "⢭",
+ "⢮",
+ "⢯",
+ "⣨",
+ "⣩",
+ "⣪",
+ "⣫",
+ "⣬",
+ "⣭",
+ "⣮",
+ "⣯",
+ "⢰",
+ "⢱",
+ "⢲",
+ "⢳",
+ "⢴",
+ "⢵",
+ "⢶",
+ "⢷",
+ "⣰",
+ "⣱",
+ "⣲",
+ "⣳",
+ "⣴",
+ "⣵",
+ "⣶",
+ "⣷",
+ "⢸",
+ "⢹",
+ "⢺",
+ "⢻",
+ "⢼",
+ "⢽",
+ "⢾",
+ "⢿",
+ "⣸",
+ "⣹",
+ "⣺",
+ "⣻",
+ "⣼",
+ "⣽",
+ "⣾",
+ "⣿",
+ ],
+ },
+ "line": {"interval": 130, "frames": ["-", "\\", "|", "/"]},
+ "line2": {"interval": 100, "frames": ["⠂", "-", "–", "—", "–", "-"]},
+ "pipe": {"interval": 100, "frames": ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"]},
+ "simpleDots": {"interval": 400, "frames": [". ", ".. ", "...", " "]},
+ "simpleDotsScrolling": {
+ "interval": 200,
+ "frames": [". ", ".. ", "...", " ..", " .", " "],
+ },
+ "star": {"interval": 70, "frames": ["✶", "✸", "✹", "✺", "✹", "✷"]},
+ "star2": {"interval": 80, "frames": ["+", "x", "*"]},
+ "flip": {
+ "interval": 70,
+ "frames": ["_", "_", "_", "-", "`", "`", "'", "´", "-", "_", "_", "_"],
+ },
+ "hamburger": {"interval": 100, "frames": ["☱", "☲", "☴"]},
+ "growVertical": {
+ "interval": 120,
+ "frames": ["▁", "▃", "▄", "▅", "▆", "▇", "▆", "▅", "▄", "▃"],
+ },
+ "growHorizontal": {
+ "interval": 120,
+ "frames": ["▏", "▎", "▍", "▌", "▋", "▊", "▉", "▊", "▋", "▌", "▍", "▎"],
+ },
+ "balloon": {"interval": 140, "frames": [" ", ".", "o", "O", "@", "*", " "]},
+ "balloon2": {"interval": 120, "frames": [".", "o", "O", "°", "O", "o", "."]},
+ "noise": {"interval": 100, "frames": ["▓", "▒", "░"]},
+ "bounce": {"interval": 120, "frames": ["⠁", "⠂", "⠄", "⠂"]},
+ "boxBounce": {"interval": 120, "frames": ["▖", "▘", "▝", "▗"]},
+ "boxBounce2": {"interval": 100, "frames": ["▌", "▀", "▐", "▄"]},
+ "triangle": {"interval": 50, "frames": ["◢", "◣", "◤", "◥"]},
+ "arc": {"interval": 100, "frames": ["◜", "◠", "◝", "◞", "◡", "◟"]},
+ "circle": {"interval": 120, "frames": ["◡", "⊙", "◠"]},
+ "squareCorners": {"interval": 180, "frames": ["◰", "◳", "◲", "◱"]},
+ "circleQuarters": {"interval": 120, "frames": ["◴", "◷", "◶", "◵"]},
+ "circleHalves": {"interval": 50, "frames": ["◐", "◓", "◑", "◒"]},
+ "squish": {"interval": 100, "frames": ["╫", "╪"]},
+ "toggle": {"interval": 250, "frames": ["⊶", "⊷"]},
+ "toggle2": {"interval": 80, "frames": ["▫", "▪"]},
+ "toggle3": {"interval": 120, "frames": ["□", "■"]},
+ "toggle4": {"interval": 100, "frames": ["■", "□", "▪", "▫"]},
+ "toggle5": {"interval": 100, "frames": ["▮", "▯"]},
+ "toggle6": {"interval": 300, "frames": ["ဝ", "၀"]},
+ "toggle7": {"interval": 80, "frames": ["⦾", "⦿"]},
+ "toggle8": {"interval": 100, "frames": ["◍", "◌"]},
+ "toggle9": {"interval": 100, "frames": ["◉", "◎"]},
+ "toggle10": {"interval": 100, "frames": ["㊂", "㊀", "㊁"]},
+ "toggle11": {"interval": 50, "frames": ["⧇", "⧆"]},
+ "toggle12": {"interval": 120, "frames": ["☗", "☖"]},
+ "toggle13": {"interval": 80, "frames": ["=", "*", "-"]},
+ "arrow": {"interval": 100, "frames": ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"]},
+ "arrow2": {
+ "interval": 80,
+ "frames": ["⬆️ ", "↗️ ", "➡️ ", "↘️ ", "⬇️ ", "↙️ ", "⬅️ ", "↖️ "],
+ },
+ "arrow3": {
+ "interval": 120,
+ "frames": ["▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"],
+ },
+ "bouncingBar": {
+ "interval": 80,
+ "frames": [
+ "[ ]",
+ "[= ]",
+ "[== ]",
+ "[=== ]",
+ "[ ===]",
+ "[ ==]",
+ "[ =]",
+ "[ ]",
+ "[ =]",
+ "[ ==]",
+ "[ ===]",
+ "[====]",
+ "[=== ]",
+ "[== ]",
+ "[= ]",
+ ],
+ },
+ "bouncingBall": {
+ "interval": 80,
+ "frames": [
+ "( ● )",
+ "( ● )",
+ "( ● )",
+ "( ● )",
+ "( ●)",
+ "( ● )",
+ "( ● )",
+ "( ● )",
+ "( ● )",
+ "(● )",
+ ],
+ },
+ "smiley": {"interval": 200, "frames": ["😄 ", "😝 "]},
+ "monkey": {"interval": 300, "frames": ["🙈 ", "🙈 ", "🙉 ", "🙊 "]},
+ "hearts": {"interval": 100, "frames": ["💛 ", "💙 ", "💜 ", "💚 ", "❤️ "]},
+ "clock": {
+ "interval": 100,
+ "frames": [
+ "🕛 ",
+ "🕐 ",
+ "🕑 ",
+ "🕒 ",
+ "🕓 ",
+ "🕔 ",
+ "🕕 ",
+ "🕖 ",
+ "🕗 ",
+ "🕘 ",
+ "🕙 ",
+ "🕚 ",
+ ],
+ },
+ "earth": {"interval": 180, "frames": ["🌍 ", "🌎 ", "🌏 "]},
+ "material": {
+ "interval": 17,
+ "frames": [
+ "█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
+ "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
+ "███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
+ "████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
+ "██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
+ "██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
+ "███████▁▁▁▁▁▁▁▁▁▁▁▁▁",
+ "████████▁▁▁▁▁▁▁▁▁▁▁▁",
+ "█████████▁▁▁▁▁▁▁▁▁▁▁",
+ "█████████▁▁▁▁▁▁▁▁▁▁▁",
+ "██████████▁▁▁▁▁▁▁▁▁▁",
+ "███████████▁▁▁▁▁▁▁▁▁",
+ "█████████████▁▁▁▁▁▁▁",
+ "██████████████▁▁▁▁▁▁",
+ "██████████████▁▁▁▁▁▁",
+ "▁██████████████▁▁▁▁▁",
+ "▁██████████████▁▁▁▁▁",
+ "▁██████████████▁▁▁▁▁",
+ "▁▁██████████████▁▁▁▁",
+ "▁▁▁██████████████▁▁▁",
+ "▁▁▁▁█████████████▁▁▁",
+ "▁▁▁▁██████████████▁▁",
+ "▁▁▁▁██████████████▁▁",
+ "▁▁▁▁▁██████████████▁",
+ "▁▁▁▁▁██████████████▁",
+ "▁▁▁▁▁██████████████▁",
+ "▁▁▁▁▁▁██████████████",
+ "▁▁▁▁▁▁██████████████",
+ "▁▁▁▁▁▁▁█████████████",
+ "▁▁▁▁▁▁▁█████████████",
+ "▁▁▁▁▁▁▁▁████████████",
+ "▁▁▁▁▁▁▁▁████████████",
+ "▁▁▁▁▁▁▁▁▁███████████",
+ "▁▁▁▁▁▁▁▁▁███████████",
+ "▁▁▁▁▁▁▁▁▁▁██████████",
+ "▁▁▁▁▁▁▁▁▁▁██████████",
+ "▁▁▁▁▁▁▁▁▁▁▁▁████████",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
+ "█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
+ "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
+ "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
+ "███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
+ "████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
+ "█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
+ "█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
+ "██████▁▁▁▁▁▁▁▁▁▁▁▁▁█",
+ "████████▁▁▁▁▁▁▁▁▁▁▁▁",
+ "█████████▁▁▁▁▁▁▁▁▁▁▁",
+ "█████████▁▁▁▁▁▁▁▁▁▁▁",
+ "█████████▁▁▁▁▁▁▁▁▁▁▁",
+ "█████████▁▁▁▁▁▁▁▁▁▁▁",
+ "███████████▁▁▁▁▁▁▁▁▁",
+ "████████████▁▁▁▁▁▁▁▁",
+ "████████████▁▁▁▁▁▁▁▁",
+ "██████████████▁▁▁▁▁▁",
+ "██████████████▁▁▁▁▁▁",
+ "▁██████████████▁▁▁▁▁",
+ "▁██████████████▁▁▁▁▁",
+ "▁▁▁█████████████▁▁▁▁",
+ "▁▁▁▁▁████████████▁▁▁",
+ "▁▁▁▁▁████████████▁▁▁",
+ "▁▁▁▁▁▁███████████▁▁▁",
+ "▁▁▁▁▁▁▁▁█████████▁▁▁",
+ "▁▁▁▁▁▁▁▁█████████▁▁▁",
+ "▁▁▁▁▁▁▁▁▁█████████▁▁",
+ "▁▁▁▁▁▁▁▁▁█████████▁▁",
+ "▁▁▁▁▁▁▁▁▁▁█████████▁",
+ "▁▁▁▁▁▁▁▁▁▁▁████████▁",
+ "▁▁▁▁▁▁▁▁▁▁▁████████▁",
+ "▁▁▁▁▁▁▁▁▁▁▁▁███████▁",
+ "▁▁▁▁▁▁▁▁▁▁▁▁███████▁",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
+ "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
+ ],
+ },
+ "moon": {
+ "interval": 80,
+ "frames": ["🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "],
+ },
+ "runner": {"interval": 140, "frames": ["🚶 ", "🏃 "]},
+ "pong": {
+ "interval": 80,
+ "frames": [
+ "▐⠂ ▌",
+ "▐⠈ ▌",
+ "▐ ⠂ ▌",
+ "▐ ⠠ ▌",
+ "▐ ⡀ ▌",
+ "▐ ⠠ ▌",
+ "▐ ⠂ ▌",
+ "▐ ⠈ ▌",
+ "▐ ⠂ ▌",
+ "▐ ⠠ ▌",
+ "▐ ⡀ ▌",
+ "▐ ⠠ ▌",
+ "▐ ⠂ ▌",
+ "▐ ⠈ ▌",
+ "▐ ⠂▌",
+ "▐ ⠠▌",
+ "▐ ⡀▌",
+ "▐ ⠠ ▌",
+ "▐ ⠂ ▌",
+ "▐ ⠈ ▌",
+ "▐ ⠂ ▌",
+ "▐ ⠠ ▌",
+ "▐ ⡀ ▌",
+ "▐ ⠠ ▌",
+ "▐ ⠂ ▌",
+ "▐ ⠈ ▌",
+ "▐ ⠂ ▌",
+ "▐ ⠠ ▌",
+ "▐ ⡀ ▌",
+ "▐⠠ ▌",
+ ],
+ },
+ "shark": {
+ "interval": 120,
+ "frames": [
+ "▐|\\____________▌",
+ "▐_|\\___________▌",
+ "▐__|\\__________▌",
+ "▐___|\\_________▌",
+ "▐____|\\________▌",
+ "▐_____|\\_______▌",
+ "▐______|\\______▌",
+ "▐_______|\\_____▌",
+ "▐________|\\____▌",
+ "▐_________|\\___▌",
+ "▐__________|\\__▌",
+ "▐___________|\\_▌",
+ "▐____________|\\▌",
+ "▐____________/|▌",
+ "▐___________/|_▌",
+ "▐__________/|__▌",
+ "▐_________/|___▌",
+ "▐________/|____▌",
+ "▐_______/|_____▌",
+ "▐______/|______▌",
+ "▐_____/|_______▌",
+ "▐____/|________▌",
+ "▐___/|_________▌",
+ "▐__/|__________▌",
+ "▐_/|___________▌",
+ "▐/|____________▌",
+ ],
+ },
+ "dqpb": {"interval": 100, "frames": ["d", "q", "p", "b"]},
+ "weather": {
+ "interval": 100,
+ "frames": [
+ "☀️ ",
+ "☀️ ",
+ "☀️ ",
+ "🌤 ",
+ "⛅️ ",
+ "🌥 ",
+ "☁️ ",
+ "🌧 ",
+ "🌨 ",
+ "🌧 ",
+ "🌨 ",
+ "🌧 ",
+ "🌨 ",
+ "⛈ ",
+ "🌨 ",
+ "🌧 ",
+ "🌨 ",
+ "☁️ ",
+ "🌥 ",
+ "⛅️ ",
+ "🌤 ",
+ "☀️ ",
+ "☀️ ",
+ ],
+ },
+ "christmas": {"interval": 400, "frames": ["🌲", "🎄"]},
+ "grenade": {
+ "interval": 80,
+ "frames": [
+ "، ",
+ "′ ",
+ " ´ ",
+ " ‾ ",
+ " ⸌",
+ " ⸊",
+ " |",
+ " ⁎",
+ " ⁕",
+ " ෴ ",
+ " ⁓",
+ " ",
+ " ",
+ " ",
+ ],
+ },
+ "point": {"interval": 125, "frames": ["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"]},
+ "layer": {"interval": 150, "frames": ["-", "=", "≡"]},
+ "betaWave": {
+ "interval": 80,
+ "frames": [
+ "ρββββββ",
+ "βρβββββ",
+ "ββρββββ",
+ "βββρβββ",
+ "ββββρββ",
+ "βββββρβ",
+ "ββββββρ",
+ ],
+ },
+ "aesthetic": {
+ "interval": 80,
+ "frames": [
+ "▰▱▱▱▱▱▱",
+ "▰▰▱▱▱▱▱",
+ "▰▰▰▱▱▱▱",
+ "▰▰▰▰▱▱▱",
+ "▰▰▰▰▰▱▱",
+ "▰▰▰▰▰▰▱",
+ "▰▰▰▰▰▰▰",
+ "▰▱▱▱▱▱▱",
+ ],
+ },
+}
diff --git a/rich/_stack.py b/rich/_stack.py
new file mode 100644
index 0000000..194564e
--- /dev/null
+++ b/rich/_stack.py
@@ -0,0 +1,16 @@
+from typing import List, TypeVar
+
+T = TypeVar("T")
+
+
+class Stack(List[T]):
+ """A small shim over builtin list."""
+
+ @property
+ def top(self) -> T:
+ """Get top of stack."""
+ return self[-1]
+
+ def push(self, item: T) -> None:
+ """Push an item on to the stack (append in stack nomenclature)."""
+ self.append(item)
diff --git a/rich/_timer.py b/rich/_timer.py
new file mode 100644
index 0000000..b30d374
--- /dev/null
+++ b/rich/_timer.py
@@ -0,0 +1,18 @@
+"""
+Timer context manager, only used in debug.
+
+"""
+
+from time import time
+
+import contextlib
+
+
+@contextlib.contextmanager
+def timer(subject: str = "time"):
+ """print the elapsed time. (only used in debugging)"""
+ start = time()
+ yield
+ elapsed = time() - start
+ elapsed_ms = elapsed * 1000
+ print(f"{subject} elapsed {elapsed_ms:.1f}ms")
diff --git a/rich/_windows.py b/rich/_windows.py
new file mode 100644
index 0000000..d252d1f
--- /dev/null
+++ b/rich/_windows.py
@@ -0,0 +1,71 @@
+import sys
+
+from dataclasses import dataclass
+
+
+@dataclass
+class WindowsConsoleFeatures:
+ """Windows features available."""
+
+ vt: bool = False
+ """The console supports VT codes."""
+ truecolor: bool = False
+ """The console supports truecolor."""
+
+
+try:
+ import ctypes
+ from ctypes import wintypes
+ from ctypes import LibraryLoader
+
+ windll = LibraryLoader(ctypes.WinDLL) # type: ignore
+except (AttributeError, ImportError, ValueError):
+
+ # Fallback if we can't load the Windows DLL
+ def get_windows_console_features() -> WindowsConsoleFeatures:
+ features = WindowsConsoleFeatures()
+ return features
+
+
+else:
+
+ STDOUT = -11
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
+ _GetConsoleMode = windll.kernel32.GetConsoleMode
+ _GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD]
+ _GetConsoleMode.restype = wintypes.BOOL
+
+ _GetStdHandle = windll.kernel32.GetStdHandle
+ _GetStdHandle.argtypes = [
+ wintypes.DWORD,
+ ]
+ _GetStdHandle.restype = wintypes.HANDLE
+
+ def get_windows_console_features() -> WindowsConsoleFeatures:
+ """Get windows console features.
+
+ Returns:
+ WindowsConsoleFeatures: An instance of WindowsConsoleFeatures.
+ """
+ handle = _GetStdHandle(STDOUT)
+ console_mode = wintypes.DWORD()
+ result = _GetConsoleMode(handle, console_mode)
+ vt = bool(result and console_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+ truecolor = False
+ if vt:
+ win_version = sys.getwindowsversion() # type: ignore
+ truecolor = win_version.major > 10 or (
+ win_version.major == 10 and win_version.build >= 15063
+ )
+ features = WindowsConsoleFeatures(vt=vt, truecolor=truecolor)
+ return features
+
+
+if __name__ == "__main__":
+ import platform
+
+ features = get_windows_console_features()
+ from rich import print
+
+ print(f'platform="{platform.system()}"')
+ print(repr(features))
diff --git a/rich/_wrap.py b/rich/_wrap.py
new file mode 100644
index 0000000..b537757
--- /dev/null
+++ b/rich/_wrap.py
@@ -0,0 +1,55 @@
+import re
+from typing import Iterable, List, Tuple
+
+from .cells import cell_len, chop_cells
+from ._loop import loop_last
+
+re_word = re.compile(r"\s*\S+\s*")
+
+
+def words(text: str) -> Iterable[Tuple[int, int, str]]:
+ position = 0
+ word_match = re_word.match(text, position)
+ while word_match is not None:
+ start, end = word_match.span()
+ word = word_match.group(0)
+ yield start, end, word
+ word_match = re_word.match(text, end)
+
+
+def divide_line(text: str, width: int, fold: bool = True) -> List[int]:
+ divides: List[int] = []
+ append = divides.append
+ line_position = 0
+ _cell_len = cell_len
+ for start, _end, word in words(text):
+ word_length = _cell_len(word.rstrip())
+ if line_position + word_length > width:
+ if word_length > width:
+ if fold:
+ for last, line in loop_last(
+ chop_cells(word, width, position=line_position)
+ ):
+ if last:
+ line_position = _cell_len(line)
+ else:
+ start += len(line)
+ append(start)
+ else:
+ if start:
+ append(start)
+ line_position = _cell_len(word)
+ elif line_position and start:
+ append(start)
+ line_position = _cell_len(word)
+ else:
+ line_position += _cell_len(word)
+ return divides
+
+
+if __name__ == "__main__": # pragma: no cover
+ from .console import Console
+
+ console = Console(width=10)
+ console.print("12345 abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ 12345")
+ print(chop_cells("abcdefghijklmnopqrstuvwxyz", 10, position=2))
diff --git a/rich/abc.py b/rich/abc.py
new file mode 100644
index 0000000..42db7c0
--- /dev/null
+++ b/rich/abc.py
@@ -0,0 +1,33 @@
+from abc import ABC
+
+
+class RichRenderable(ABC):
+ """An abstract base class for Rich renderables.
+
+ Note that there is no need to extend this class, the intended use is to check if an
+ object supports the Rich renderable protocol. For example::
+
+ if isinstance(my_object, RichRenderable):
+ console.print(my_object)
+
+ """
+
+ @classmethod
+ def __subclasshook__(cls, other: type) -> bool:
+ """Check if this class supports the rich render protocol."""
+ return hasattr(other, "__rich_console__") or hasattr(other, "__rich__")
+
+
+if __name__ == "__main__": # pragma: no cover
+ from rich.text import Text
+
+ t = Text()
+ print(isinstance(Text, RichRenderable))
+ print(isinstance(t, RichRenderable))
+
+ class Foo:
+ pass
+
+ f = Foo()
+ print(isinstance(f, RichRenderable))
+ print(isinstance("", RichRenderable))
diff --git a/rich/align.py b/rich/align.py
new file mode 100644
index 0000000..05657d7
--- /dev/null
+++ b/rich/align.py
@@ -0,0 +1,304 @@
+from itertools import chain
+from typing import Iterable, TYPE_CHECKING
+
+from typing_extensions import Literal
+from .constrain import Constrain
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .segment import Segment
+from .style import StyleType
+
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderResult, RenderableType
+
+AlignMethod = Literal["left", "center", "right"]
+VerticalAlignMethod = Literal["top", "middle", "bottom"]
+AlignValues = AlignMethod # TODO: deprecate AlignValues
+
+
+class Align(JupyterMixin):
+ """Align a renderable by adding spaces if necessary.
+
+ Args:
+ renderable (RenderableType): A console renderable.
+ align (AlignMethod): One of "left", "center", or "right""
+ style (StyleType, optional): An optional style to apply to the background.
+ vertical (Optional[VerticalAlginMethod], optional): Optional vertical align, one of "top", "middle", or "bottom". Defaults to None.
+ pad (bool, optional): Pad the right with spaces. Defaults to True.
+ width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.
+ height (int, optional): Set height of align renderable, or None to fit to contents. Defaults to None.
+
+ Raises:
+ ValueError: if ``align`` is not one of the expected values.
+ """
+
+ def __init__(
+ self,
+ renderable: "RenderableType",
+ align: AlignMethod = "left",
+ style: StyleType = None,
+ *,
+ vertical: VerticalAlignMethod = None,
+ pad: bool = True,
+ width: int = None,
+ height: int = None,
+ ) -> None:
+ if align not in ("left", "center", "right"):
+ raise ValueError(
+ f'invalid value for align, expected "left", "center", or "right" (not {align!r})'
+ )
+ if vertical is not None and vertical not in ("top", "middle", "bottom"):
+ raise ValueError(
+ f'invalid value for vertical, expected "top", "middle", or "bottom" (not {vertical!r})'
+ )
+ self.renderable = renderable
+ self.align = align
+ self.style = style
+ self.vertical = vertical
+ self.pad = pad
+ self.width = width
+ self.height = height
+
+ def __repr__(self) -> str:
+ return f"Align({self.renderable!r}, {self.align!r})"
+
+ @classmethod
+ def left(
+ cls,
+ renderable: "RenderableType",
+ style: StyleType = None,
+ *,
+ vertical: VerticalAlignMethod = None,
+ pad: bool = True,
+ width: int = None,
+ height: int = None,
+ ) -> "Align":
+ """Align a renderable to the left."""
+ return cls(
+ renderable,
+ "left",
+ style=style,
+ vertical=vertical,
+ pad=pad,
+ width=width,
+ height=height,
+ )
+
+ @classmethod
+ def center(
+ cls,
+ renderable: "RenderableType",
+ style: StyleType = None,
+ *,
+ vertical: VerticalAlignMethod = None,
+ pad: bool = True,
+ width: int = None,
+ height: int = None,
+ ) -> "Align":
+ """Align a renderable to the center."""
+ return cls(
+ renderable,
+ "center",
+ style=style,
+ vertical=vertical,
+ pad=pad,
+ width=width,
+ height=height,
+ )
+
+ @classmethod
+ def right(
+ cls,
+ renderable: "RenderableType",
+ style: StyleType = None,
+ *,
+ vertical: VerticalAlignMethod = None,
+ pad: bool = True,
+ width: int = None,
+ height: int = None,
+ ) -> "Align":
+ """Align a renderable to the right."""
+ return cls(
+ renderable,
+ "right",
+ style=style,
+ vertical=vertical,
+ pad=pad,
+ width=width,
+ height=height,
+ )
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ align = self.align
+ width = Measurement.get(console, self.renderable).maximum
+ rendered = console.render(
+ Constrain(
+ self.renderable, width if self.width is None else min(width, self.width)
+ ),
+ options,
+ )
+ lines = list(Segment.split_lines(rendered))
+ width, height = Segment.get_shape(lines)
+ lines = Segment.set_shape(lines, width, height)
+ new_line = Segment.line()
+ excess_space = options.max_width - width
+ style = console.get_style(self.style) if self.style is not None else None
+
+ def generate_segments() -> Iterable[Segment]:
+ if excess_space <= 0:
+ # Exact fit
+ for line in lines:
+ yield from line
+ yield new_line
+
+ elif align == "left":
+ # Pad on the right
+ pad = Segment(" " * excess_space, style) if self.pad else None
+ for line in lines:
+ yield from line
+ if pad:
+ yield pad
+ yield new_line
+
+ elif align == "center":
+ # Pad left and right
+ left = excess_space // 2
+ pad = Segment(" " * left, style)
+ pad_right = (
+ Segment(" " * (excess_space - left), style) if self.pad else None
+ )
+ for line in lines:
+ if left:
+ yield pad
+ yield from line
+ if pad_right:
+ yield pad_right
+ yield new_line
+
+ elif align == "right":
+ # Padding on left
+ pad = Segment(" " * excess_space, style)
+ for line in lines:
+ yield pad
+ yield from line
+ yield new_line
+
+ blank_line = (
+ Segment(f"{' ' * (self.width or options.max_width)}\n", style)
+ if self.pad
+ else Segment("\n")
+ )
+
+ def blank_lines(count) -> Iterable[Segment]:
+ if count > 0:
+ for _ in range(count):
+ yield blank_line
+
+ vertical_height = self.height or options.height
+ iter_segments: Iterable[Segment]
+ if self.vertical and vertical_height is not None:
+ if self.vertical == "top":
+ bottom_space = vertical_height - height
+ iter_segments = chain(generate_segments(), blank_lines(bottom_space))
+ elif self.vertical == "middle":
+ top_space = (vertical_height - height) // 2
+ bottom_space = vertical_height - top_space - height
+ iter_segments = chain(
+ blank_lines(top_space),
+ generate_segments(),
+ blank_lines(bottom_space),
+ )
+ else: # self.vertical == "bottom":
+ top_space = vertical_height - height
+ iter_segments = chain(blank_lines(top_space), generate_segments())
+ else:
+ iter_segments = generate_segments()
+ if self.style is not None:
+ style = console.get_style(self.style)
+ iter_segments = Segment.apply_style(iter_segments, style)
+ yield from iter_segments
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
+ measurement = Measurement.get(console, self.renderable, max_width)
+ return measurement
+
+
+class VerticalCenter(JupyterMixin):
+ """Vertically aligns a renderable.
+
+ Warn:
+ This class is deprecated and may be removed in a future version. Use Align class with
+ `vertical="middle"`.
+
+ Args:
+ renderable (RenderableType): A renderable object.
+ """
+
+ def __init__(
+ self,
+ renderable: "RenderableType",
+ style: StyleType = None,
+ ) -> None:
+ self.renderable = renderable
+ self.style = style
+
+ def __repr__(self) -> str:
+ return f"VerticalCenter({self.renderable!r})"
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ style = console.get_style(self.style) if self.style is not None else None
+ lines = console.render_lines(
+ self.renderable, options.update(height=None), pad=False
+ )
+ width, _height = Segment.get_shape(lines)
+ new_line = Segment.line()
+ height = options.height or options.size.height
+ top_space = (height - len(lines)) // 2
+ bottom_space = height - top_space - len(lines)
+ blank_line = Segment(f"{' ' * width}", style)
+
+ def blank_lines(count) -> Iterable[Segment]:
+ for _ in range(count):
+ yield blank_line
+ yield new_line
+
+ if top_space > 0:
+ yield from blank_lines(top_space)
+ for line in lines:
+ yield from line
+ yield new_line
+ if bottom_space > 0:
+ yield from blank_lines(bottom_space)
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
+ measurement = Measurement.get(console, self.renderable, max_width)
+ return measurement
+
+
+if __name__ == "__main__": # pragma: no cover
+ from rich.console import Console, RenderGroup
+ from rich.highlighter import ReprHighlighter
+ from rich.panel import Panel
+
+ highlighter = ReprHighlighter()
+ console = Console()
+
+ panel = Panel(
+ RenderGroup(
+ Align.left(highlighter("align='left'")),
+ Align.center(highlighter("align='center'")),
+ Align.right(highlighter("align='right'")),
+ ),
+ width=60,
+ style="on dark_blue",
+ title="Algin",
+ )
+
+ console.print(
+ Align.center(panel, vertical="middle", style="on red", height=console.height)
+ )
diff --git a/rich/ansi.py b/rich/ansi.py
new file mode 100644
index 0000000..85410de
--- /dev/null
+++ b/rich/ansi.py
@@ -0,0 +1,228 @@
+from contextlib import suppress
+import re
+from typing import Iterable, NamedTuple
+
+from .color import Color
+from .style import Style
+from .text import Text
+
+re_ansi = re.compile(r"(?:\x1b\[(.*?)m)|(?:\x1b\](.*?)\x1b\\)")
+re_csi = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
+
+
+class _AnsiToken(NamedTuple):
+ """Result of ansi tokenized string."""
+
+ plain: str = ""
+ sgr: str = ""
+ osc: str = ""
+
+
+def _ansi_tokenize(ansi_text: str) -> Iterable[_AnsiToken]:
+ """Tokenize a string in to plain text and ANSI codes.
+
+ Args:
+ ansi_text (str): A String containing ANSI codes.
+
+ Yields:
+ AnsiToken: A named tuple of (plain, sgr, osc)
+ """
+
+ def remove_csi(ansi_text: str) -> str:
+ """Remove unknown CSI sequences."""
+ return re_csi.sub("", ansi_text)
+
+ position = 0
+ for match in re_ansi.finditer(ansi_text):
+ start, end = match.span(0)
+ sgr, osc = match.groups()
+ if start > position:
+ yield _AnsiToken(remove_csi(ansi_text[position:start]))
+ yield _AnsiToken("", sgr, osc)
+ position = end
+ if position < len(ansi_text):
+ yield _AnsiToken(remove_csi(ansi_text[position:]))
+
+
+SGR_STYLE_MAP = {
+ 1: "bold",
+ 2: "dim",
+ 3: "italic",
+ 4: "underline",
+ 5: "blink",
+ 6: "blink2",
+ 7: "reverse",
+ 8: "conceal",
+ 9: "strike",
+ 21: "underline2",
+ 22: "not dim not bold",
+ 23: "not italic",
+ 24: "not underline",
+ 25: "not blink",
+ 26: "not blink2",
+ 27: "not reverse",
+ 28: "not conceal",
+ 29: "not strike",
+ 30: "color(0)",
+ 31: "color(1)",
+ 32: "color(2)",
+ 33: "color(3)",
+ 34: "color(4)",
+ 35: "color(5)",
+ 36: "color(6)",
+ 37: "color(7)",
+ 39: "default",
+ 40: "on color(0)",
+ 41: "on color(1)",
+ 42: "on color(2)",
+ 43: "on color(3)",
+ 44: "on color(4)",
+ 45: "on color(5)",
+ 46: "on color(6)",
+ 47: "on color(7)",
+ 49: "on default",
+ 51: "frame",
+ 52: "encircle",
+ 53: "overline",
+ 54: "not frame not encircle",
+ 55: "not overline",
+ 90: "color(8)",
+ 91: "color(9)",
+ 92: "color(10)",
+ 93: "color(11)",
+ 94: "color(12)",
+ 95: "color(13)",
+ 96: "color(14)",
+ 97: "color(15)",
+ 100: "on color(8)",
+ 101: "on color(9)",
+ 102: "on color(10)",
+ 103: "on color(11)",
+ 104: "on color(12)",
+ 105: "on color(13)",
+ 106: "on color(14)",
+ 107: "on color(15)",
+}
+
+
+class AnsiDecoder:
+ """Translate ANSI code in to styled Text."""
+
+ def __init__(self) -> None:
+ self.style = Style.null()
+
+ def decode(self, terminal_text: str) -> Iterable[Text]:
+ """Decode ANSI codes in an interable of lines.
+
+ Args:
+ lines (Iterable[str]): An iterable of lines of terminal output.
+
+ Yields:
+ Text: Marked up Text.
+ """
+ for line in terminal_text.splitlines():
+ yield self.decode_line(line)
+
+ def decode_line(self, line: str) -> Text:
+ """Decode a line containing ansi codes.
+
+ Args:
+ line (str): A line of terminal output.
+
+ Returns:
+ Text: A Text instance marked up according to ansi codes.
+ """
+ from_ansi = Color.from_ansi
+ from_rgb = Color.from_rgb
+ _Style = Style
+ text = Text()
+ append = text.append
+ line = line.rsplit("\r", 1)[-1]
+ for token in _ansi_tokenize(line):
+ plain_text, sgr, osc = token
+ if plain_text:
+ append(plain_text, self.style or None)
+ elif osc:
+ if osc.startswith("8;"):
+ _params, semicolon, link = osc[2:].partition(";")
+ if semicolon:
+ self.style = self.style.update_link(link or None)
+ elif sgr:
+ # Translate in to semi-colon separated codes
+ # Ignore invalid codes, because we want to be lenient
+ codes = [
+ min(255, int(_code)) for _code in sgr.split(";") if _code.isdigit()
+ ]
+ iter_codes = iter(codes)
+ for code in iter_codes:
+ if code == 0:
+ # reset
+ self.style = _Style.null()
+ elif code in SGR_STYLE_MAP:
+ # styles
+ self.style += _Style.parse(SGR_STYLE_MAP[code])
+ elif code == 38:
+ #  Foreground
+ with suppress(StopIteration):
+ color_type = next(iter_codes)
+ if color_type == 5:
+ self.style += _Style.from_color(
+ from_ansi(next(iter_codes))
+ )
+ elif color_type == 2:
+ self.style += _Style.from_color(
+ from_rgb(
+ next(iter_codes),
+ next(iter_codes),
+ next(iter_codes),
+ )
+ )
+ elif code == 48:
+ # Background
+ with suppress(StopIteration):
+ color_type = next(iter_codes)
+ if color_type == 5:
+ self.style += _Style.from_color(
+ None, from_ansi(next(iter_codes))
+ )
+ elif color_type == 2:
+ self.style += _Style.from_color(
+ None,
+ from_rgb(
+ next(iter_codes),
+ next(iter_codes),
+ next(iter_codes),
+ ),
+ )
+
+ return text
+
+
+if __name__ == "__main__": # pragma: no cover
+ import pty
+ import io
+ import os
+ import sys
+
+ decoder = AnsiDecoder()
+
+ stdout = io.BytesIO()
+
+ def read(fd):
+ data = os.read(fd, 1024)
+ stdout.write(data)
+ return data
+
+ pty.spawn(sys.argv[1:], read)
+
+ from .console import Console
+
+ console = Console(record=True)
+
+ stdout_result = stdout.getvalue().decode("utf-8")
+ print(stdout_result)
+
+ for line in decoder.decode(stdout_result):
+ console.print(line)
+
+ console.save_html("stdout.html")
diff --git a/rich/bar.py b/rich/bar.py
new file mode 100644
index 0000000..5764b85
--- /dev/null
+++ b/rich/bar.py
@@ -0,0 +1,89 @@
+from typing import Union
+
+from .color import Color
+from .console import Console, ConsoleOptions, RenderResult
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .segment import Segment
+from .style import Style
+
+# There are left-aligned characters for 1/8 to 7/8, but
+# the right-aligned characters exist only for 1/8 and 4/8.
+BEGIN_BLOCK_ELEMENTS = ["█", "█", "█", "▐", "▐", "▐", "▕", "▕"]
+END_BLOCK_ELEMENTS = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"]
+FULL_BLOCK = "█"
+
+
+class Bar(JupyterMixin):
+ """Renders a solid block bar.
+
+ Args:
+ size (float): Value for the end of the bar.
+ begin (float): Begin point (between 0 and size, inclusive).
+ end (float): End point (between 0 and size, inclusive).
+ width (int, optional): Width of the bar, or ``None`` for maximum width. Defaults to None.
+ color (Union[Color, str], optional): Color of the bar. Defaults to "default".
+ bgcolor (Union[Color, str], optional): Color of bar background. Defaults to "default".
+ """
+
+ def __init__(
+ self,
+ size: float,
+ begin: float,
+ end: float,
+ *,
+ width: int = None,
+ color: Union[Color, str] = "default",
+ bgcolor: Union[Color, str] = "default",
+ ):
+ self.size = size
+ self.begin = max(begin, 0)
+ self.end = min(end, size)
+ self.width = width
+ self.style = Style(color=color, bgcolor=bgcolor)
+
+ def __repr__(self) -> str:
+ return f"Bar({self.size}, {self.begin}, {self.end})"
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+
+ width = min(self.width or options.max_width, options.max_width)
+
+ if self.begin >= self.end:
+ yield Segment(" " * width, self.style)
+ yield Segment.line()
+ return
+
+ prefix_complete_eights = int(width * 8 * self.begin / self.size)
+ prefix_bar_count = prefix_complete_eights // 8
+ prefix_eights_count = prefix_complete_eights % 8
+
+ body_complete_eights = int(width * 8 * self.end / self.size)
+ body_bar_count = body_complete_eights // 8
+ body_eights_count = body_complete_eights % 8
+
+ # When start and end fall into the same cell, we ideally should render
+ # a symbol that's "center-aligned", but there is no good symbol in Unicode.
+ # In this case, we fall back to right-aligned block symbol for simplicity.
+
+ prefix = " " * prefix_bar_count
+ if prefix_eights_count:
+ prefix += BEGIN_BLOCK_ELEMENTS[prefix_eights_count]
+
+ body = FULL_BLOCK * body_bar_count
+ if body_eights_count:
+ body += END_BLOCK_ELEMENTS[body_eights_count]
+
+ suffix = " " * (width - len(body))
+
+ yield Segment(prefix + body[len(prefix) :] + suffix, self.style)
+ yield Segment.line()
+
+ def __rich_measure__(self, console: Console, max_width: int) -> Measurement:
+ return (
+ Measurement(self.width, self.width)
+ if self.width is not None
+ else Measurement(4, max_width)
+ )
diff --git a/rich/box.py b/rich/box.py
new file mode 100644
index 0000000..b70b5c2
--- /dev/null
+++ b/rich/box.py
@@ -0,0 +1,478 @@
+from typing import TYPE_CHECKING, Iterable, List
+
+from typing_extensions import Literal
+
+from ._loop import loop_last
+
+if TYPE_CHECKING:
+ from rich.console import ConsoleOptions
+
+
+class Box:
+ """Defines characters to render boxes.
+
+ ┌─┬┐ top
+ │ ││ head
+ ├─┼┤ head_row
+ │ ││ mid
+ ├─┼┤ row
+ ├─┼┤ foot_row
+ │ ││ foot
+ └─┴┘ bottom
+
+ Args:
+ box (str): Characters making up box.
+ ascii (bool, optional): True if this box uses ascii characters only. Default is False.
+ """
+
+ def __init__(self, box: str, *, ascii: bool = False) -> None:
+ self._box = box
+ self.ascii = ascii
+ line1, line2, line3, line4, line5, line6, line7, line8 = box.splitlines()
+ # top
+ self.top_left, self.top, self.top_divider, self.top_right = iter(line1)
+ # head
+ self.head_left, _, self.head_vertical, self.head_right = iter(line2)
+ # head_row
+ (
+ self.head_row_left,
+ self.head_row_horizontal,
+ self.head_row_cross,
+ self.head_row_right,
+ ) = iter(line3)
+
+ # mid
+ self.mid_left, _, self.mid_vertical, self.mid_right = iter(line4)
+ # row
+ self.row_left, self.row_horizontal, self.row_cross, self.row_right = iter(line5)
+ # foot_row
+ (
+ self.foot_row_left,
+ self.foot_row_horizontal,
+ self.foot_row_cross,
+ self.foot_row_right,
+ ) = iter(line6)
+ # foot
+ self.foot_left, _, self.foot_vertical, self.foot_right = iter(line7)
+ # bottom
+ self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right = iter(
+ line8
+ )
+
+ def __repr__(self) -> str:
+ return "Box(...)"
+
+ def __str__(self) -> str:
+ return self._box
+
+ def substitute(self, options: "ConsoleOptions", safe: bool = True) -> "Box":
+ """Substitute this box for another if it won't render due to platform issues.
+
+ Args:
+ options (ConsoleOptions): Console options used in rendering.
+ safe (bool, optional): Substitute this for another Box if there are known problems
+ displaying on the platform (currently only relevant on Windows). Default is True.
+
+ Returns:
+ Box: A different Box or the same Box.
+ """
+ box = self
+ if options.legacy_windows and safe:
+ box = LEGACY_WINDOWS_SUBSTITUTIONS.get(box, box)
+ if options.ascii_only and not box.ascii:
+ box = ASCII
+ return box
+
+ def get_top(self, widths: Iterable[int]) -> str:
+ """Get the top of a simple box.
+
+ Args:
+ widths (List[int]): Widths of columns.
+
+ Returns:
+ str: A string of box characters.
+ """
+
+ parts: List[str] = []
+ append = parts.append
+ append(self.top_left)
+ for last, width in loop_last(widths):
+ append(self.top * width)
+ if not last:
+ append(self.top_divider)
+ append(self.top_right)
+ return "".join(parts)
+
+ def get_row(
+ self,
+ widths: Iterable[int],
+ level: Literal["head", "row", "foot", "mid"] = "row",
+ edge: bool = True,
+ ) -> str:
+ """Get the top of a simple box.
+
+ Args:
+ width (List[int]): Widths of columns.
+
+ Returns:
+ str: A string of box characters.
+ """
+ if level == "head":
+ left = self.head_row_left
+ horizontal = self.head_row_horizontal
+ cross = self.head_row_cross
+ right = self.head_row_right
+ elif level == "row":
+ left = self.row_left
+ horizontal = self.row_horizontal
+ cross = self.row_cross
+ right = self.row_right
+ elif level == "mid":
+ left = self.mid_left
+ horizontal = " "
+ cross = self.mid_vertical
+ right = self.mid_right
+ elif level == "foot":
+ left = self.foot_row_left
+ horizontal = self.foot_row_horizontal
+ cross = self.foot_row_cross
+ right = self.foot_row_right
+ else:
+ raise ValueError("level must be 'head', 'row' or 'foot'")
+
+ parts: List[str] = []
+ append = parts.append
+ if edge:
+ append(left)
+ for last, width in loop_last(widths):
+ append(horizontal * width)
+ if not last:
+ append(cross)
+ if edge:
+ append(right)
+ return "".join(parts)
+
+ def get_bottom(self, widths: Iterable[int]) -> str:
+ """Get the bottom of a simple box.
+
+ Args:
+ widths (List[int]): Widths of columns.
+
+ Returns:
+ str: A string of box characters.
+ """
+
+ parts: List[str] = []
+ append = parts.append
+ append(self.bottom_left)
+ for last, width in loop_last(widths):
+ append(self.bottom * width)
+ if not last:
+ append(self.bottom_divider)
+ append(self.bottom_right)
+ return "".join(parts)
+
+
+ASCII: Box = Box(
+ """\
++--+
+| ||
+|-+|
+| ||
+|-+|
+|-+|
+| ||
++--+
+""",
+ ascii=True,
+)
+
+ASCII2: Box = Box(
+ """\
++-++
+| ||
++-++
+| ||
++-++
++-++
+| ||
++-++
+""",
+ ascii=True,
+)
+
+ASCII_DOUBLE_HEAD: Box = Box(
+ """\
++-++
+| ||
++=++
+| ||
++-++
++-++
+| ||
++-++
+""",
+ ascii=True,
+)
+
+SQUARE: Box = Box(
+ """\
+┌─┬┐
+│ ││
+├─┼┤
+│ ││
+├─┼┤
+├─┼┤
+│ ││
+└─┴┘
+"""
+)
+
+SQUARE_DOUBLE_HEAD: Box = Box(
+ """\
+┌─┬┐
+│ ││
+╞═╪╡
+│ ││
+├─┼┤
+├─┼┤
+│ ││
+└─┴┘
+"""
+)
+
+MINIMAL: Box = Box(
+ """\
+ ╷
+ │
+╶─┼╴
+ │
+╶─┼╴
+╶─┼╴
+ │
+ ╵
+"""
+)
+
+
+MINIMAL_HEAVY_HEAD: Box = Box(
+ """\
+ ╷
+ │
+╺━┿╸
+ │
+╶─┼╴
+╶─┼╴
+ │
+ ╵
+"""
+)
+
+MINIMAL_DOUBLE_HEAD: Box = Box(
+ """\
+ ╷
+ │
+ ═╪
+ │
+ ─┼
+ ─┼
+ │
+ ╵
+"""
+)
+
+
+SIMPLE: Box = Box(
+ """\
+
+
+ ──
+
+
+ ──
+
+
+"""
+)
+
+SIMPLE_HEAD: Box = Box(
+ """\
+
+
+ ──
+
+
+
+
+
+"""
+)
+
+
+SIMPLE_HEAVY: Box = Box(
+ """\
+
+
+ ━━
+
+
+ ━━
+
+
+"""
+)
+
+
+HORIZONTALS: Box = Box(
+ """\
+ ──
+
+ ──
+
+ ──
+ ──
+
+ ──
+"""
+)
+
+ROUNDED: Box = Box(
+ """\
+╭─┬╮
+│ ││
+├─┼┤
+│ ││
+├─┼┤
+├─┼┤
+│ ││
+╰─┴╯
+"""
+)
+
+HEAVY: Box = Box(
+ """\
+┏━┳┓
+┃ ┃┃
+┣━╋┫
+┃ ┃┃
+┣━╋┫
+┣━╋┫
+┃ ┃┃
+┗━┻┛
+"""
+)
+
+HEAVY_EDGE: Box = Box(
+ """\
+┏━┯┓
+┃ │┃
+┠─┼┨
+┃ │┃
+┠─┼┨
+┠─┼┨
+┃ │┃
+┗━┷┛
+"""
+)
+
+HEAVY_HEAD: Box = Box(
+ """\
+┏━┳┓
+┃ ┃┃
+┡━╇┩
+│ ││
+├─┼┤
+├─┼┤
+│ ││
+└─┴┘
+"""
+)
+
+DOUBLE: Box = Box(
+ """\
+╔═╦╗
+║ ║║
+╠═╬╣
+║ ║║
+╠═╬╣
+╠═╬╣
+║ ║║
+╚═╩╝
+"""
+)
+
+DOUBLE_EDGE: Box = Box(
+ """\
+╔═╤╗
+║ │║
+╟─┼╢
+║ │║
+╟─┼╢
+╟─┼╢
+║ │║
+╚═╧╝
+"""
+)
+
+# Map Boxes that don't render with raster fonts on to equivalent that do
+LEGACY_WINDOWS_SUBSTITUTIONS = {
+ ROUNDED: SQUARE,
+ MINIMAL_HEAVY_HEAD: MINIMAL,
+ SIMPLE_HEAVY: SIMPLE,
+ HEAVY: SQUARE,
+ HEAVY_EDGE: SQUARE,
+ HEAVY_HEAD: SQUARE,
+}
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from rich.columns import Columns
+ from rich.panel import Panel
+
+ from . import box
+ from .console import Console
+ from .table import Table
+ from .text import Text
+
+ console = Console(record=True)
+
+ BOXES = [
+ "ASCII",
+ "ASCII2",
+ "ASCII_DOUBLE_HEAD",
+ "SQUARE",
+ "SQUARE_DOUBLE_HEAD",
+ "MINIMAL",
+ "MINIMAL_HEAVY_HEAD",
+ "MINIMAL_DOUBLE_HEAD",
+ "SIMPLE",
+ "SIMPLE_HEAD",
+ "SIMPLE_HEAVY",
+ "HORIZONTALS",
+ "ROUNDED",
+ "HEAVY",
+ "HEAVY_EDGE",
+ "HEAVY_HEAD",
+ "DOUBLE",
+ "DOUBLE_EDGE",
+ ]
+
+ console.print(Panel("[bold green]Box Constants", style="green"), justify="center")
+ console.print()
+
+ columns = Columns(expand=True, padding=2)
+ for box_name in sorted(BOXES):
+ table = Table(
+ show_footer=True, style="dim", border_style="not dim", expand=True
+ )
+ table.add_column("Header 1", "Footer 1")
+ table.add_column("Header 2", "Footer 2")
+ table.add_row("Cell", "Cell")
+ table.add_row("Cell", "Cell")
+ table.box = getattr(box, box_name)
+ table.title = Text(f"box.{box_name}", style="magenta")
+ columns.add_renderable(table)
+ console.print(columns)
+
+ # console.save_html("box.html", inline_styles=True)
diff --git a/rich/cells.py b/rich/cells.py
new file mode 100644
index 0000000..1a0ebcc
--- /dev/null
+++ b/rich/cells.py
@@ -0,0 +1,124 @@
+from functools import lru_cache
+from typing import Dict, List
+
+from ._cell_widths import CELL_WIDTHS
+from ._lru_cache import LRUCache
+
+
+def cell_len(text: str, _cache: Dict[str, int] = LRUCache(1024 * 4)) -> 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) <= 64:
+ _cache[text] = total_size
+ return total_size
+
+
+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
+ return _get_codepoint_cell_size(codepoint)
+
+
+@lru_cache(maxsize=4096)
+def _get_codepoint_cell_size(codepoint: int) -> 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.
+ """
+
+ _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 and character_sizes:
+ excess -= pop()
+ text = text[: len(character_sizes)]
+ if excess == -1:
+ text += " "
+ return text
+
+
+def chop_cells(text: str, max_size: int, position: int = 0) -> 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 = position
+ 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__": # pragma: no cover
+
+ 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/color.py b/rich/color.py
new file mode 100644
index 0000000..3421061
--- /dev/null
+++ b/rich/color.py
@@ -0,0 +1,575 @@
+import platform
+import re
+from colorsys import rgb_to_hls
+from enum import IntEnum
+from functools import lru_cache
+from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple
+
+from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE
+from .color_triplet import ColorTriplet
+from .terminal_theme import DEFAULT_TERMINAL_THEME
+
+if TYPE_CHECKING: # pragma: no cover
+ from .terminal_theme import TerminalTheme
+ from .text import Text
+
+
+WINDOWS = platform.system() == "Windows"
+
+
+class ColorSystem(IntEnum):
+ """One of the 3 color system supported by terminals."""
+
+ STANDARD = 1
+ EIGHT_BIT = 2
+ TRUECOLOR = 3
+ WINDOWS = 4
+
+
+class ColorType(IntEnum):
+ """Type of color stored in Color class."""
+
+ DEFAULT = 0
+ STANDARD = 1
+ EIGHT_BIT = 2
+ TRUECOLOR = 3
+ WINDOWS = 4
+
+
+ANSI_COLOR_NAMES = {
+ "black": 0,
+ "red": 1,
+ "green": 2,
+ "yellow": 3,
+ "blue": 4,
+ "magenta": 5,
+ "cyan": 6,
+ "white": 7,
+ "bright_black": 8,
+ "bright_red": 9,
+ "bright_green": 10,
+ "bright_yellow": 11,
+ "bright_blue": 12,
+ "bright_magenta": 13,
+ "bright_cyan": 14,
+ "bright_white": 15,
+ "grey0": 16,
+ "navy_blue": 17,
+ "dark_blue": 18,
+ "blue3": 20,
+ "blue1": 21,
+ "dark_green": 22,
+ "deep_sky_blue4": 25,
+ "dodger_blue3": 26,
+ "dodger_blue2": 27,
+ "green4": 28,
+ "spring_green4": 29,
+ "turquoise4": 30,
+ "deep_sky_blue3": 32,
+ "dodger_blue1": 33,
+ "green3": 40,
+ "spring_green3": 41,
+ "dark_cyan": 36,
+ "light_sea_green": 37,
+ "deep_sky_blue2": 38,
+ "deep_sky_blue1": 39,
+ "spring_green2": 47,
+ "cyan3": 43,
+ "dark_turquoise": 44,
+ "turquoise2": 45,
+ "green1": 46,
+ "spring_green1": 48,
+ "medium_spring_green": 49,
+ "cyan2": 50,
+ "cyan1": 51,
+ "dark_red": 88,
+ "deep_pink4": 125,
+ "purple4": 55,
+ "purple3": 56,
+ "blue_violet": 57,
+ "orange4": 94,
+ "grey37": 59,
+ "medium_purple4": 60,
+ "slate_blue3": 62,
+ "royal_blue1": 63,
+ "chartreuse4": 64,
+ "dark_sea_green4": 71,
+ "pale_turquoise4": 66,
+ "steel_blue": 67,
+ "steel_blue3": 68,
+ "cornflower_blue": 69,
+ "chartreuse3": 76,
+ "cadet_blue": 73,
+ "sky_blue3": 74,
+ "steel_blue1": 81,
+ "pale_green3": 114,
+ "sea_green3": 78,
+ "aquamarine3": 79,
+ "medium_turquoise": 80,
+ "chartreuse2": 112,
+ "sea_green2": 83,
+ "sea_green1": 85,
+ "aquamarine1": 122,
+ "dark_slate_gray2": 87,
+ "dark_magenta": 91,
+ "dark_violet": 128,
+ "purple": 129,
+ "light_pink4": 95,
+ "plum4": 96,
+ "medium_purple3": 98,
+ "slate_blue1": 99,
+ "yellow4": 106,
+ "wheat4": 101,
+ "grey53": 102,
+ "light_slate_grey": 103,
+ "medium_purple": 104,
+ "light_slate_blue": 105,
+ "dark_olive_green3": 149,
+ "dark_sea_green": 108,
+ "light_sky_blue3": 110,
+ "sky_blue2": 111,
+ "dark_sea_green3": 150,
+ "dark_slate_gray3": 116,
+ "sky_blue1": 117,
+ "chartreuse1": 118,
+ "light_green": 120,
+ "pale_green1": 156,
+ "dark_slate_gray1": 123,
+ "red3": 160,
+ "medium_violet_red": 126,
+ "magenta3": 164,
+ "dark_orange3": 166,
+ "indian_red": 167,
+ "hot_pink3": 168,
+ "medium_orchid3": 133,
+ "medium_orchid": 134,
+ "medium_purple2": 140,
+ "dark_goldenrod": 136,
+ "light_salmon3": 173,
+ "rosy_brown": 138,
+ "grey63": 139,
+ "medium_purple1": 141,
+ "gold3": 178,
+ "dark_khaki": 143,
+ "navajo_white3": 144,
+ "grey69": 145,
+ "light_steel_blue3": 146,
+ "light_steel_blue": 147,
+ "yellow3": 184,
+ "dark_sea_green2": 157,
+ "light_cyan3": 152,
+ "light_sky_blue1": 153,
+ "green_yellow": 154,
+ "dark_olive_green2": 155,
+ "dark_sea_green1": 193,
+ "pale_turquoise1": 159,
+ "deep_pink3": 162,
+ "magenta2": 200,
+ "hot_pink2": 169,
+ "orchid": 170,
+ "medium_orchid1": 207,
+ "orange3": 172,
+ "light_pink3": 174,
+ "pink3": 175,
+ "plum3": 176,
+ "violet": 177,
+ "light_goldenrod3": 179,
+ "tan": 180,
+ "misty_rose3": 181,
+ "thistle3": 182,
+ "plum2": 183,
+ "khaki3": 185,
+ "light_goldenrod2": 222,
+ "light_yellow3": 187,
+ "grey84": 188,
+ "light_steel_blue1": 189,
+ "yellow2": 190,
+ "dark_olive_green1": 192,
+ "honeydew2": 194,
+ "light_cyan1": 195,
+ "red1": 196,
+ "deep_pink2": 197,
+ "deep_pink1": 199,
+ "magenta1": 201,
+ "orange_red1": 202,
+ "indian_red1": 204,
+ "hot_pink": 206,
+ "dark_orange": 208,
+ "salmon1": 209,
+ "light_coral": 210,
+ "pale_violet_red1": 211,
+ "orchid2": 212,
+ "orchid1": 213,
+ "orange1": 214,
+ "sandy_brown": 215,
+ "light_salmon1": 216,
+ "light_pink1": 217,
+ "pink1": 218,
+ "plum1": 219,
+ "gold1": 220,
+ "navajo_white1": 223,
+ "misty_rose1": 224,
+ "thistle1": 225,
+ "yellow1": 226,
+ "light_goldenrod1": 227,
+ "khaki1": 228,
+ "wheat1": 229,
+ "cornsilk1": 230,
+ "grey100": 231,
+ "grey3": 232,
+ "grey7": 233,
+ "grey11": 234,
+ "grey15": 235,
+ "grey19": 236,
+ "grey23": 237,
+ "grey27": 238,
+ "grey30": 239,
+ "grey35": 240,
+ "grey39": 241,
+ "grey42": 242,
+ "grey46": 243,
+ "grey50": 244,
+ "grey54": 245,
+ "grey58": 246,
+ "grey62": 247,
+ "grey66": 248,
+ "grey70": 249,
+ "grey74": 250,
+ "grey78": 251,
+ "grey82": 252,
+ "grey85": 253,
+ "grey89": 254,
+ "grey93": 255,
+}
+
+
+class ColorParseError(Exception):
+ """The color could not be parsed."""
+
+
+RE_COLOR = re.compile(
+ r"""^
+\#([0-9a-f]{6})$|
+color\(([0-9]{1,3})\)$|
+rgb\(([\d\s,]+)\)$
+""",
+ re.VERBOSE,
+)
+
+
+class Color(NamedTuple):
+ """Terminal color definition."""
+
+ name: str
+ """The name of the color (typically the input to Color.parse)."""
+ type: ColorType
+ """The type of the color."""
+ number: Optional[int] = None
+ """The color number, if a standard color, or None."""
+ triplet: Optional[ColorTriplet] = None
+ """A triplet of color components, if an RGB color."""
+
+ def __repr__(self) -> str:
+ return (
+ f"<color {self.name!r} ({self.type.name.lower()})>"
+ if self.number is None
+ else f"<color {self.name!r} {self.number} ({self.type.name.lower()})>"
+ )
+
+ def __rich__(self) -> "Text":
+ """Dispays the actual color if Rich printed."""
+ from .text import Text
+ from .style import Style
+
+ return Text.assemble(
+ f"<color {self.name!r} ({self.type.name.lower()})",
+ ("⬤", Style(color=self)),
+ " >",
+ )
+
+ @property
+ def system(self) -> ColorSystem:
+ """Get the native color system for this color."""
+ if self.type == ColorType.DEFAULT:
+ return ColorSystem.STANDARD
+ return ColorSystem(int(self.type))
+
+ @property
+ def is_system_defined(self) -> bool:
+ """Check if the color is ultimately defined by the system."""
+ return self.system not in (ColorSystem.EIGHT_BIT, ColorSystem.TRUECOLOR)
+
+ @property
+ def is_default(self) -> bool:
+ """Check if the color is a default color."""
+ return self.type == ColorType.DEFAULT
+
+ def get_truecolor(
+ self, theme: "TerminalTheme" = None, foreground=True
+ ) -> ColorTriplet:
+ """Get an equivalent color triplet for this color.
+
+ Args:
+ theme (TerminalTheme, optional): Optional terminal theme, or None to use default. Defaults to None.
+ foreground (bool, optional): True for a foreground color, or False for background. Defaults to True.
+
+ Returns:
+ ColorTriplet: A color triplet containing RGB components.
+ """
+
+ if theme is None:
+ theme = DEFAULT_TERMINAL_THEME
+ if self.type == ColorType.TRUECOLOR:
+ assert self.triplet is not None
+ return self.triplet
+ elif self.type == ColorType.EIGHT_BIT:
+ assert self.number is not None
+ return EIGHT_BIT_PALETTE[self.number]
+ elif self.type == ColorType.STANDARD:
+ assert self.number is not None
+ return theme.ansi_colors[self.number]
+ elif self.type == ColorType.WINDOWS:
+ assert self.number is not None
+ return WINDOWS_PALETTE[self.number]
+ else: # self.type == ColorType.DEFAULT:
+ assert self.number is None
+ return theme.foreground_color if foreground else theme.background_color
+
+ @classmethod
+ def from_ansi(cls, number: int) -> "Color":
+ """Create a Color number from it's 8-bit ansi number.
+
+ Args:
+ number (int): A number between 0-255 inclusive.
+
+ Returns:
+ Color: A new Color instance.
+ """
+ return cls(
+ name=f"color({number})",
+ type=(ColorType.STANDARD if number < 16 else ColorType.EIGHT_BIT),
+ number=number,
+ )
+
+ @classmethod
+ def from_triplet(cls, triplet: "ColorTriplet") -> "Color":
+ """Create a truecolor RGB color from a triplet of values.
+
+ Args:
+ triplet (ColorTriplet): A color triplet containing red, green and blue components.
+
+ Returns:
+ Color: A new color object.
+ """
+ return cls(name=triplet.hex, type=ColorType.TRUECOLOR, triplet=triplet)
+
+ @classmethod
+ def from_rgb(cls, red: float, green: float, blue: float) -> "Color":
+ """Create a truecolor from three color components in the range(0->255).
+
+ Args:
+ red (float): Red component in range 0-255.
+ green (float): Green component in range 0-255.
+ blue (float): Blue component in range 0-255.
+
+ Returns:
+ Color: A new color object.
+ """
+ return cls.from_triplet(ColorTriplet(int(red), int(green), int(blue)))
+
+ @classmethod
+ def default(cls) -> "Color":
+ """Get a Color instance representing the default color.
+
+ Returns:
+ Color: Default color.
+ """
+ return cls(name="default", type=ColorType.DEFAULT)
+
+ @classmethod
+ @lru_cache(maxsize=1024)
+ def parse(cls, color: str) -> "Color":
+ """Parse a color definition."""
+ original_color = color
+ color = color.lower().strip()
+
+ if color == "default":
+ return cls(color, type=ColorType.DEFAULT)
+
+ color_number = ANSI_COLOR_NAMES.get(color)
+ if color_number is not None:
+ return cls(
+ color,
+ type=(ColorType.STANDARD if color_number < 16 else ColorType.EIGHT_BIT),
+ number=color_number,
+ )
+
+ color_match = RE_COLOR.match(color)
+ if color_match is None:
+ raise ColorParseError(f"{original_color!r} is not a valid color")
+
+ color_24, color_8, color_rgb = color_match.groups()
+ if color_24:
+ triplet = ColorTriplet(
+ int(color_24[0:2], 16), int(color_24[2:4], 16), int(color_24[4:6], 16)
+ )
+ return cls(color, ColorType.TRUECOLOR, triplet=triplet)
+
+ elif color_8:
+ number = int(color_8)
+ if number > 255:
+ raise ColorParseError(f"color number must be <= 255 in {color!r}")
+ return cls(
+ color,
+ type=(ColorType.STANDARD if number < 16 else ColorType.EIGHT_BIT),
+ number=number,
+ )
+
+ else: # color_rgb:
+ components = color_rgb.split(",")
+ if len(components) != 3:
+ raise ColorParseError(
+ f"expected three components in {original_color!r}"
+ )
+ red, green, blue = components
+ triplet = ColorTriplet(int(red), int(green), int(blue))
+ if not all(component <= 255 for component in triplet):
+ raise ColorParseError(
+ f"color components must be <= 255 in {original_color!r}"
+ )
+ return cls(color, ColorType.TRUECOLOR, triplet=triplet)
+
+ @lru_cache(maxsize=1024)
+ def get_ansi_codes(self, foreground: bool = True) -> Tuple[str, ...]:
+ """Get the ANSI escape codes for this color."""
+ _type = self.type
+ if _type == ColorType.DEFAULT:
+ return ("39" if foreground else "49",)
+
+ elif _type == ColorType.WINDOWS:
+ number = self.number
+ assert number is not None
+ fore, back = (30, 40) if number < 8 else (82, 92)
+ return (str(fore + number if foreground else back + number),)
+
+ elif _type == ColorType.STANDARD:
+ number = self.number
+ assert number is not None
+ fore, back = (30, 40) if number < 8 else (82, 92)
+ return (str(fore + number if foreground else back + number),)
+
+ elif _type == ColorType.EIGHT_BIT:
+ assert self.number is not None
+ return ("38" if foreground else "48", "5", str(self.number))
+
+ else: # self.standard == ColorStandard.TRUECOLOR:
+ assert self.triplet is not None
+ red, green, blue = self.triplet
+ return ("38" if foreground else "48", "2", str(red), str(green), str(blue))
+
+ @lru_cache(maxsize=1024)
+ def downgrade(self, system: ColorSystem) -> "Color":
+ """Downgrade a color system to a system with fewer colors."""
+
+ if self.type == ColorType.DEFAULT or self.type == system:
+ return self
+ # Convert to 8-bit color from truecolor color
+ if system == ColorSystem.EIGHT_BIT and self.system == ColorSystem.TRUECOLOR:
+ assert self.triplet is not None
+ red, green, blue = self.triplet.normalized
+ _h, l, s = rgb_to_hls(red, green, blue)
+ # If saturation is under 10% assume it is grayscale
+ if s < 0.1:
+ gray = round(l * 25.0)
+ if gray == 0:
+ color_number = 16
+ elif gray == 25:
+ color_number = 231
+ else:
+ color_number = 231 + gray
+ return Color(self.name, ColorType.EIGHT_BIT, number=color_number)
+
+ color_number = (
+ 16 + 36 * round(red * 5.0) + 6 * round(green * 5.0) + round(blue * 5.0)
+ )
+ return Color(self.name, ColorType.EIGHT_BIT, number=color_number)
+
+ # Convert to standard from truecolor or 8-bit
+ elif system == ColorSystem.STANDARD:
+ if self.system == ColorSystem.TRUECOLOR:
+ assert self.triplet is not None
+ triplet = self.triplet
+ else: # self.system == ColorSystem.EIGHT_BIT
+ assert self.number is not None
+ triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number])
+
+ color_number = STANDARD_PALETTE.match(triplet)
+ return Color(self.name, ColorType.STANDARD, number=color_number)
+
+ elif system == ColorSystem.WINDOWS:
+ if self.system == ColorSystem.TRUECOLOR:
+ assert self.triplet is not None
+ triplet = self.triplet
+ else: # self.system == ColorSystem.EIGHT_BIT
+ assert self.number is not None
+ if self.number < 16:
+ return Color(self.name, ColorType.WINDOWS, number=self.number)
+ triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number])
+
+ color_number = WINDOWS_PALETTE.match(triplet)
+ return Color(self.name, ColorType.WINDOWS, number=color_number)
+
+ return self
+
+
+def parse_rgb_hex(hex_color: str) -> ColorTriplet:
+ """Parse six hex characters in to RGB triplet."""
+ assert len(hex_color) == 6, "must be 6 characters"
+ color = ColorTriplet(
+ int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
+ )
+ return color
+
+
+def blend_rgb(
+ color1: ColorTriplet, color2: ColorTriplet, cross_fade: float = 0.5
+) -> ColorTriplet:
+ """Blend one RGB color in to another."""
+ r1, g1, b1 = color1
+ r2, g2, b2 = color2
+ new_color = ColorTriplet(
+ int(r1 + (r2 - r1) * cross_fade),
+ int(g1 + (g2 - g1) * cross_fade),
+ int(b1 + (b2 - b1) * cross_fade),
+ )
+ return new_color
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from .console import Console
+ from .table import Table
+ from .text import Text
+ from . import box
+
+ console = Console()
+
+ table = Table(show_footer=False, show_edge=True)
+ table.add_column("Color", width=10, overflow="ellipsis")
+ table.add_column("Number", justify="right", style="yellow")
+ table.add_column("Name", style="green")
+ table.add_column("Hex", style="blue")
+ table.add_column("RGB", style="magenta")
+
+ colors = sorted((v, k) for k, v in ANSI_COLOR_NAMES.items())
+ for color_number, name in colors:
+ color_cell = Text(" " * 10, style=f"on {name}")
+ if color_number < 16:
+ table.add_row(color_cell, f"{color_number}", Text(f'"{name}"'))
+ else:
+ color = EIGHT_BIT_PALETTE[color_number] # type: ignore
+ table.add_row(
+ color_cell, str(color_number), Text(f'"{name}"'), color.hex, color.rgb
+ )
+
+ console.print(table)
diff --git a/rich/color_triplet.py b/rich/color_triplet.py
new file mode 100644
index 0000000..75c03d2
--- /dev/null
+++ b/rich/color_triplet.py
@@ -0,0 +1,38 @@
+from typing import NamedTuple, Tuple
+
+
+class ColorTriplet(NamedTuple):
+ """The red, green, and blue components of a color."""
+
+ red: int
+ """Red component in 0 to 255 range."""
+ green: int
+ """Green component in 0 to 255 range."""
+ blue: int
+ """Blue component in 0 to 255 range."""
+
+ @property
+ def hex(self) -> str:
+ """get the color triplet in CSS style."""
+ red, green, blue = self
+ return f"#{red:02x}{green:02x}{blue:02x}"
+
+ @property
+ def rgb(self) -> str:
+ """The color in RGB format.
+
+ Returns:
+ str: An rgb color, e.g. ``"rgb(100,23,255)"``.
+ """
+ red, green, blue = self
+ return f"rgb({red},{green},{blue})"
+
+ @property
+ def normalized(self) -> Tuple[float, float, float]:
+ """Covert components in to floats between 0 and 1.
+
+ Returns:
+ Tuple[float, float, float]: A tuple of three normalized colour components.
+ """
+ red, green, blue = self
+ return red / 255.0, green / 255.0, blue / 255.0
diff --git a/rich/columns.py b/rich/columns.py
new file mode 100644
index 0000000..d152dcd
--- /dev/null
+++ b/rich/columns.py
@@ -0,0 +1,189 @@
+from collections import defaultdict
+from itertools import chain
+from operator import itemgetter
+from typing import Dict, Iterable, List, Optional, Tuple
+
+from .align import Align, AlignMethod
+from .console import Console, ConsoleOptions, RenderableType, RenderResult
+from .constrain import Constrain
+from .measure import Measurement
+from .padding import Padding, PaddingDimensions
+from .table import Table
+from .text import TextType
+from .jupyter import JupyterMixin
+
+
+class Columns(JupyterMixin):
+ """Display renderables in neat columns.
+
+ Args:
+ renderables (Iterable[RenderableType]): Any number of Rich renderables (including str).
+ width (int, optional): The desired width of the columns, or None to auto detect. Defaults to None.
+ padding (PaddingDimensions, optional): Optional padding around cells. Defaults to (0, 1).
+ expand (bool, optional): Expand columns to full width. Defaults to False.
+ equal (bool, optional): Arrange in to equal sized columns. Defaults to False.
+ column_first (bool, optional): Align items from top to bottom (rather than left to right). Defaults to False.
+ right_to_left (bool, optional): Start column from right hand side. Defaults to False.
+ align (str, optional): Align value ("left", "right", or "center") or None for default. Defaults to None.
+ title (TextType, optional): Optional title for Columns.
+ """
+
+ def __init__(
+ self,
+ renderables: Iterable[RenderableType] = None,
+ padding: PaddingDimensions = (0, 1),
+ *,
+ width: int = None,
+ expand: bool = False,
+ equal: bool = False,
+ column_first: bool = False,
+ right_to_left: bool = False,
+ align: AlignMethod = None,
+ title: TextType = None,
+ ) -> None:
+ self.renderables = list(renderables or [])
+ self.width = width
+ self.padding = padding
+ self.expand = expand
+ self.equal = equal
+ self.column_first = column_first
+ self.right_to_left = right_to_left
+ self.align = align
+ self.title = title
+
+ def add_renderable(self, renderable: RenderableType) -> None:
+ """Add a renderable to the columns.
+
+ Args:
+ renderable (RenderableType): Any renderable object.
+ """
+ self.renderables.append(renderable)
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ render_str = console.render_str
+ renderables = [
+ render_str(renderable) if isinstance(renderable, str) else renderable
+ for renderable in self.renderables
+ ]
+ if not renderables:
+ return
+ _top, right, _bottom, left = Padding.unpack(self.padding)
+ width_padding = max(left, right)
+ max_width = options.max_width
+ widths: Dict[int, int] = defaultdict(int)
+ column_count = len(renderables)
+
+ get_measurement = Measurement.get
+ renderable_widths = [
+ get_measurement(console, renderable, max_width).maximum
+ for renderable in renderables
+ ]
+ if self.equal:
+ renderable_widths = [max(renderable_widths)] * len(renderable_widths)
+
+ def iter_renderables(
+ column_count: int,
+ ) -> Iterable[Tuple[int, Optional[RenderableType]]]:
+ item_count = len(renderables)
+ if self.column_first:
+ width_renderables = list(zip(renderable_widths, renderables))
+
+ column_lengths: List[int] = [item_count // column_count] * column_count
+ for col_no in range(item_count % column_count):
+ column_lengths[col_no] += 1
+
+ row_count = (item_count + column_count - 1) // column_count
+ cells = [[-1] * column_count for _ in range(row_count)]
+ row = col = 0
+ for index in range(item_count):
+ cells[row][col] = index
+ column_lengths[col] -= 1
+ if column_lengths[col]:
+ row += 1
+ else:
+ col += 1
+ row = 0
+ for index in chain.from_iterable(cells):
+ if index == -1:
+ break
+ yield width_renderables[index]
+ else:
+ yield from zip(renderable_widths, renderables)
+ # Pad odd elements with spaces
+ if item_count % column_count:
+ for _ in range(column_count - (item_count % column_count)):
+ yield 0, None
+
+ table = Table.grid(padding=self.padding, collapse_padding=True, pad_edge=False)
+ table.expand = self.expand
+ table.title = self.title
+
+ if self.width is not None:
+ column_count = (max_width) // (self.width + width_padding)
+ for _ in range(column_count):
+ table.add_column(width=self.width)
+ else:
+ while column_count > 1:
+ widths.clear()
+ column_no = 0
+ for renderable_width, _ in iter_renderables(column_count):
+ widths[column_no] = max(widths[column_no], renderable_width)
+ total_width = sum(widths.values()) + width_padding * (
+ len(widths) - 1
+ )
+ if total_width > max_width:
+ column_count = len(widths) - 1
+ break
+ else:
+ column_no = (column_no + 1) % column_count
+ else:
+ break
+
+ get_renderable = itemgetter(1)
+ _renderables = [
+ get_renderable(_renderable)
+ for _renderable in iter_renderables(column_count)
+ ]
+ if self.equal:
+ _renderables = [
+ None
+ if renderable is None
+ else Constrain(renderable, renderable_widths[0])
+ for renderable in _renderables
+ ]
+ if self.align:
+ align = self.align
+ _Align = Align
+ _renderables = [
+ None if renderable is None else _Align(renderable, align)
+ for renderable in _renderables
+ ]
+
+ right_to_left = self.right_to_left
+ add_row = table.add_row
+ for start in range(0, len(_renderables), column_count):
+ row = _renderables[start : start + column_count]
+ if right_to_left:
+ row = row[::-1]
+ add_row(*row)
+ yield table
+
+
+if __name__ == "__main__": # pragma: no cover
+ import os
+
+ console = Console()
+
+ from rich.panel import Panel
+
+ files = [f"{i} {s}" for i, s in enumerate(sorted(os.listdir()))]
+ columns = Columns(files, padding=(0, 1), expand=False, equal=False)
+ console.print(columns)
+ console.rule()
+ columns.column_first = True
+ console.print(columns)
+ columns.right_to_left = True
+ console.rule()
+ console.print(columns)
diff --git a/rich/console.py b/rich/console.py
new file mode 100644
index 0000000..a59dbd6
--- /dev/null
+++ b/rich/console.py
@@ -0,0 +1,1821 @@
+import inspect
+import os
+import platform
+import shutil
+import sys
+import threading
+from abc import ABC, abstractmethod
+from collections import abc
+from dataclasses import dataclass, field, replace
+from datetime import datetime
+from functools import wraps
+from getpass import getpass
+from itertools import islice
+from time import monotonic
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ Optional,
+ TextIO,
+ Tuple,
+ Union,
+ cast,
+)
+
+from typing_extensions import Literal, Protocol, runtime_checkable
+
+from . import errors, themes
+from ._emoji_replace import _emoji_replace
+from ._log_render import LogRender, FormatTimeCallable
+from .align import Align, AlignMethod
+from .color import ColorSystem
+from .control import Control
+from .highlighter import NullHighlighter, ReprHighlighter
+from .markup import render as render_markup
+from .measure import Measurement, measure_renderables
+from .pager import Pager, SystemPager
+from .pretty import Pretty
+from .scope import render_scope
+from .screen import Screen
+from .segment import Segment
+from .style import Style, StyleType
+from .styled import Styled
+from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme
+from .text import Text, TextType
+from .theme import Theme, ThemeStack
+
+if TYPE_CHECKING:
+ from ._windows import WindowsConsoleFeatures
+ from .live import Live
+ from .status import Status
+
+WINDOWS = platform.system() == "Windows"
+
+HighlighterType = Callable[[Union[str, "Text"]], "Text"]
+JustifyMethod = Literal["default", "left", "center", "right", "full"]
+OverflowMethod = Literal["fold", "crop", "ellipsis", "ignore"]
+
+
+class NoChange:
+ pass
+
+
+NO_CHANGE = NoChange()
+
+
+CONSOLE_HTML_FORMAT = """\
+<!DOCTYPE html>
+<head>
+<meta charset="UTF-8">
+<style>
+{stylesheet}
+body {{
+ color: {foreground};
+ background-color: {background};
+}}
+</style>
+</head>
+<html>
+<body>
+ <code>
+ <pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace">{code}</pre>
+ </code>
+</body>
+</html>
+"""
+
+_TERM_COLORS = {"256color": ColorSystem.EIGHT_BIT, "16color": ColorSystem.STANDARD}
+
+
+class ConsoleDimensions(NamedTuple):
+ """Size of the terminal."""
+
+ width: int
+ """The width of the console in 'cells'."""
+ height: int
+ """The height of the console in lines."""
+
+
+@dataclass
+class ConsoleOptions:
+ """Options for __rich_console__ method."""
+
+ size: ConsoleDimensions
+ """Size of console."""
+ legacy_windows: bool
+ """legacy_windows: flag for legacy windows."""
+ min_width: int
+ """Minimum width of renderable."""
+ max_width: int
+ """Maximum width of renderable."""
+ is_terminal: bool
+ """True if the target is a terminal, otherwise False."""
+ encoding: str
+ """Encoding of terminal."""
+ justify: Optional[JustifyMethod] = None
+ """Justify value override for renderable."""
+ overflow: Optional[OverflowMethod] = None
+ """Overflow value override for renderable."""
+ no_wrap: Optional[bool] = False
+ """Disable wrapping for text."""
+ highlight: Optional[bool] = None
+ """Highlight override for render_str."""
+ height: Optional[int] = None
+ """Height available, or None for no height limit."""
+
+ @property
+ def ascii_only(self) -> bool:
+ """Check if renderables should use ascii only."""
+ return not self.encoding.startswith("utf")
+
+ def update(
+ self,
+ width: Union[int, NoChange] = NO_CHANGE,
+ min_width: Union[int, NoChange] = NO_CHANGE,
+ max_width: Union[int, NoChange] = NO_CHANGE,
+ justify: Union[Optional[JustifyMethod], NoChange] = NO_CHANGE,
+ overflow: Union[Optional[OverflowMethod], NoChange] = NO_CHANGE,
+ no_wrap: Union[Optional[bool], NoChange] = NO_CHANGE,
+ highlight: Union[Optional[bool], NoChange] = NO_CHANGE,
+ height: Union[Optional[int], NoChange] = NO_CHANGE,
+ ) -> "ConsoleOptions":
+ """Update values, return a copy."""
+ options = replace(self)
+ if not isinstance(width, NoChange):
+ options.min_width = options.max_width = width
+ if not isinstance(min_width, NoChange):
+ options.min_width = min_width
+ if not isinstance(max_width, NoChange):
+ options.max_width = max_width
+ if not isinstance(justify, NoChange):
+ options.justify = justify
+ if not isinstance(overflow, NoChange):
+ options.overflow = overflow
+ if not isinstance(no_wrap, NoChange):
+ options.no_wrap = no_wrap
+ if not isinstance(highlight, NoChange):
+ options.highlight = highlight
+ if not isinstance(height, NoChange):
+ options.height = height
+ return options
+
+ def update_width(self, width: int) -> "ConsoleOptions":
+ """Update just the width, return a copy.
+
+ Args:
+ width (int): New width (sets both min_width and max_width)
+
+ Returns:
+ ~ConsoleOptions: New console options instance
+ """
+ options = replace(self, min_width=width, max_width=width)
+ return options
+
+
+@runtime_checkable
+class RichCast(Protocol):
+ """An object that may be 'cast' to a console renderable."""
+
+ def __rich__(self) -> Union["ConsoleRenderable", str]: # pragma: no cover
+ ...
+
+
+@runtime_checkable
+class ConsoleRenderable(Protocol):
+ """An object that supports the console protocol."""
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult": # pragma: no cover
+ ...
+
+
+RenderableType = Union[ConsoleRenderable, RichCast, str]
+"""A type that may be rendered by Console."""
+
+RenderResult = Iterable[Union[RenderableType, Segment]]
+"""The result of calling a __rich_console__ method."""
+
+
+_null_highlighter = NullHighlighter()
+
+
+class CaptureError(Exception):
+ """An error in the Capture context manager."""
+
+
+class Capture:
+ """Context manager to capture the result of printing to the console.
+ See :meth:`~rich.console.Console.capture` for how to use.
+
+ Args:
+ console (Console): A console instance to capture output.
+ """
+
+ def __init__(self, console: "Console") -> None:
+ self._console = console
+ self._result: Optional[str] = None
+
+ def __enter__(self) -> "Capture":
+ self._console.begin_capture()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+ self._result = self._console.end_capture()
+
+ def get(self) -> str:
+ """Get the result of the capture."""
+ if self._result is None:
+ raise CaptureError(
+ "Capture result is not available until context manager exits."
+ )
+ return self._result
+
+
+class ThemeContext:
+ """A context manager to use a temporary theme. See :meth:`~rich.console.Console.use_theme` for usage."""
+
+ def __init__(self, console: "Console", theme: Theme, inherit: bool = True) -> None:
+ self.console = console
+ self.theme = theme
+ self.inherit = inherit
+
+ def __enter__(self) -> "ThemeContext":
+ self.console.push_theme(self.theme)
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+ self.console.pop_theme()
+
+
+class PagerContext:
+ """A context manager that 'pages' content. See :meth:`~rich.console.Console.pager` for usage."""
+
+ def __init__(
+ self,
+ console: "Console",
+ pager: Pager = None,
+ styles: bool = False,
+ links: bool = False,
+ ) -> None:
+ self._console = console
+ self.pager = SystemPager() if pager is None else pager
+ self.styles = styles
+ self.links = links
+
+ def __enter__(self) -> "PagerContext":
+ self._console._enter_buffer()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+ if exc_type is None:
+ with self._console._lock:
+ buffer: List[Segment] = self._console._buffer[:]
+ del self._console._buffer[:]
+ segments: Iterable[Segment] = buffer
+ if not self.styles:
+ segments = Segment.strip_styles(segments)
+ elif not self.links:
+ segments = Segment.strip_links(segments)
+ content = self._console._render_buffer(segments)
+ self.pager.show(content)
+ self._console._exit_buffer()
+
+
+class ScreenContext:
+ """A context manager that enables an alternative screen. See :meth:`~rich.console.Console.screen` for usage."""
+
+ def __init__(
+ self, console: "Console", hide_cursor: bool, style: StyleType = ""
+ ) -> None:
+ self.console = console
+ self.hide_cursor = hide_cursor
+ self.screen = Screen(style=style)
+ self._changed = False
+
+ def update(
+ self, renderable: RenderableType = None, style: StyleType = None
+ ) -> None:
+ """Update the screen.
+
+ Args:
+ renderable (RenderableType, optional): Optional renderable to replace current renderable,
+ or None for no change. Defaults to None.
+ style: (Style, optional): Replacement style, or None for no change. Defaults to None.
+ """
+ if renderable is not None:
+ self.screen.renderable = renderable
+ if style is not None:
+ self.screen.style = style
+ self.console.print(self.screen, end="")
+
+ def __enter__(self) -> "ScreenContext":
+ self._changed = self.console.set_alt_screen(True)
+ if self._changed and self.hide_cursor:
+ self.console.show_cursor(False)
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+ if self._changed:
+ self.console.set_alt_screen(False)
+ if self.hide_cursor:
+ self.console.show_cursor(True)
+
+
+class RenderGroup:
+ """Takes a group of renderables and returns a renderable object that renders the group.
+
+ Args:
+ renderables (Iterable[RenderableType]): An iterable of renderable objects.
+ fit (bool, optional): Fit dimension of group to contents, or fill available space. Defaults to True.
+ """
+
+ def __init__(self, *renderables: "RenderableType", fit: bool = True) -> None:
+ self._renderables = renderables
+ self.fit = fit
+ self._render: Optional[List[RenderableType]] = None
+
+ @property
+ def renderables(self) -> List["RenderableType"]:
+ if self._render is None:
+ self._render = list(self._renderables)
+ return self._render
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
+ if self.fit:
+ return measure_renderables(console, self.renderables, max_width)
+ else:
+ return Measurement(max_width, max_width)
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> RenderResult:
+ yield from self.renderables
+
+
+def render_group(fit: bool = True) -> Callable:
+ """A decorator that turns an iterable of renderables in to a group.
+
+ Args:
+ fit (bool, optional): Fit dimension of group to contents, or fill available space. Defaults to True.
+ """
+
+ def decorator(method):
+ """Convert a method that returns an iterable of renderables in to a RenderGroup."""
+
+ @wraps(method)
+ def _replace(*args, **kwargs):
+ renderables = method(*args, **kwargs)
+ return RenderGroup(*renderables, fit=fit)
+
+ return _replace
+
+ return decorator
+
+
+def _is_jupyter() -> bool: # pragma: no cover
+ """Check if we're running in a Jupyter notebook."""
+ try:
+ get_ipython # type: ignore
+ except NameError:
+ return False
+ shell = get_ipython().__class__.__name__ # type: ignore
+ if shell == "ZMQInteractiveShell":
+ return True # Jupyter notebook or qtconsole
+ elif shell == "TerminalInteractiveShell":
+ return False # Terminal running IPython
+ else:
+ return False # Other type (?)
+
+
+COLOR_SYSTEMS = {
+ "standard": ColorSystem.STANDARD,
+ "256": ColorSystem.EIGHT_BIT,
+ "truecolor": ColorSystem.TRUECOLOR,
+ "windows": ColorSystem.WINDOWS,
+}
+
+
+_COLOR_SYSTEMS_NAMES = {system: name for name, system in COLOR_SYSTEMS.items()}
+
+
+@dataclass
+class ConsoleThreadLocals(threading.local):
+ """Thread local values for Console context."""
+
+ theme_stack: ThemeStack
+ buffer: List[Segment] = field(default_factory=list)
+ buffer_index: int = 0
+
+
+class RenderHook(ABC):
+ """Provides hooks in to the render process."""
+
+ @abstractmethod
+ def process_renderables(
+ self, renderables: List[ConsoleRenderable]
+ ) -> List[ConsoleRenderable]:
+ """Called with a list of objects to render.
+
+ This method can return a new list of renderables, or modify and return the same list.
+
+ Args:
+ renderables (List[ConsoleRenderable]): A number of renderable objects.
+
+ Returns:
+ List[ConsoleRenderable]: A replacement list of renderables.
+ """
+
+
+_windows_console_features: Optional["WindowsConsoleFeatures"] = None
+
+
+def get_windows_console_features() -> "WindowsConsoleFeatures": # pragma: no cover
+ global _windows_console_features
+ if _windows_console_features is not None:
+ return _windows_console_features
+ from ._windows import get_windows_console_features
+
+ _windows_console_features = get_windows_console_features()
+ return _windows_console_features
+
+
+def detect_legacy_windows() -> bool:
+ """Detect legacy Windows."""
+ return WINDOWS and not get_windows_console_features().vt
+
+
+if detect_legacy_windows(): # pragma: no cover
+ from colorama import init
+
+ init()
+
+
+class Console:
+ """A high level console interface.
+
+ Args:
+ color_system (str, optional): The color system supported by your terminal,
+ either ``"standard"``, ``"256"`` or ``"truecolor"``. Leave as ``"auto"`` to autodetect.
+ force_terminal (Optional[bool], optional): Enable/disable terminal control codes, or None to auto-detect terminal. Defaults to None.
+ force_jupyter (Optional[bool], optional): Enable/disable Jupyter rendering, or None to auto-detect Jupyter. Defaults to None.
+ force_interactive (Optional[bool], optional): Enable/disable interactive mode, or None to auto detect. Defaults to None.
+ soft_wrap (Optional[bool], optional): Set soft wrap default on print method. Defaults to False.
+ theme (Theme, optional): An optional style theme object, or ``None`` for default theme.
+ stderr (bool, optional): Use stderr rather than stdout if ``file`` is not specified. Defaults to False.
+ file (IO, optional): A file object where the console should write to. Defaults to stdout.
+ quiet (bool, Optional): Boolean to suppress all output. Defaults to False.
+ width (int, optional): The width of the terminal. Leave as default to auto-detect width.
+ height (int, optional): The height of the terminal. Leave as default to auto-detect height.
+ style (StyleType, optional): Style to apply to all output, or None for no style. Defaults to None.
+ no_color (Optional[bool], optional): Enabled no color mode, or None to auto detect. Defaults to None.
+ tab_size (int, optional): Number of spaces used to replace a tab character. Defaults to 8.
+ record (bool, optional): Boolean to enable recording of terminal output,
+ required to call :meth:`export_html` and :meth:`export_text`. Defaults to False.
+ 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_path (bool, optional): Boolean to enable the logging of the caller by :meth:`log`. Defaults to True.
+ log_time_format (Union[str, TimeFormatterCallable], optional): If ``log_time`` is enabled, either string for strftime or callable that formats the time. Defaults to "[%X] ".
+ highlighter (HighlighterType, optional): Default highlighter.
+ legacy_windows (bool, optional): Enable legacy Windows mode, or ``None`` to auto detect. Defaults to ``None``.
+ safe_box (bool, optional): Restrict box options that don't render on legacy Windows.
+ get_datetime (Callable[[], datetime], optional): Callable that gets the current time as a datetime.datetime object (used by Console.log),
+ or None for datetime.now.
+ get_time (Callable[[], time], optional): Callable that gets the current time in seconds, default uses time.monotonic.
+ """
+
+ def __init__(
+ self,
+ *,
+ color_system: Optional[
+ Literal["auto", "standard", "256", "truecolor", "windows"]
+ ] = "auto",
+ force_terminal: bool = None,
+ force_jupyter: bool = None,
+ force_interactive: bool = None,
+ soft_wrap: bool = False,
+ theme: Theme = None,
+ stderr: bool = False,
+ file: IO[str] = None,
+ quiet: bool = False,
+ width: int = None,
+ height: int = None,
+ style: StyleType = None,
+ no_color: bool = None,
+ tab_size: int = 8,
+ record: bool = False,
+ markup: bool = True,
+ emoji: bool = True,
+ highlight: bool = True,
+ log_time: bool = True,
+ log_path: bool = True,
+ log_time_format: Union[str, FormatTimeCallable] = "[%X]",
+ highlighter: Optional["HighlighterType"] = ReprHighlighter(),
+ legacy_windows: bool = None,
+ safe_box: bool = True,
+ get_datetime: Callable[[], datetime] = None,
+ get_time: Callable[[], float] = None,
+ _environ: Dict[str, str] = None,
+ ):
+ # Copy of os.environ allows us to replace it for testing
+ self._environ = os.environ if _environ is None else _environ
+
+ self.is_jupyter = _is_jupyter() if force_jupyter is None else force_jupyter
+ if self.is_jupyter:
+ width = width or 93
+ height = height or 100
+ self.soft_wrap = soft_wrap
+ self._width = width
+ self._height = height
+ self.tab_size = tab_size
+ self.record = record
+ self._markup = markup
+ self._emoji = emoji
+ self._highlight = highlight
+ self.legacy_windows: bool = (
+ (detect_legacy_windows() and not self.is_jupyter)
+ if legacy_windows is None
+ else legacy_windows
+ )
+
+ self._color_system: Optional[ColorSystem]
+ self._force_terminal = force_terminal
+ self._file = file
+ self.quiet = quiet
+ self.stderr = stderr
+
+ if color_system is None:
+ self._color_system = None
+ elif color_system == "auto":
+ self._color_system = self._detect_color_system()
+ else:
+ self._color_system = COLOR_SYSTEMS[color_system]
+
+ self._lock = threading.RLock()
+ self._log_render = LogRender(
+ show_time=log_time,
+ show_path=log_path,
+ time_format=log_time_format,
+ )
+ self.highlighter: HighlighterType = highlighter or _null_highlighter
+ self.safe_box = safe_box
+ self.get_datetime = get_datetime or datetime.now
+ self.get_time = get_time or monotonic
+ self.style = style
+ self.no_color = (
+ no_color if no_color is not None else "NO_COLOR" in self._environ
+ )
+ self.is_interactive = (
+ (self.is_terminal and not self.is_dumb_terminal)
+ if force_interactive is None
+ else force_interactive
+ )
+
+ self._record_buffer_lock = threading.RLock()
+ self._thread_locals = ConsoleThreadLocals(
+ theme_stack=ThemeStack(themes.DEFAULT if theme is None else theme)
+ )
+ self._record_buffer: List[Segment] = []
+ self._render_hooks: List[RenderHook] = []
+ self._live: Optional["Live"] = None
+
+ def __repr__(self) -> str:
+ return f"<console width={self.width} {str(self._color_system)}>"
+
+ @property
+ def file(self) -> IO[str]:
+ """Get the file object to write to."""
+ file = self._file or (sys.stderr if self.stderr else sys.stdout)
+ file = getattr(file, "rich_proxied_file", file)
+ return file
+
+ @file.setter
+ def file(self, new_file: IO[str]) -> None:
+ """Set a new file object."""
+ self._file = new_file
+
+ @property
+ def _buffer(self) -> List[Segment]:
+ """Get a thread local buffer."""
+ return self._thread_locals.buffer
+
+ @property
+ def _buffer_index(self) -> int:
+ """Get a thread local buffer."""
+ return self._thread_locals.buffer_index
+
+ @_buffer_index.setter
+ def _buffer_index(self, value: int) -> None:
+ self._thread_locals.buffer_index = value
+
+ @property
+ def _theme_stack(self) -> ThemeStack:
+ """Get the thread local theme stack."""
+ return self._thread_locals.theme_stack
+
+ def _detect_color_system(self) -> Optional[ColorSystem]:
+ """Detect color system from env vars."""
+ if self.is_jupyter:
+ return ColorSystem.TRUECOLOR
+ if not self.is_terminal or self.is_dumb_terminal:
+ return None
+ if WINDOWS: # pragma: no cover
+ if self.legacy_windows: # pragma: no cover
+ return ColorSystem.WINDOWS
+ windows_console_features = get_windows_console_features()
+ return (
+ ColorSystem.TRUECOLOR
+ if windows_console_features.truecolor
+ else ColorSystem.EIGHT_BIT
+ )
+ else:
+ color_term = self._environ.get("COLORTERM", "").strip().lower()
+ if color_term in ("truecolor", "24bit"):
+ return ColorSystem.TRUECOLOR
+ term = self._environ.get("TERM", "").strip().lower()
+ _term_name, _hyphen, colors = term.partition("-")
+ color_system = _TERM_COLORS.get(colors, ColorSystem.STANDARD)
+ return color_system
+
+ def _enter_buffer(self) -> None:
+ """Enter in to a buffer context, and buffer all output."""
+ self._buffer_index += 1
+
+ def _exit_buffer(self) -> None:
+ """Leave buffer context, and render content if required."""
+ self._buffer_index -= 1
+ self._check_buffer()
+
+ def set_live(self, live: "Live") -> None:
+ """Set Live instance. Used by Live context manager.
+
+ Args:
+ live (Live): Live instance using this Console.
+
+ Raises:
+ errors.LiveError: If this Console has a Live context currently active.
+ """
+ with self._lock:
+ if self._live is not None:
+ raise errors.LiveError("Only one live display may be active at once")
+ self._live = live
+
+ def clear_live(self) -> None:
+ """Clear the Live instance."""
+ with self._lock:
+ self._live = None
+
+ def push_render_hook(self, hook: RenderHook) -> None:
+ """Add a new render hook to the stack.
+
+ Args:
+ hook (RenderHook): Render hook instance.
+ """
+
+ self._render_hooks.append(hook)
+
+ def pop_render_hook(self) -> None:
+ """Pop the last renderhook from the stack."""
+ self._render_hooks.pop()
+
+ def __enter__(self) -> "Console":
+ """Own context manager to enter buffer context."""
+ self._enter_buffer()
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback) -> None:
+ """Exit buffer context."""
+ self._exit_buffer()
+
+ def begin_capture(self) -> None:
+ """Begin capturing console output. Call :meth:`end_capture` to exit capture mode and return output."""
+ self._enter_buffer()
+
+ def end_capture(self) -> str:
+ """End capture mode and return captured string.
+
+ Returns:
+ str: Console output.
+ """
+ render_result = self._render_buffer(self._buffer)
+ del self._buffer[:]
+ self._exit_buffer()
+ return render_result
+
+ def push_theme(self, theme: Theme, *, inherit: bool = True) -> None:
+ """Push a new theme on to the top of the stack, replacing the styles from the previous theme.
+ Generally speaking, you should call :meth:`~rich.console.Console.use_theme` to get a context manager, rather
+ than calling this method directly.
+
+ Args:
+ theme (Theme): A theme instance.
+ inherit (bool, optional): Inherit existing styles. Defaults to True.
+ """
+ self._theme_stack.push_theme(theme, inherit=inherit)
+
+ def pop_theme(self) -> None:
+ """Remove theme from top of stack, restoring previous theme."""
+ self._theme_stack.pop_theme()
+
+ def use_theme(self, theme: Theme, *, inherit: bool = True) -> ThemeContext:
+ """Use a different theme for the duration of the context manager.
+
+ Args:
+ theme (Theme): Theme instance to user.
+ inherit (bool, optional): Inherit existing console styles. Defaults to True.
+
+ Returns:
+ ThemeContext: [description]
+ """
+ return ThemeContext(self, theme, inherit)
+
+ @property
+ def color_system(self) -> Optional[str]:
+ """Get color system string.
+
+ Returns:
+ Optional[str]: "standard", "256" or "truecolor".
+ """
+
+ if self._color_system is not None:
+ return _COLOR_SYSTEMS_NAMES[self._color_system]
+ else:
+ return None
+
+ @property
+ def encoding(self) -> str:
+ """Get the encoding of the console file, e.g. ``"utf-8"``.
+
+ Returns:
+ str: A standard encoding string.
+ """
+ return (getattr(self.file, "encoding", "utf-8") or "utf-8").lower()
+
+ @property
+ def is_terminal(self) -> bool:
+ """Check if the console is writing to a terminal.
+
+ Returns:
+ bool: True if the console writing to a device capable of
+ understanding terminal codes, otherwise False.
+ """
+ if self._force_terminal is not None:
+ return self._force_terminal
+ isatty = getattr(self.file, "isatty", None)
+ return False if isatty is None else isatty()
+
+ @property
+ def is_dumb_terminal(self) -> bool:
+ """Detect dumb terminal.
+
+ Returns:
+ bool: True if writing to a dumb terminal, otherwise False.
+
+ """
+ _term = self._environ.get("TERM", "")
+ is_dumb = _term.lower() in ("dumb", "unknown")
+ return self.is_terminal and is_dumb
+
+ @property
+ def options(self) -> ConsoleOptions:
+ """Get default console options."""
+ return ConsoleOptions(
+ size=self.size,
+ legacy_windows=self.legacy_windows,
+ min_width=1,
+ max_width=self.width,
+ encoding=self.encoding,
+ is_terminal=self.is_terminal,
+ )
+
+ @property
+ def size(self) -> ConsoleDimensions:
+ """Get the size of the console.
+
+ Returns:
+ ConsoleDimensions: A named tuple containing the dimensions.
+ """
+
+ if self._width is not None and self._height is not None:
+ return ConsoleDimensions(self._width, self._height)
+
+ if self.is_dumb_terminal:
+ return ConsoleDimensions(80, 25)
+
+ width: Optional[int] = None
+ height: Optional[int] = None
+ if WINDOWS: # pragma: no cover
+ width, height = shutil.get_terminal_size()
+ else:
+ try:
+ width, height = os.get_terminal_size(sys.stdin.fileno())
+ except (AttributeError, ValueError, OSError):
+ try:
+ width, height = os.get_terminal_size(sys.stdout.fileno())
+ except (AttributeError, ValueError, OSError):
+ pass
+
+ # get_terminal_size can report 0, 0 if run from pseudo-terminal
+ width = width or 80
+ height = height or 25
+ return ConsoleDimensions(
+ (width - self.legacy_windows) if self._width is None else self._width,
+ height if self._height is None else self._height,
+ )
+
+ @property
+ def width(self) -> int:
+ """Get the width of the console.
+
+ Returns:
+ int: The width (in characters) of the console.
+ """
+ width, _ = self.size
+ return width
+
+ @property
+ def height(self) -> int:
+ """Get the height of the console.
+
+ Returns:
+ int: The height (in lines) of the console.
+ """
+ _, height = self.size
+ return height
+
+ def bell(self) -> None:
+ """Play a 'bell' sound (if supported by the terminal)."""
+ self.control("\x07")
+
+ def capture(self) -> Capture:
+ """A context manager to *capture* the result of print() or log() in a string,
+ rather than writing it to the console.
+
+ Example:
+ >>> from rich.console import Console
+ >>> console = Console()
+ >>> with console.capture() as capture:
+ ... console.print("[bold magenta]Hello World[/]")
+ >>> print(capture.get())
+
+ Returns:
+ Capture: Context manager with disables writing to the terminal.
+ """
+ capture = Capture(self)
+ return capture
+
+ def pager(
+ self, pager: Pager = None, styles: bool = False, links: bool = False
+ ) -> PagerContext:
+ """A context manager to display anything printed within a "pager". The pager application
+ is defined by the system and will typically support at least pressing a key to scroll.
+
+ Args:
+ pager (Pager, optional): A pager object, or None to use :class:~rich.pager.SystemPager`. Defaults to None.
+ styles (bool, optional): Show styles in pager. Defaults to False.
+ links (bool, optional): Show links in pager. Defaults to False.
+
+ Example:
+ >>> from rich.console import Console
+ >>> from rich.__main__ import make_test_card
+ >>> console = Console()
+ >>> with console.pager():
+ console.print(make_test_card())
+
+ Returns:
+ PagerContext: A context manager.
+ """
+ return PagerContext(self, pager=pager, styles=styles, links=links)
+
+ def line(self, count: int = 1) -> None:
+ """Write new line(s).
+
+ Args:
+ count (int, optional): Number of new lines. Defaults to 1.
+ """
+
+ assert count >= 0, "count must be >= 0"
+ if count:
+ self._buffer.append(Segment("\n" * count))
+ self._check_buffer()
+
+ def clear(self, home: bool = True) -> None:
+ """Clear the screen.
+
+ Args:
+ home (bool, optional): Also move the cursor to 'home' position. Defaults to True.
+ """
+ self.control("\033[2J\033[H" if home else "\033[2J")
+
+ def status(
+ self,
+ status: RenderableType,
+ *,
+ spinner: str = "dots",
+ spinner_style: str = "status.spinner",
+ speed: float = 1.0,
+ refresh_per_second: float = 12.5,
+ ) -> "Status":
+ """Display a status and spinner.
+
+ Args:
+ status (RenderableType): A status renderable (str or Text typically).
+ console (Console, optional): Console instance to use, or None for global console. Defaults to None.
+ spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots".
+ spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner".
+ speed (float, optional): Speed factor for spinner animation. Defaults to 1.0.
+ refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5.
+
+ Returns:
+ Status: A Status object that may be used as a context manager.
+ """
+ from .status import Status
+
+ status_renderable = Status(
+ status,
+ console=self,
+ spinner=spinner,
+ spinner_style=spinner_style,
+ speed=speed,
+ refresh_per_second=refresh_per_second,
+ )
+ return status_renderable
+
+ def show_cursor(self, show: bool = True) -> bool:
+ """Show or hide the cursor.
+
+ Args:
+ show (bool, optional): Set visibility of the cursor.
+ """
+ if self.is_terminal and not self.legacy_windows:
+ self.control("\033[?25h" if show else "\033[?25l")
+ return True
+ return False
+
+ def set_alt_screen(self, enable: bool = True) -> bool:
+ """Enables alternative screen mode.
+
+ Note, if you enable this mode, you should ensure that is disabled before
+ the application exits. See :meth:`~rich.Console.screen` for a context manager
+ that handles this for you.
+
+ Args:
+ enable (bool, optional): Enable (True) or disable (False) alternate screen. Defaults to True.
+
+ Returns:
+ bool: True if the control codes were written.
+
+ """
+ changed = False
+ if self.is_terminal and not self.legacy_windows:
+ self.control("\033[?1049h\033[H" if enable else "\033[?1049l")
+ changed = True
+ return changed
+
+ def screen(
+ self, hide_cursor: bool = True, style: StyleType = None
+ ) -> "ScreenContext":
+ """Context manager to enable and disable 'alternative screen' mode.
+
+ Args:
+ hide_cursor (bool, optional): Also hide the cursor. Defaults to False.
+ style (Style, optional): Optional style for screen. Defaults to None.
+
+ Returns:
+ ~ScreenContext: Context which enables alternate screen on enter, and disables it on exit.
+ """
+ return ScreenContext(self, hide_cursor=hide_cursor, style=style or "")
+
+ def render(
+ self, renderable: RenderableType, options: ConsoleOptions = None
+ ) -> Iterable[Segment]:
+ """Render an object in to an iterable of `Segment` instances.
+
+ This method contains the logic for rendering objects with the console protocol.
+ You are unlikely to need to use it directly, unless you are extending the library.
+
+ Args:
+ renderable (RenderableType): An object supporting the console protocol, or
+ an object that may be converted to a string.
+ options (ConsoleOptions, optional): An options object, or None to use self.options. Defaults to None.
+
+ Returns:
+ Iterable[Segment]: An iterable of segments that may be rendered.
+ """
+
+ _options = options or self.options
+ if _options.max_width < 1:
+ # No space to render anything. This prevents potential recursion errors.
+ return
+ render_iterable: RenderResult
+ if isinstance(renderable, RichCast):
+ renderable = renderable.__rich__()
+ if isinstance(renderable, ConsoleRenderable):
+ render_iterable = renderable.__rich_console__(self, _options)
+ elif isinstance(renderable, str):
+ yield from self.render(
+ self.render_str(renderable, highlight=_options.highlight), _options
+ )
+ return
+ else:
+ raise errors.NotRenderableError(
+ f"Unable to render {renderable!r}; "
+ "A str, Segment or object with __rich_console__ method is required"
+ )
+
+ try:
+ iter_render = iter(render_iterable)
+ except TypeError:
+ raise errors.NotRenderableError(
+ f"object {render_iterable!r} is not renderable"
+ )
+ for render_output in iter_render:
+ if isinstance(render_output, Segment):
+ yield render_output
+ else:
+ yield from self.render(render_output, _options)
+
+ def render_lines(
+ self,
+ renderable: RenderableType,
+ options: Optional[ConsoleOptions] = None,
+ *,
+ style: Optional[Style] = None,
+ pad: bool = True,
+ ) -> List[List[Segment]]:
+ """Render objects in to a list of lines.
+
+ The output of render_lines is useful when further formatting of rendered console text
+ is required, such as the Panel class which draws a border around any renderable object.
+
+ Args:
+ renderable (RenderableType): Any object renderable in the console.
+ options (Optional[ConsoleOptions], optional): Console options, or None to use self.options. Default to ``None``.
+ style (Style, optional): Optional style to apply to renderables. Defaults to ``None``.
+ pad (bool, optional): Pad lines shorter than render width. Defaults to ``True``.
+ range (Optional[Tuple[int, int]], optional): Range of lines to render, or ``None`` for all line. Defaults to ``None``
+
+ Returns:
+ List[List[Segment]]: A list of lines, where a line is a list of Segment objects.
+ """
+ render_options = options or self.options
+ _rendered = self.render(renderable, render_options)
+ if style is not None:
+ _rendered = Segment.apply_style(_rendered, style)
+ lines = list(
+ Segment.split_and_crop_lines(
+ _rendered, render_options.max_width, include_new_lines=False, pad=pad
+ )
+ )
+ if render_options.height is not None:
+ lines = Segment.set_shape(
+ lines, render_options.max_width, render_options.height, style=style
+ )
+ return lines
+
+ def render_str(
+ self,
+ text: str,
+ *,
+ style: Union[str, Style] = "",
+ justify: JustifyMethod = None,
+ overflow: OverflowMethod = None,
+ emoji: bool = None,
+ markup: bool = None,
+ highlight: bool = None,
+ highlighter: HighlighterType = None,
+ ) -> "Text":
+ """Convert a string to a Text instance. This is is called automatically if
+ you print or log a string.
+
+ Args:
+ text (str): Text to render.
+ style (Union[str, Style], optional): Style to apply to rendered text.
+ justify (str, optional): Justify method: "default", "left", "center", "full", or "right". Defaults to ``None``.
+ overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to ``None``.
+ emoji (Optional[bool], optional): Enable emoji, or ``None`` to use Console default.
+ markup (Optional[bool], optional): Enable markup, or ``None`` to use Console default.
+ highlight (Optional[bool], optional): Enable highlighting, or ``None`` to use Console default.
+ highlighter (HighlighterType, optional): Optional highlighter to apply.
+ Returns:
+ ConsoleRenderable: Renderable object.
+
+ """
+ emoji_enabled = emoji or (emoji is None and self._emoji)
+ markup_enabled = markup or (markup is None and self._markup)
+ highlight_enabled = highlight or (highlight is None and self._highlight)
+
+ if markup_enabled:
+ rich_text = render_markup(text, style=style, emoji=emoji_enabled)
+ rich_text.justify = justify
+ rich_text.overflow = overflow
+ else:
+ rich_text = Text(
+ _emoji_replace(text) if emoji_enabled else text,
+ justify=justify,
+ overflow=overflow,
+ style=style,
+ )
+
+ _highlighter = (highlighter or self.highlighter) if highlight_enabled else None
+ if _highlighter is not None:
+ highlight_text = _highlighter(str(rich_text))
+ highlight_text.copy_styles(rich_text)
+ return highlight_text
+
+ return rich_text
+
+ def get_style(
+ self, name: Union[str, Style], *, default: Union[Style, str] = None
+ ) -> Style:
+ """Get a Style instance by it's theme name or parse a definition.
+
+ Args:
+ name (str): The name of a style or a style definition.
+
+ Returns:
+ Style: A Style object.
+
+ Raises:
+ MissingStyle: If no style could be parsed from name.
+
+ """
+ if isinstance(name, Style):
+ return name
+
+ try:
+ style = self._theme_stack.get(name)
+ if style is None:
+ style = Style.parse(name)
+ return style.copy() if style.link else style
+ except errors.StyleSyntaxError as error:
+ if default is not None:
+ return self.get_style(default)
+ raise errors.MissingStyle(
+ f"Failed to get style {name!r}; {error}"
+ ) from None
+
+ def _collect_renderables(
+ self,
+ objects: Iterable[Any],
+ sep: str,
+ end: str,
+ *,
+ justify: JustifyMethod = None,
+ emoji: bool = None,
+ markup: bool = None,
+ highlight: bool = None,
+ ) -> List[ConsoleRenderable]:
+ """Combine a number of renderables and text into one renderable.
+
+ Args:
+ objects (Iterable[Any]): Anything that Rich can render.
+ sep (str): String to write between print data.
+ end (str): String to write at end of print data.
+ justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
+ 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.
+ highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default.
+
+ Returns:
+ List[ConsoleRenderable]: A list of things to render.
+ """
+ renderables: List[ConsoleRenderable] = []
+ _append = renderables.append
+ text: List[Text] = []
+ append_text = text.append
+
+ append = _append
+ if justify in ("left", "center", "right"):
+
+ def align_append(renderable: RenderableType) -> None:
+ _append(Align(renderable, cast(AlignMethod, justify)))
+
+ append = align_append
+
+ _highlighter: HighlighterType = _null_highlighter
+ if highlight or (highlight is None and self._highlight):
+ _highlighter = self.highlighter
+
+ def check_text() -> None:
+ if text:
+ sep_text = Text(sep, justify=justify, end=end)
+ append(sep_text.join(text))
+ del text[:]
+
+ for renderable in objects:
+ # I promise this is sane
+ # This detects an object which claims to have all attributes, such as MagicMock.mock_calls
+ if hasattr(
+ renderable, "jwevpw_eors4dfo6mwo345ermk7kdnfnwerwer"
+ ): # pragma: no cover
+ renderable = repr(renderable)
+ rich_cast = getattr(renderable, "__rich__", None)
+ if rich_cast:
+ renderable = rich_cast()
+ if isinstance(renderable, str):
+ append_text(
+ self.render_str(
+ renderable, emoji=emoji, markup=markup, highlighter=_highlighter
+ )
+ )
+ elif isinstance(renderable, ConsoleRenderable):
+ check_text()
+ append(renderable)
+ elif isinstance(renderable, (abc.Mapping, abc.Sequence, abc.Set)):
+ check_text()
+ append(Pretty(renderable, highlighter=_highlighter))
+ else:
+ append_text(_highlighter(str(renderable)))
+
+ check_text()
+
+ if self.style is not None:
+ style = self.get_style(self.style)
+ renderables = [Styled(renderable, style) for renderable in renderables]
+
+ return renderables
+
+ def rule(
+ self,
+ title: TextType = "",
+ *,
+ characters: str = "─",
+ style: Union[str, Style] = "rule.line",
+ align: AlignMethod = "center",
+ ) -> None:
+ """Draw a line with optional centered title.
+
+ Args:
+ title (str, optional): Text to render over the rule. Defaults to "".
+ characters (str, optional): Character(s) to form the line. Defaults to "─".
+ style (str, optional): Style of line. Defaults to "rule.line".
+ align (str, optional): How to align the title, one of "left", "center", or "right". Defaults to "center".
+ """
+ from .rule import Rule
+
+ rule = Rule(title=title, characters=characters, style=style, align=align)
+ self.print(rule)
+
+ def control(self, control_codes: Union["Control", str]) -> None:
+ """Insert non-printing control codes.
+
+ Args:
+ control_codes (str): Control codes, such as those that may move the cursor.
+ """
+ if not self.is_dumb_terminal:
+ self._buffer.append(Segment.control(str(control_codes)))
+ self._check_buffer()
+
+ def out(
+ self,
+ *objects: Any,
+ sep=" ",
+ end="\n",
+ style: Union[str, Style] = None,
+ highlight: bool = None,
+ ) -> None:
+ """Output to the terminal. This is a low-level way of writing to the terminal which unlike
+ :meth:`~rich.console.Console.print` won't pretty print, wrap text, or apply markup, but will
+ optionally apply highlighting and a basic style.
+
+ Args:
+ 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".
+ style (Union[str, Style], optional): A style to apply to output. Defaults to None.
+ highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use
+ console default. Defaults to ``None``.
+ """
+ raw_output: str = sep.join(str(_object) for _object in objects)
+ self.print(
+ raw_output,
+ style=style,
+ highlight=highlight,
+ emoji=False,
+ markup=False,
+ no_wrap=True,
+ overflow="ignore",
+ crop=False,
+ end=end,
+ )
+
+ def print(
+ self,
+ *objects: Any,
+ sep=" ",
+ end="\n",
+ style: Union[str, Style] = None,
+ justify: JustifyMethod = None,
+ overflow: OverflowMethod = None,
+ no_wrap: bool = None,
+ emoji: bool = None,
+ markup: bool = None,
+ highlight: bool = None,
+ width: int = None,
+ height: int = None,
+ crop: bool = True,
+ soft_wrap: bool = None,
+ ) -> None:
+ """Print to the console.
+
+ Args:
+ objects (positional args): Objects to log to the terminal.
+ 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".
+ style (Union[str, Style], optional): A style to apply to output. Defaults to None.
+ justify (str, optional): Justify method: "default", "left", "right", "center", or "full". Defaults to ``None``.
+ overflow (str, optional): Overflow method: "ignore", "crop", "fold", or "ellipsis". Defaults to None.
+ no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to None.
+ 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. Defaults to ``None``.
+ highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to ``None``.
+ width (Optional[int], optional): Width of output, or ``None`` to auto-detect. Defaults to ``None``.
+ crop (Optional[bool], optional): Crop output to width of terminal. Defaults to True.
+ soft_wrap (bool, optional): Enable soft wrap mode which disables word wrapping and cropping of text or None for
+ Console default. Defaults to ``None``.
+ """
+ if not objects:
+ self.line()
+ return
+
+ if soft_wrap is None:
+ soft_wrap = self.soft_wrap
+ if soft_wrap:
+ if no_wrap is None:
+ no_wrap = True
+ if overflow is None:
+ overflow = "ignore"
+ crop = False
+
+ with self:
+ renderables = self._collect_renderables(
+ objects,
+ sep,
+ end,
+ justify=justify,
+ emoji=emoji,
+ markup=markup,
+ highlight=highlight,
+ )
+ for hook in self._render_hooks:
+ renderables = hook.process_renderables(renderables)
+ render_options = self.options.update(
+ justify="default",
+ overflow=overflow,
+ width=min(width, self.width) if width else NO_CHANGE,
+ height=height,
+ no_wrap=no_wrap,
+ )
+
+ new_segments: List[Segment] = []
+ extend = new_segments.extend
+ render = self.render
+ if style is None:
+ for renderable in renderables:
+ extend(render(renderable, render_options))
+ else:
+ for renderable in renderables:
+ extend(
+ Segment.apply_style(
+ render(renderable, render_options), self.get_style(style)
+ )
+ )
+ if crop:
+ buffer_extend = self._buffer.extend
+ for line in Segment.split_and_crop_lines(
+ new_segments, self.width, pad=False
+ ):
+ buffer_extend(line)
+ else:
+ self._buffer.extend(new_segments)
+
+ def print_exception(
+ self,
+ *,
+ width: Optional[int] = 100,
+ extra_lines: int = 3,
+ theme: Optional[str] = None,
+ word_wrap: bool = False,
+ show_locals: bool = False,
+ ) -> None:
+ """Prints a rich render of the last exception and traceback.
+
+ Args:
+ width (Optional[int], optional): Number of characters used to render code. Defaults to 88.
+ extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
+ theme (str, optional): Override pygments theme used in traceback
+ word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
+ show_locals (bool, optional): Enable display of local variables. Defaults to False.
+ """
+ from .traceback import Traceback
+
+ traceback = Traceback(
+ width=width,
+ extra_lines=extra_lines,
+ theme=theme,
+ word_wrap=word_wrap,
+ show_locals=show_locals,
+ )
+ self.print(traceback)
+
+ def log(
+ self,
+ *objects: Any,
+ sep=" ",
+ end="\n",
+ style: Union[str, Style] = None,
+ justify: JustifyMethod = None,
+ emoji: bool = None,
+ markup: bool = None,
+ highlight: bool = None,
+ log_locals: bool = False,
+ _stack_offset=1,
+ ) -> None:
+ """Log rich content to the terminal.
+
+ Args:
+ objects (positional args): Objects to log to the terminal.
+ 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".
+ style (Union[str, Style], optional): A style to apply to output. Defaults to None.
+ justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
+ overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None.
+ 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. Defaults to None.
+ 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()``
+ was called. Defaults to False.
+ _stack_offset (int, optional): Offset of caller from end of call stack. Defaults to 1.
+ """
+ if not objects:
+ self.line()
+ return
+ with self:
+ renderables = self._collect_renderables(
+ objects,
+ sep,
+ end,
+ justify=justify,
+ emoji=emoji,
+ markup=markup,
+ highlight=highlight,
+ )
+ if style is not None:
+ renderables = [Styled(renderable, style) for renderable in renderables]
+
+ caller = inspect.stack()[_stack_offset]
+ link_path = (
+ None
+ if caller.filename.startswith("<")
+ else os.path.abspath(caller.filename)
+ )
+ path = caller.filename.rpartition(os.sep)[-1]
+ line_no = caller.lineno
+ if log_locals:
+ locals_map = {
+ key: value
+ for key, value in caller.frame.f_locals.items()
+ if not key.startswith("__")
+ }
+ renderables.append(render_scope(locals_map, title="[i]locals"))
+
+ renderables = [
+ self._log_render(
+ self,
+ renderables,
+ log_time=self.get_datetime(),
+ path=path,
+ line_no=line_no,
+ link_path=link_path,
+ )
+ ]
+ for hook in self._render_hooks:
+ renderables = hook.process_renderables(renderables)
+ new_segments: List[Segment] = []
+ extend = new_segments.extend
+ render = self.render
+ render_options = self.options
+ for renderable in renderables:
+ extend(render(renderable, render_options))
+ buffer_extend = self._buffer.extend
+ for line in Segment.split_and_crop_lines(
+ new_segments, self.width, pad=False
+ ):
+ buffer_extend(line)
+
+ def _check_buffer(self) -> None:
+ """Check if the buffer may be rendered."""
+ if self.quiet:
+ del self._buffer[:]
+ return
+ with self._lock:
+ if self._buffer_index == 0:
+ if self.is_jupyter: # pragma: no cover
+ from .jupyter import display
+
+ display(self._buffer)
+ del self._buffer[:]
+ else:
+ text = self._render_buffer(self._buffer[:])
+ del self._buffer[:]
+ if text:
+ try:
+ if WINDOWS: # pragma: no cover
+ # https://bugs.python.org/issue37871
+ write = self.file.write
+ for line in text.splitlines(True):
+ write(line)
+ else:
+ self.file.write(text)
+ self.file.flush()
+ except UnicodeEncodeError as error:
+ error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
+ raise
+
+ def _render_buffer(self, buffer: Iterable[Segment]) -> str:
+ """Render buffered output, and clear buffer."""
+ output: List[str] = []
+ append = output.append
+ color_system = self._color_system
+ legacy_windows = self.legacy_windows
+ if self.record:
+ with self._record_buffer_lock:
+ self._record_buffer.extend(buffer)
+ not_terminal = not self.is_terminal
+ if self.no_color and color_system:
+ buffer = Segment.remove_color(buffer)
+ for text, style, is_control in buffer:
+ if style:
+ append(
+ style.render(
+ text,
+ color_system=color_system,
+ legacy_windows=legacy_windows,
+ )
+ )
+ elif not (not_terminal and is_control):
+ append(text)
+
+ rendered = "".join(output)
+ return rendered
+
+ def input(
+ self,
+ prompt: TextType = "",
+ *,
+ markup: bool = True,
+ emoji: bool = True,
+ password: bool = False,
+ stream: TextIO = None,
+ ) -> str:
+ """Displays a prompt and waits for input from the user. The prompt may contain color / style.
+
+ Args:
+ prompt (Union[str, Text]): Text to render in the prompt.
+ markup (bool, optional): Enable console markup (requires a str prompt). Defaults to True.
+ emoji (bool, optional): Enable emoji (requires a str prompt). Defaults to True.
+ password: (bool, optional): Hide typed text. Defaults to False.
+ stream: (TextIO, optional): Optional file to read input from (rather than stdin). Defaults to None.
+
+ Returns:
+ str: Text read from stdin.
+ """
+ prompt_str = ""
+ if prompt:
+ with self.capture() as capture:
+ self.print(prompt, markup=markup, emoji=emoji, end="")
+ prompt_str = capture.get()
+ if self.legacy_windows:
+ # Legacy windows doesn't like ANSI codes in getpass or input (colorama bug)?
+ self.file.write(prompt_str)
+ prompt_str = ""
+ if password:
+ result = getpass(prompt_str, stream=stream)
+ else:
+ if stream:
+ self.file.write(prompt_str)
+ result = stream.readline()
+ else:
+ result = input(prompt_str)
+ return result
+
+ def export_text(self, *, clear: bool = True, styles: bool = False) -> str:
+ """Generate text from console contents (requires record=True argument in constructor).
+
+ Args:
+ clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
+ styles (bool, optional): If ``True``, ansi escape codes will be included. ``False`` for plain text.
+ Defaults to ``False``.
+
+ Returns:
+ str: String containing console contents.
+
+ """
+ assert (
+ self.record
+ ), "To export console contents set record=True in the constructor or instance"
+
+ with self._record_buffer_lock:
+ if styles:
+ text = "".join(
+ (style.render(text) if style else text)
+ for text, style, _ in self._record_buffer
+ )
+ else:
+ text = "".join(
+ segment.text
+ for segment in self._record_buffer
+ if not segment.is_control
+ )
+ if clear:
+ del self._record_buffer[:]
+ return text
+
+ def save_text(self, path: str, *, clear: bool = True, styles: bool = False) -> None:
+ """Generate text from console and save to a given location (requires record=True argument in constructor).
+
+ Args:
+ path (str): Path to write text files.
+ clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
+ styles (bool, optional): If ``True``, ansi style codes will be included. ``False`` for plain text.
+ Defaults to ``False``.
+
+ """
+ text = self.export_text(clear=clear, styles=styles)
+ with open(path, "wt", encoding="utf-8") as write_file:
+ write_file.write(text)
+
+ def export_html(
+ self,
+ *,
+ theme: TerminalTheme = None,
+ clear: bool = True,
+ code_format: str = None,
+ inline_styles: bool = False,
+ ) -> str:
+ """Generate HTML from console contents (requires record=True argument in constructor).
+
+ Args:
+ theme (TerminalTheme, optional): TerminalTheme object containing console colors.
+ clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
+ code_format (str, optional): Format string to render HTML, should contain {foreground}
+ {background} and {code}.
+ inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
+ larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
+ Defaults to False.
+
+ Returns:
+ str: String containing console contents as HTML.
+ """
+ assert (
+ self.record
+ ), "To export console contents set record=True in the constructor or instance"
+ fragments: List[str] = []
+ append = fragments.append
+ _theme = theme or DEFAULT_TERMINAL_THEME
+ stylesheet = ""
+
+ def escape(text: str) -> str:
+ """Escape html."""
+ return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
+
+ render_code_format = CONSOLE_HTML_FORMAT if code_format is None else code_format
+
+ with self._record_buffer_lock:
+ if inline_styles:
+ for text, style, _ in Segment.filter_control(
+ Segment.simplify(self._record_buffer)
+ ):
+ text = escape(text)
+ if style:
+ rule = style.get_html_style(_theme)
+ text = f'<span style="{rule}">{text}</span>' if rule else text
+ if style.link:
+ text = f'<a href="{style.link}">{text}</a>'
+ append(text)
+ else:
+ styles: Dict[str, int] = {}
+ for text, style, _ in Segment.filter_control(
+ Segment.simplify(self._record_buffer)
+ ):
+ text = escape(text)
+ if style:
+ rule = style.get_html_style(_theme)
+ if rule:
+ style_number = styles.setdefault(rule, len(styles) + 1)
+ text = f'<span class="r{style_number}">{text}</span>'
+ if style.link:
+ text = f'<a href="{style.link}">{text}</a>'
+ append(text)
+ stylesheet_rules: List[str] = []
+ stylesheet_append = stylesheet_rules.append
+ for style_rule, style_number in styles.items():
+ if style_rule:
+ stylesheet_append(f".r{style_number} {{{style_rule}}}")
+ stylesheet = "\n".join(stylesheet_rules)
+
+ rendered_code = render_code_format.format(
+ code="".join(fragments),
+ stylesheet=stylesheet,
+ foreground=_theme.foreground_color.hex,
+ background=_theme.background_color.hex,
+ )
+ if clear:
+ del self._record_buffer[:]
+ return rendered_code
+
+ def save_html(
+ self,
+ path: str,
+ *,
+ theme: TerminalTheme = None,
+ clear: bool = True,
+ code_format=CONSOLE_HTML_FORMAT,
+ inline_styles: bool = False,
+ ) -> None:
+ """Generate HTML from console contents and write to a file (requires record=True argument in constructor).
+
+ Args:
+ path (str): Path to write html file.
+ theme (TerminalTheme, optional): TerminalTheme object containing console colors.
+ clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
+ code_format (str, optional): Format string to render HTML, should contain {foreground}
+ {background} and {code}.
+ inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
+ larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
+ Defaults to False.
+
+ """
+ html = self.export_html(
+ theme=theme,
+ clear=clear,
+ code_format=code_format,
+ inline_styles=inline_styles,
+ )
+ with open(path, "wt", encoding="utf-8") as write_file:
+ write_file.write(html)
+
+
+if __name__ == "__main__": # pragma: no cover
+ console = Console()
+
+ console.log(
+ "JSONRPC [i]request[/i]",
+ 5,
+ 1.3,
+ True,
+ False,
+ None,
+ {
+ "jsonrpc": "2.0",
+ "method": "subtract",
+ "params": {"minuend": 42, "subtrahend": 23},
+ "id": 3,
+ },
+ )
+
+ console.log("Hello, World!", "{'a': 1}", repr(console))
+
+ console.print(
+ {
+ "name": None,
+ "empty": [],
+ "quiz": {
+ "sport": {
+ "answered": True,
+ "q1": {
+ "question": "Which one is correct team name in NBA?",
+ "options": [
+ "New York Bulls",
+ "Los Angeles Kings",
+ "Golden State Warriors",
+ "Huston Rocket",
+ ],
+ "answer": "Huston Rocket",
+ },
+ },
+ "maths": {
+ "answered": False,
+ "q1": {
+ "question": "5 + 7 = ?",
+ "options": [10, 11, 12, 13],
+ "answer": 12,
+ },
+ "q2": {
+ "question": "12 - 8 = ?",
+ "options": [1, 2, 3, 4],
+ "answer": 4,
+ },
+ },
+ },
+ }
+ )
+ console.log("foo")
diff --git a/rich/constrain.py b/rich/constrain.py
new file mode 100644
index 0000000..c96233a
--- /dev/null
+++ b/rich/constrain.py
@@ -0,0 +1,35 @@
+from typing import Optional, TYPE_CHECKING
+
+from .jupyter import JupyterMixin
+from .measure import Measurement
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderableType, RenderResult
+
+
+class Constrain(JupyterMixin):
+ """Constrain the width of a renderable to a given number of characters.
+
+ Args:
+ renderable (RenderableType): A renderable object.
+ width (int, optional): The maximum width (in characters) to render. Defaults to 80.
+ """
+
+ def __init__(self, renderable: "RenderableType", width: Optional[int] = 80) -> None:
+ self.renderable = renderable
+ self.width = width
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ if self.width is None:
+ yield self.renderable
+ else:
+ child_options = options.update(width=min(self.width, options.max_width))
+ yield from console.render(self.renderable, child_options)
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
+ if self.width is not None:
+ max_width = min(self.width, max_width)
+ measurement = Measurement.get(console, self.renderable, max_width)
+ return measurement
diff --git a/rich/containers.py b/rich/containers.py
new file mode 100644
index 0000000..c42c814
--- /dev/null
+++ b/rich/containers.py
@@ -0,0 +1,161 @@
+from itertools import zip_longest
+from typing import (
+ Iterator,
+ Iterable,
+ List,
+ overload,
+ TypeVar,
+ TYPE_CHECKING,
+)
+
+if TYPE_CHECKING:
+ from .console import (
+ Console,
+ ConsoleOptions,
+ JustifyMethod,
+ OverflowMethod,
+ RenderResult,
+ RenderableType,
+ )
+ from .text import Text
+
+from .cells import cell_len
+from .measure import Measurement
+
+T = TypeVar("T")
+
+
+class Renderables:
+ """A list subclass which renders its contents to the console."""
+
+ def __init__(self, renderables: Iterable["RenderableType"] = None) -> None:
+ self._renderables: List["RenderableType"] = (
+ list(renderables) if renderables is not None else []
+ )
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ """Console render method to insert line-breaks."""
+ yield from self._renderables
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
+ dimensions = [
+ Measurement.get(console, renderable, max_width)
+ for renderable in self._renderables
+ ]
+ if not dimensions:
+ return Measurement(1, 1)
+ _min = max(dimension.minimum for dimension in dimensions)
+ _max = max(dimension.maximum for dimension in dimensions)
+ return Measurement(_min, _max)
+
+ def append(self, renderable: "RenderableType") -> None:
+ self._renderables.append(renderable)
+
+ def __iter__(self) -> Iterable["RenderableType"]:
+ return iter(self._renderables)
+
+
+class Lines:
+ """A list subclass which can render to the console."""
+
+ def __init__(self, lines: Iterable["Text"] = ()) -> None:
+ self._lines: List["Text"] = list(lines)
+
+ def __repr__(self) -> str:
+ return f"Lines({self._lines!r})"
+
+ def __iter__(self) -> Iterator["Text"]:
+ return iter(self._lines)
+
+ @overload
+ def __getitem__(self, index: int) -> "Text":
+ ...
+
+ @overload
+ def __getitem__(self, index: slice) -> "Lines":
+ ...
+
+ def __getitem__(self, index):
+ return self._lines[index]
+
+ def __setitem__(self, index: int, value: "Text") -> "Lines":
+ self._lines[index] = value
+ return self
+
+ def __len__(self) -> int:
+ return self._lines.__len__()
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ """Console render method to insert line-breaks."""
+ yield from self._lines
+
+ def append(self, line: "Text") -> None:
+ self._lines.append(line)
+
+ def extend(self, lines: Iterable["Text"]) -> None:
+ self._lines.extend(lines)
+
+ def pop(self, index=-1) -> "Text":
+ return self._lines.pop(index)
+
+ def justify(
+ self,
+ console: "Console",
+ width: int,
+ justify: "JustifyMethod" = "left",
+ overflow: "OverflowMethod" = "fold",
+ ) -> None:
+ """Justify and overflow text to a given width.
+
+ Args:
+ console (Console): Console instance.
+ width (int): Number of characters per line.
+ justify (str, optional): Default justify method for text: "left", "center", "full" or "right". Defaults to "left".
+ overflow (str, optional): Default overflow for text: "crop", "fold", or "ellipsis". Defaults to "fold".
+
+ """
+ from .text import Text
+
+ if justify == "left":
+ for line in self._lines:
+ line.truncate(width, overflow=overflow, pad=True)
+ elif justify == "center":
+ for line in self._lines:
+ line.rstrip()
+ line.truncate(width, overflow=overflow)
+ line.pad_left((width - cell_len(line.plain)) // 2)
+ line.pad_right(width - cell_len(line.plain))
+ elif justify == "right":
+ for line in self._lines:
+ line.rstrip()
+ line.truncate(width, overflow=overflow)
+ line.pad_left(width - cell_len(line.plain))
+ elif justify == "full":
+ for line_index, line in enumerate(self._lines):
+ if line_index == len(self._lines) - 1:
+ break
+ words = line.split(" ")
+ words_size = sum(cell_len(word.plain) for word in words)
+ num_spaces = len(words) - 1
+ spaces = [1 for _ in range(num_spaces)]
+ index = 0
+ if spaces:
+ while words_size + num_spaces < width:
+ spaces[len(spaces) - index - 1] += 1
+ num_spaces += 1
+ index = (index + 1) % len(spaces)
+ tokens: List[Text] = []
+ for index, (word, next_word) in enumerate(
+ zip_longest(words, words[1:])
+ ):
+ tokens.append(word)
+ if index < len(spaces):
+ style = word.get_style_at_offset(console, -1)
+ next_style = next_word.get_style_at_offset(console, 0)
+ space_style = style if style == next_style else line.style
+ tokens.append(Text(" " * spaces[index], style=space_style))
+ self[line_index] = Text("").join(tokens)
diff --git a/rich/control.py b/rich/control.py
new file mode 100644
index 0000000..f4cfd44
--- /dev/null
+++ b/rich/control.py
@@ -0,0 +1,57 @@
+from typing import TYPE_CHECKING
+
+from .segment import Segment
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderResult
+
+STRIP_CONTROL_CODES = [
+ 8, # Backspace
+ 11, # Vertical tab
+ 12, # Form feed
+ 13, # Carriage return
+]
+_CONTROL_TRANSLATE = {_codepoint: None for _codepoint in STRIP_CONTROL_CODES}
+
+
+class Control:
+ """A renderable that inserts a control code (non printable but may move cursor).
+
+ Args:
+ control_codes (str): A string containing control codes.
+ """
+
+ __slots__ = ["_control_codes"]
+
+ def __init__(self, control_codes: str) -> None:
+ self._control_codes = Segment.control(control_codes)
+
+ @classmethod
+ def home(cls) -> "Control":
+ """Move cursor to 'home' position."""
+ return cls("\033[H")
+
+ def __str__(self) -> str:
+ return self._control_codes.text
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ yield self._control_codes
+
+
+def strip_control_codes(text: str, _translate_table=_CONTROL_TRANSLATE) -> str:
+ """Remove control codes from text.
+
+ Args:
+ text (str): A string possibly contain control codes.
+
+ Returns:
+ str: String with control codes removed.
+ """
+ return text.translate(_translate_table)
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ print(strip_control_codes("hello\rWorld"))
diff --git a/rich/default_styles.py b/rich/default_styles.py
new file mode 100644
index 0000000..6413f8e
--- /dev/null
+++ b/rich/default_styles.py
@@ -0,0 +1,154 @@
+from typing import Dict
+
+from .style import Style
+
+DEFAULT_STYLES: Dict[str, Style] = {
+ "none": Style.null(),
+ "reset": Style(
+ color="default",
+ bgcolor="default",
+ dim=False,
+ bold=False,
+ italic=False,
+ underline=False,
+ blink=False,
+ blink2=False,
+ reverse=False,
+ conceal=False,
+ strike=False,
+ ),
+ "dim": Style(dim=True),
+ "bright": Style(dim=False),
+ "bold": Style(bold=True),
+ "strong": Style(bold=True),
+ "code": Style(reverse=True, bold=True),
+ "italic": Style(italic=True),
+ "emphasize": Style(italic=True),
+ "underline": Style(underline=True),
+ "blink": Style(blink=True),
+ "blink2": Style(blink2=True),
+ "reverse": Style(reverse=True),
+ "strike": Style(strike=True),
+ "black": Style(color="black"),
+ "red": Style(color="red"),
+ "green": Style(color="green"),
+ "yellow": Style(color="yellow"),
+ "magenta": Style(color="magenta"),
+ "cyan": Style(color="cyan"),
+ "white": Style(color="white"),
+ "inspect.attr": Style(color="yellow", italic=True),
+ "inspect.attr.dunder": Style(color="yellow", italic=True, dim=True),
+ "inspect.callable": Style(bold=True, color="red"),
+ "inspect.def": Style(italic=True, color="bright_cyan"),
+ "inspect.error": Style(bold=True, color="red"),
+ "inspect.equals": Style(),
+ "inspect.help": Style(color="cyan"),
+ "inspect.doc": Style(dim=True),
+ "inspect.value.border": Style(color="green"),
+ "live.ellipsis": Style(bold=True, color="red"),
+ "logging.keyword": Style(bold=True, color="yellow"),
+ "logging.level.notset": Style(dim=True),
+ "logging.level.debug": Style(color="green"),
+ "logging.level.info": Style(color="blue"),
+ "logging.level.warning": Style(color="red"),
+ "logging.level.error": Style(color="red", bold=True),
+ "logging.level.critical": Style(color="red", bold=True, reverse=True),
+ "log.level": Style.null(),
+ "log.time": Style(color="cyan", dim=True),
+ "log.message": Style.null(),
+ "log.path": Style(dim=True),
+ "repr.ellipsis": Style(color="yellow"),
+ "repr.indent": Style(color="green", dim=True),
+ "repr.error": Style(color="red", bold=True),
+ "repr.str": Style(color="green", italic=False, bold=False),
+ "repr.brace": Style(bold=True),
+ "repr.comma": Style(bold=True),
+ "repr.ipv4": Style(bold=True, color="bright_green"),
+ "repr.ipv6": Style(bold=True, color="bright_green"),
+ "repr.eui48": Style(bold=True, color="bright_green"),
+ "repr.eui64": Style(bold=True, color="bright_green"),
+ "repr.tag_start": Style(bold=True),
+ "repr.tag_name": Style(color="bright_magenta", bold=True),
+ "repr.tag_contents": Style(color="default"),
+ "repr.tag_end": Style(bold=True),
+ "repr.attrib_name": Style(color="yellow", italic=False),
+ "repr.attrib_equal": Style(bold=True),
+ "repr.attrib_value": Style(color="magenta", italic=False),
+ "repr.number": Style(color="blue", bold=True, italic=False),
+ "repr.bool_true": Style(color="bright_green", italic=True),
+ "repr.bool_false": Style(color="bright_red", italic=True),
+ "repr.none": Style(color="magenta", italic=True),
+ "repr.url": Style(underline=True, color="bright_blue", italic=False, bold=False),
+ "repr.uuid": Style(color="bright_yellow", bold=False),
+ "rule.line": Style(color="bright_green"),
+ "rule.text": Style.null(),
+ "prompt": Style.null(),
+ "prompt.choices": Style(color="magenta", bold=True),
+ "prompt.default": Style(color="cyan", bold=True),
+ "prompt.invalid": Style(color="red"),
+ "prompt.invalid.choice": Style(color="red"),
+ "pretty": Style.null(),
+ "scope.border": Style(color="blue"),
+ "scope.key": Style(color="yellow", italic=True),
+ "scope.key.special": Style(color="yellow", italic=True, dim=True),
+ "scope.equals": Style(color="red"),
+ "repr.path": Style(color="magenta"),
+ "repr.filename": Style(color="bright_magenta"),
+ "table.header": Style(bold=True),
+ "table.footer": Style(bold=True),
+ "table.cell": Style.null(),
+ "table.title": Style(italic=True),
+ "table.caption": Style(italic=True, dim=True),
+ "traceback.error": Style(color="red", italic=True),
+ "traceback.border.syntax_error": Style(color="bright_red"),
+ "traceback.border": Style(color="red"),
+ "traceback.text": Style.null(),
+ "traceback.title": Style(color="red", bold=True),
+ "traceback.exc_type": Style(color="bright_red", bold=True),
+ "traceback.exc_value": Style.null(),
+ "traceback.offset": Style(color="bright_red", bold=True),
+ "bar.back": Style(color="grey23"),
+ "bar.complete": Style(color="rgb(249,38,114)"),
+ "bar.finished": Style(color="rgb(114,156,31)"),
+ "bar.pulse": Style(color="rgb(249,38,114)"),
+ "progress.description": Style.null(),
+ "progress.filesize": Style(color="green"),
+ "progress.filesize.total": Style(color="green"),
+ "progress.download": Style(color="green"),
+ "progress.elapsed": Style(color="yellow"),
+ "progress.percentage": Style(color="magenta"),
+ "progress.remaining": Style(color="cyan"),
+ "progress.data.speed": Style(color="red"),
+ "progress.spinner": Style(color="green"),
+ "status.spinner": Style(color="green"),
+ "tree": Style(),
+ "tree.line": Style(),
+}
+
+MARKDOWN_STYLES = {
+ "markdown.paragraph": Style(),
+ "markdown.text": Style(),
+ "markdown.emph": Style(italic=True),
+ "markdown.strong": Style(bold=True),
+ "markdown.code": Style(bgcolor="black", color="bright_white"),
+ "markdown.code_block": Style(dim=True, color="cyan", bgcolor="black"),
+ "markdown.block_quote": Style(color="magenta"),
+ "markdown.list": Style(color="cyan"),
+ "markdown.item": Style(),
+ "markdown.item.bullet": Style(color="yellow", bold=True),
+ "markdown.item.number": Style(color="yellow", bold=True),
+ "markdown.hr": Style(color="yellow"),
+ "markdown.h1.border": Style(),
+ "markdown.h1": Style(bold=True),
+ "markdown.h2": Style(bold=True, underline=True),
+ "markdown.h3": Style(bold=True),
+ "markdown.h4": Style(bold=True, dim=True),
+ "markdown.h5": Style(underline=True),
+ "markdown.h6": Style(italic=True),
+ "markdown.h7": Style(italic=True, dim=True),
+ "markdown.link": Style(color="bright_blue"),
+ "markdown.link_url": Style(color="blue"),
+}
+
+
+DEFAULT_STYLES.update(MARKDOWN_STYLES)
diff --git a/rich/diagnose.py b/rich/diagnose.py
new file mode 100644
index 0000000..455e11d
--- /dev/null
+++ b/rich/diagnose.py
@@ -0,0 +1,6 @@
+if __name__ == "__main__": # pragma: no cover
+ from rich.console import Console
+ from rich import inspect
+
+ console = Console()
+ inspect(console)
diff --git a/rich/emoji.py b/rich/emoji.py
new file mode 100644
index 0000000..4c1e200
--- /dev/null
+++ b/rich/emoji.py
@@ -0,0 +1,74 @@
+from typing import Union
+
+from .console import Console, ConsoleOptions, RenderResult
+from .jupyter import JupyterMixin
+from .segment import Segment
+from .style import Style
+from ._emoji_codes import EMOJI
+from ._emoji_replace import _emoji_replace
+
+
+class NoEmoji(Exception):
+ """No emoji by that name."""
+
+
+class Emoji(JupyterMixin):
+ __slots__ = ["name", "style", "_char"]
+
+ def __init__(self, name: str, style: Union[str, Style] = "none") -> None:
+ """A single emoji character.
+
+ Args:
+ name (str): Name of emoji.
+ style (Union[str, Style], optional): Optional style. Defaults to None.
+
+ Raises:
+ NoEmoji: If the emoji doesn't exist.
+ """
+ self.name = name
+ self.style = style
+ try:
+ self._char = EMOJI[name]
+ except KeyError:
+ raise NoEmoji(f"No emoji called {name!r}")
+
+ @classmethod
+ def replace(cls, text: str) -> str:
+ """Replace emoji markup with corresponding unicode characters.
+
+ Args:
+ text (str): A string with emojis codes, e.g. "Hello :smiley:!"
+
+ Returns:
+ str: A string with emoji codes replaces with actual emoji.
+ """
+ return _emoji_replace(text)
+
+ def __repr__(self) -> str:
+ return f"<emoji {self.name!r}>"
+
+ def __str__(self) -> str:
+ return self._char
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ yield Segment(self._char, console.get_style(self.style))
+
+
+if __name__ == "__main__": # pragma: no cover
+ import sys
+
+ from rich.columns import Columns
+ from rich.console import Console
+
+ console = Console(record=True)
+
+ columns = Columns(
+ (f":{name}: {name}" for name in sorted(EMOJI.keys()) if "\u200D" not in name),
+ column_first=True,
+ )
+
+ console.print(columns)
+ if len(sys.argv) > 1:
+ console.save_html(sys.argv[1])
diff --git a/rich/errors.py b/rich/errors.py
new file mode 100644
index 0000000..e295fbc
--- /dev/null
+++ b/rich/errors.py
@@ -0,0 +1,30 @@
+class ConsoleError(Exception):
+ """An error in console operation."""
+
+
+class StyleError(Exception):
+ """An error in styles."""
+
+
+class StyleSyntaxError(ConsoleError):
+ """Style was badly formatted."""
+
+
+class MissingStyle(StyleError):
+ """No such style."""
+
+
+class StyleStackError(ConsoleError):
+ """Style stack is invalid."""
+
+
+class NotRenderableError(ConsoleError):
+ """Object is not renderable."""
+
+
+class MarkupError(ConsoleError):
+ """Markup was badly formatted."""
+
+
+class LiveError(ConsoleError):
+ """Error related to Live display."""
diff --git a/rich/file_proxy.py b/rich/file_proxy.py
new file mode 100644
index 0000000..99a6922
--- /dev/null
+++ b/rich/file_proxy.py
@@ -0,0 +1,54 @@
+import io
+from typing import List, Any, IO, TYPE_CHECKING
+
+from .ansi import AnsiDecoder
+from .text import Text
+
+if TYPE_CHECKING:
+ from .console import Console
+
+
+class FileProxy(io.TextIOBase):
+ """Wraps a file (e.g. sys.stdout) and redirects writes to a console."""
+
+ def __init__(self, console: "Console", file: IO[str]) -> None:
+ self.__console = console
+ self.__file = file
+ self.__buffer: List[str] = []
+ self.__ansi_decoder = AnsiDecoder()
+
+ @property
+ def rich_proxied_file(self) -> IO[str]:
+ """Get proxied file."""
+ return self.__file
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self.__file, name)
+
+ def write(self, text: str) -> int:
+ if not isinstance(text, str):
+ raise TypeError(f"write() argument must be str, not {type(text).__name__}")
+ buffer = self.__buffer
+ lines: List[str] = []
+ while text:
+ line, new_line, text = text.partition("\n")
+ if new_line:
+ lines.append("".join(buffer) + line)
+ del buffer[:]
+ else:
+ buffer.append(line)
+ break
+ if lines:
+ console = self.__console
+ with console:
+ output = Text("\n").join(
+ self.__ansi_decoder.decode_line(line) for line in lines
+ )
+ console.print(output, markup=False, emoji=False, highlight=False)
+ return len(text)
+
+ def flush(self) -> None:
+ buffer = self.__buffer
+ if buffer:
+ self.__console.print("".join(buffer))
+ del buffer[:]
diff --git a/rich/filesize.py b/rich/filesize.py
new file mode 100644
index 0000000..4876823
--- /dev/null
+++ b/rich/filesize.py
@@ -0,0 +1,62 @@
+# coding: utf-8
+"""Functions for reporting filesizes. Borrowed from https://github.com/PyFilesystem/pyfilesystem2
+
+The functions declared in this module should cover the different
+usecases needed to generate a string representation of a file size
+using several different units. Since there are many standards regarding
+file size units, three different functions have been implemented.
+
+See Also:
+ * `Wikipedia: Binary prefix <https://en.wikipedia.org/wiki/Binary_prefix>`_
+
+"""
+
+__all__ = ["decimal"]
+
+from typing import Iterable, List, Tuple
+
+
+def _to_str(size: int, suffixes: Iterable[str], base: int) -> str:
+ if size == 1:
+ return "1 byte"
+ elif size < base:
+ return "{:,} bytes".format(size)
+
+ for i, suffix in enumerate(suffixes, 2): # noqa: B007
+ unit = base ** i
+ if size < unit:
+ break
+ return "{:,.1f} {}".format((base * size / unit), suffix)
+
+
+def pick_unit_and_suffix(size: int, suffixes: List[str], base: int) -> Tuple[int, str]:
+ """Pick a suffix and base for the given size."""
+ for i, suffix in enumerate(suffixes):
+ unit = base ** i
+ if size < unit * base:
+ break
+ return unit, suffix
+
+
+def decimal(size: int) -> str:
+ """Convert a filesize in to a string (powers of 1000, SI prefixes).
+
+ In this convention, ``1000 B = 1 kB``.
+
+ This is typically the format used to advertise the storage
+ capacity of USB flash drives and the like (*256 MB* meaning
+ actually a storage capacity of more than *256 000 000 B*),
+ or used by **Mac OS X** since v10.6 to report file sizes.
+
+ Arguments:
+ int (size): A file size.
+
+ Returns:
+ `str`: A string containing a abbreviated file size and units.
+
+ Example:
+ >>> filesize.decimal(30000)
+ '30.0 kB'
+
+ """
+ return _to_str(size, ("kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"), 1000)
diff --git a/rich/highlighter.py b/rich/highlighter.py
new file mode 100644
index 0000000..37b36f6
--- /dev/null
+++ b/rich/highlighter.py
@@ -0,0 +1,131 @@
+from abc import ABC, abstractmethod
+from typing import List, Union
+
+from .text import Text
+
+
+def _combine_regex(*regexes: str) -> str:
+ """Combine a number of regexes in to a single regex.
+
+ Returns:
+ str: New regex with all regexes ORed together.
+ """
+ return "|".join(regexes)
+
+
+class Highlighter(ABC):
+ """Abstract base class for highlighters."""
+
+ def __call__(self, text: Union[str, Text]) -> Text:
+ """Highlight a str or Text instance.
+
+ Args:
+ text (Union[str, ~Text]): Text to highlight.
+
+ Raises:
+ TypeError: If not called with text or str.
+
+ Returns:
+ Text: A test instance with highlighting applied.
+ """
+ if isinstance(text, str):
+ highlight_text = Text(text)
+ elif isinstance(text, Text):
+ highlight_text = text.copy()
+ else:
+ raise TypeError(f"str or Text instance required, not {text!r}")
+ self.highlight(highlight_text)
+ return highlight_text
+
+ @abstractmethod
+ def highlight(self, text: Text) -> None:
+ """Apply highlighting in place to text.
+
+ Args:
+ text (~Text): A text object highlight.
+ """
+
+
+class NullHighlighter(Highlighter):
+ """A highlighter object that doesn't highlight.
+
+ May be used to disable highlighting entirely.
+
+ """
+
+ def highlight(self, text: Text) -> None:
+ """Nothing to do"""
+
+
+class RegexHighlighter(Highlighter):
+ """Applies highlighting from a list of regular expressions."""
+
+ highlights: List[str] = []
+ base_style: str = ""
+
+ def highlight(self, text: Text) -> None:
+ """Highlight :class:`rich.text.Text` using regular expressions.
+
+ Args:
+ text (~Text): Text to highlighted.
+
+ """
+
+ highlight_regex = text.highlight_regex
+ for re_highlight in self.highlights:
+ highlight_regex(re_highlight, style_prefix=self.base_style)
+
+
+class ReprHighlighter(RegexHighlighter):
+ """Highlights the text typically produced from ``__repr__`` methods."""
+
+ base_style = "repr."
+ highlights = [
+ r"(?P<tag_start>\<)(?P<tag_name>[\w\-\.\:]*)(?P<tag_contents>.*?)(?P<tag_end>\>)",
+ r"(?P<attrib_name>[\w_]{1,50})=(?P<attrib_value>\"?[\w_]+\"?)?",
+ _combine_regex(
+ r"(?P<ipv4>[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})",
+ r"(?P<ipv6>([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})",
+ r"(?P<eui64>(?:[0-9A-Fa-f]{1,2}-){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){3}[0-9A-Fa-f]{4})",
+ r"(?P<eui48>(?:[0-9A-Fa-f]{1,2}-){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4})",
+ r"(?P<brace>[\{\[\(\)\]\}])",
+ r"(?P<bool_true>True)|(?P<bool_false>False)|(?P<none>None)",
+ r"(?P<ellipsis>\.\.\.)",
+ r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[\-\+]?\d+?)?\b|0x[0-9a-fA-F]*)",
+ r"(?P<path>\B(\/[\w\.\-\_\+]+)*\/)(?P<filename>[\w\.\-\_\+]*)?",
+ r"(?<!\\)(?P<str>b?\'\'\'.*?(?<!\\)\'\'\'|b?\'.*?(?<!\\)\'|b?\"\"\".*?(?<!\\)\"\"\"|b?\".*?(?<!\\)\")",
+ r"(?P<uuid>[a-fA-F0-9]{8}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{12})",
+ r"(?P<url>https?:\/\/[0-9a-zA-Z\$\-\_\+\!`\(\)\,\.\?\/\;\:\&\=\%\#]*)",
+ ),
+ ]
+
+
+if __name__ == "__main__": # pragma: no cover
+ from .console import Console
+
+ console = Console()
+ console.print("[bold green]hello world![/bold green]")
+ console.print("'[bold green]hello world![/bold green]'")
+
+ console.print(" /foo")
+ console.print("/foo/")
+ console.print("/foo/bar")
+ console.print("foo/bar/baz")
+
+ console.print("/foo/bar/baz?foo=bar+egg&egg=baz")
+ console.print("/foo/bar/baz/")
+ console.print("/foo/bar/baz/egg")
+ console.print("/foo/bar/baz/egg.py")
+ console.print("/foo/bar/baz/egg.py word")
+ console.print(" /foo/bar/baz/egg.py word")
+ console.print("foo /foo/bar/baz/egg.py word")
+ console.print("foo /foo/bar/ba._++z/egg+.py word")
+ console.print("https://example.org?foo=bar#header")
+
+ console.print(1234567.34)
+ console.print(1 / 2)
+ console.print(-1 / 123123123123)
+
+ console.print(
+ "127.0.1.1 bar 192.168.1.4 2001:0db8:85a3:0000:0000:8a2e:0370:7334 foo"
+ )
diff --git a/rich/jupyter.py b/rich/jupyter.py
new file mode 100644
index 0000000..f76380f
--- /dev/null
+++ b/rich/jupyter.py
@@ -0,0 +1,79 @@
+from typing import Iterable, List, TYPE_CHECKING
+
+from . import get_console
+from .segment import Segment
+from .terminal_theme import DEFAULT_TERMINAL_THEME
+
+if TYPE_CHECKING:
+ from .console import RenderableType
+
+JUPYTER_HTML_FORMAT = """\
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace">{code}</pre>
+"""
+
+
+class JupyterRenderable:
+ """A shim to write html to Jupyter notebook."""
+
+ def __init__(self, html: str) -> None:
+ self.html = html
+
+ @classmethod
+ def render(cls, rich_renderable: "RenderableType") -> str:
+ console = get_console()
+ segments = console.render(rich_renderable, console.options)
+ html = _render_segments(segments)
+ return html
+
+ def _repr_html_(self) -> str:
+ return self.html
+
+
+class JupyterMixin:
+ """Add to an Rich renderable to make it render in Jupyter notebook."""
+
+ def _repr_html_(self) -> str:
+ console = get_console()
+ segments = list(console.render(self, console.options)) # type: ignore
+ html = _render_segments(segments)
+ return html
+
+
+def _render_segments(segments: Iterable[Segment]) -> str:
+ def escape(text: str) -> str:
+ """Escape html."""
+ return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
+
+ fragments: List[str] = []
+ append_fragment = fragments.append
+ theme = DEFAULT_TERMINAL_THEME
+ for text, style, is_control in Segment.simplify(segments):
+ if is_control:
+ continue
+ text = escape(text)
+ if style:
+ rule = style.get_html_style(theme)
+ text = f'<span style="{rule}">{text}</span>' if rule else text
+ if style.link:
+ text = f'<a href="{style.link}">{text}</a>'
+ append_fragment(text)
+
+ code = "".join(fragments)
+ html = JUPYTER_HTML_FORMAT.format(code=code)
+
+ return html
+
+
+def display(segments: Iterable[Segment]) -> None:
+ """Render segments to Jupyter."""
+ from IPython.display import display as ipython_display
+
+ html = _render_segments(segments)
+ jupyter_renderable = JupyterRenderable(html)
+ ipython_display(jupyter_renderable)
+
+
+def print(*args, **kwargs) -> None:
+ """Proxy for Console print."""
+ console = get_console()
+ return console.print(*args, **kwargs)
diff --git a/rich/layout.py b/rich/layout.py
new file mode 100644
index 0000000..c528435
--- /dev/null
+++ b/rich/layout.py
@@ -0,0 +1,238 @@
+from .align import Align
+from .console import Console, ConsoleOptions, RenderResult, RenderableType
+from .highlighter import ReprHighlighter
+from .panel import Panel
+from .pretty import Pretty
+from ._ratio import ratio_resolve
+from .segment import Segment
+from .style import StyleType
+
+
+from typing import List, Optional, TYPE_CHECKING
+from typing_extensions import Literal
+
+Direction = Literal["horizontal", "vertical"]
+
+
+if TYPE_CHECKING:
+ from rich.tree import Tree
+
+
+class _Placeholder:
+ """An internal renderable used as a Layout placeholder."""
+
+ highlighter = ReprHighlighter()
+
+ def __init__(self, layout: "Layout", style: StyleType = "") -> None:
+ self.layout = layout
+ self.style = style
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ width = options.max_width
+ height = options.height or options.size.height
+ layout = self.layout
+
+ layout_info = {
+ "size": layout.size,
+ "minimum_size": layout.minimum_size,
+ "ratio": layout.ratio,
+ "name": layout.name,
+ }
+
+ title = (
+ f"{layout.name!r} ({width} x {height})"
+ if layout.name
+ else f"({width} x {height})"
+ )
+ yield Panel(
+ Align.center(Pretty(layout_info), vertical="middle"),
+ style=self.style,
+ title=self.highlighter(title),
+ border_style="blue",
+ )
+
+
+class Layout:
+ """A renderable to divide a fixed height in to rows or columns.
+
+ Args:
+ renderable (RenderableType, optional): Renderable content, or None for placeholder. Defaults to None.
+ direction (str, optional): Direction of split, one of "vertical" or "horizontal". Defaults to "vertical".
+ size (int, optional): Optional fixed size of layout. Defaults to None.
+ minimum_size (int, optional): Minimum size of layout. Defaults to 1.
+ ratio (int, optional): Optional ratio for flexible layout. Defaults to 1.
+ name (str, optional): Optional identifier for Layout. Defaults to None.
+ visible (bool, optional): Visibility of layout. Defaults to True.
+ """
+
+ def __init__(
+ self,
+ renderable: RenderableType = None,
+ *,
+ direction: str = "vertical",
+ size: int = None,
+ minimum_size: int = 1,
+ ratio: int = 1,
+ name: str = None,
+ visible: bool = True,
+ ) -> None:
+ self._renderable = renderable or _Placeholder(self)
+ self.direction = direction
+ self.size = size
+ self.minimum_size = minimum_size
+ self.ratio = ratio
+ self.name = name
+ self.visible = visible
+ self._children: List[Layout] = []
+
+ def __repr__(self) -> str:
+ return f"Layout(size={self.size!r}, minimum_size={self.size!r}, ratio={self.ratio!r}, name={self.name!r}, visible={self.visible!r})"
+
+ @property
+ def renderable(self) -> RenderableType:
+ """Layout renderable."""
+ return self if self._children else self._renderable
+
+ @property
+ def children(self) -> List["Layout"]:
+ """Gets (visible) layout children."""
+ return [child for child in self._children if child.visible]
+
+ def get(self, name: str) -> Optional["Layout"]:
+ """Get a named layout, or None if it doesn't exist.
+
+ Args:
+ name (str): Name of layout.
+
+ Returns:
+ Optional[Layout]: Layout instance or None if no layout was found.
+ """
+ if self.name == name:
+ return self
+ else:
+ for child in self._children:
+ named_layout = child.get(name)
+ if named_layout is not None:
+ return named_layout
+ return None
+
+ def __getitem__(self, name: str) -> "Layout":
+ layout = self.get(name)
+ if layout is None:
+ raise KeyError(f"No layout with name {name!r}")
+ return layout
+
+ @property
+ def tree(self) -> "Tree":
+ """Get a tree renderable to show layout structure."""
+ from rich.highlighter import ReprHighlighter
+ from rich.text import Text
+ from rich.tree import Tree
+
+ highlighter = ReprHighlighter()
+
+ def summary(layout) -> "Text":
+ name = repr(layout.name) + " " if layout.name else ""
+ direction = (
+ ("➡" if layout.direction == "horizontal" else "⬇")
+ if layout._children
+ else "■"
+ )
+ if layout.size:
+ _summary = highlighter(f"{direction} {name}(size={layout.size})")
+ else:
+ _summary = highlighter(f"{direction} {name}(ratio={layout.ratio})")
+ _summary.stylize("" if layout.visible else "dim")
+ return _summary
+
+ layout = self
+ tree = Tree(summary(layout), highlight=True)
+
+ def recurse(tree, layout):
+ for child in layout._children:
+ recurse(tree.add(summary(child)), child)
+
+ recurse(tree, self)
+ return tree
+
+ def split(self, *layouts, direction: Direction = None) -> None:
+ """Split the layout in to multiple sub-layours.
+
+ Args:
+ *layouts (Layout): Positional arguments should be (sub) Layout instances.
+ direction (Direction, optional): One of "horizontal" or "vertical" or None for no change. Defaults to None.
+ """
+ if direction is not None:
+ self.direction = direction
+ self._children.extend(layouts)
+
+ def update(self, renderable: RenderableType) -> None:
+ """Update renderable.
+
+ Args:
+ renderable (RenderableType): New renderable object.
+ """
+ self._renderable = renderable
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ options = options.update(height=options.height or options.size.height)
+ if not self.children:
+ yield from console.render(self._renderable or "", options)
+ elif self.direction == "vertical":
+ yield from self._render_vertical(console, options)
+ elif self.direction == "horizontal":
+ yield from self._render_horizontal(console, options)
+
+ def _render_horizontal(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ render_widths = ratio_resolve(options.max_width, self.children)
+ renders = [
+ console.render_lines(child, options.update(width=render_width))
+ for child, render_width in zip(self.children, render_widths)
+ ]
+ new_line = Segment.line()
+ for lines in zip(*renders):
+ for line in lines:
+ yield from line
+ yield new_line
+
+ def _render_vertical(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ render_heights = ratio_resolve(options.height or console.height, self.children)
+ renders = [
+ console.render_lines(child.renderable, options.update(height=render_height))
+ for child, render_height in zip(self.children, render_heights)
+ ]
+ new_line = Segment.line()
+ for render in renders:
+ for line in render:
+ yield from line
+ yield new_line
+
+
+if __name__ == "__main__": # type: ignore
+ from rich.console import Console
+ from rich.panel import Panel
+
+ console = Console()
+ layout = Layout()
+
+ layout.split(
+ Layout(name="header", size=3),
+ Layout(ratio=1, name="main"),
+ Layout(size=10, name="footer"),
+ )
+
+ layout["main"].split(
+ Layout(name="side"), Layout(name="body", ratio=2), direction="horizontal"
+ )
+
+ layout["side"].split(Layout(), Layout())
+
+ console.print(layout)
diff --git a/rich/live.py b/rich/live.py
new file mode 100644
index 0000000..696c2c7
--- /dev/null
+++ b/rich/live.py
@@ -0,0 +1,366 @@
+import sys
+from threading import Event, RLock, Thread
+from typing import IO, Any, Callable, List, Optional
+
+from . import get_console
+from .console import Console, ConsoleRenderable, RenderableType, RenderHook
+from .control import Control
+from .file_proxy import FileProxy
+from .jupyter import JupyterMixin
+from .live_render import LiveRender, VerticalOverflowMethod
+from .screen import Screen
+from .text import Text
+
+
+class _RefreshThread(Thread):
+ """A thread that calls refresh() at regular intervals."""
+
+ def __init__(self, live: "Live", refresh_per_second: float) -> None:
+ self.live = live
+ self.refresh_per_second = refresh_per_second
+ self.done = Event()
+ super().__init__(daemon=True)
+
+ def stop(self) -> None:
+ self.done.set()
+
+ def run(self) -> None:
+ while not self.done.wait(1 / self.refresh_per_second):
+ with self.live._lock:
+ if not self.done.is_set():
+ self.live.refresh()
+
+
+class Live(JupyterMixin, RenderHook):
+ """Renders an auto-updating live display of any given renderable.
+
+ Args:
+ renderable (RenderableType, optional): The renderable to live display. Defaults to displaying nothing.
+ console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout.
+ screen (bool, optional): Enable alternate screen mode. Defaults to False.
+ auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()` or `update()` with refresh flag. Defaults to True
+ refresh_per_second (float, optional): Number of times per second to refresh the live display. Defaults to 1.
+ transient (bool, optional): Clear the renderable on exit. Defaults to False.
+ redirect_stdout (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True.
+ redirect_stderr (bool, optional): Enable redirection of stderr. Defaults to True.
+ vertical_overflow (VerticalOverflowMethod, optional): How to handle renderable when it is too tall for the console. Defaults to "ellipsis".
+ get_renderable (Callable[[], RenderableType], optional): Optional callable to get renderable. Defaults to None.
+ """
+
+ def __init__(
+ self,
+ renderable: RenderableType = None,
+ *,
+ console: Console = None,
+ screen: bool = False,
+ auto_refresh: bool = True,
+ refresh_per_second: float = 4,
+ transient: bool = False,
+ redirect_stdout: bool = True,
+ redirect_stderr: bool = True,
+ vertical_overflow: VerticalOverflowMethod = "ellipsis",
+ get_renderable: Callable[[], RenderableType] = None,
+ ) -> None:
+ assert refresh_per_second > 0, "refresh_per_second must be > 0"
+ self._renderable = renderable
+ self.console = console if console is not None else get_console()
+ self._screen = screen
+ self._alt_screen = False
+
+ self._redirect_stdout = redirect_stdout
+ self._redirect_stderr = redirect_stderr
+ self._restore_stdout: Optional[IO[str]] = None
+ self._restore_stderr: Optional[IO[str]] = None
+
+ self._lock = RLock()
+ self.ipy_widget: Optional[Any] = None
+ self.auto_refresh = auto_refresh
+ self._started: bool = False
+ self.transient = transient
+
+ self._refresh_thread: Optional[_RefreshThread] = None
+ self.refresh_per_second = refresh_per_second
+
+ self.vertical_overflow = vertical_overflow
+ self._get_renderable = get_renderable
+ self._live_render = LiveRender(
+ self.get_renderable(), vertical_overflow=vertical_overflow
+ )
+ # cant store just clear_control as the live_render shape is lazily computed on render
+
+ def get_renderable(self) -> RenderableType:
+ renderable = (
+ self._get_renderable()
+ if self._get_renderable is not None
+ else self._renderable
+ )
+ return renderable or ""
+
+ def start(self, refresh=False) -> None:
+ """Start live rendering display.
+
+ Args:
+ refresh (bool, optional): Also refresh. Defaults to False.
+ """
+ with self._lock:
+ if self._started:
+ return
+ self.console.set_live(self)
+ self._started = True
+ if self._screen:
+ self._alt_screen = self.console.set_alt_screen(True)
+ self.console.show_cursor(False)
+ self._enable_redirect_io()
+ self.console.push_render_hook(self)
+ if refresh:
+ self.refresh()
+ if self.auto_refresh:
+ self._refresh_thread = _RefreshThread(self, self.refresh_per_second)
+ self._refresh_thread.start()
+
+ def stop(self) -> None:
+ """Stop live rendering display."""
+ with self._lock:
+ if not self._started:
+ return
+ self.console.clear_live()
+ self._started = False
+ try:
+ if self.auto_refresh and self._refresh_thread is not None:
+ self._refresh_thread.stop()
+ # allow it to fully render on the last even if overflow
+ self.vertical_overflow = "visible"
+ if not self._alt_screen:
+ if not self.console.is_jupyter:
+ self.refresh()
+ if self.console.is_terminal:
+ self.console.line()
+ finally:
+ self._disable_redirect_io()
+ self.console.pop_render_hook()
+ self.console.show_cursor(True)
+ if self._alt_screen:
+ self.console.set_alt_screen(False)
+
+ if self._refresh_thread is not None:
+ self._refresh_thread.join()
+ self._refresh_thread = None
+ if self.transient and not self._screen:
+ self.console.control(self._live_render.restore_cursor())
+ if self.ipy_widget is not None: # pragma: no cover
+ if self.transient:
+ self.ipy_widget.close()
+ else:
+ # jupyter last refresh must occur after console pop render hook
+ # i am not sure why this is needed
+ self.refresh()
+
+ def __enter__(self) -> "Live":
+ self.start(refresh=self._renderable is not None)
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+ self.stop()
+
+ def _enable_redirect_io(self):
+ """Enable redirecting of stdout / stderr."""
+ if self.console.is_terminal:
+ if self._redirect_stdout and not isinstance(sys.stdout, FileProxy): # type: ignore
+ self._restore_stdout = sys.stdout
+ sys.stdout = FileProxy(self.console, sys.stdout)
+ if self._redirect_stderr and not isinstance(sys.stderr, FileProxy): # type: ignore
+ self._restore_stderr = sys.stderr
+ sys.stderr = FileProxy(self.console, sys.stderr)
+
+ def _disable_redirect_io(self):
+ """Disable redirecting of stdout / stderr."""
+ if self._restore_stdout:
+ sys.stdout = self._restore_stdout
+ self._restore_stdout = None
+ if self._restore_stderr:
+ sys.stderr = self._restore_stderr
+ self._restore_stderr = None
+
+ @property
+ def renderable(self) -> RenderableType:
+ """Get the renderable that is being displayed
+
+ Returns:
+ RenderableType: Displayed renderable.
+ """
+ renderable = self.get_renderable()
+ return Screen(renderable) if self._alt_screen else renderable
+
+ def update(self, renderable: RenderableType, *, refresh: bool = False) -> None:
+ """Update the renderable that is being displayed
+
+ Args:
+ renderable (RenderableType): New renderable to use.
+ refresh (bool, optional): Refresh the display. Defaults to False.
+ """
+ with self._lock:
+ self._renderable = renderable
+ if refresh:
+ self.refresh()
+
+ def refresh(self) -> None:
+ """Update the display of the Live Render."""
+ self._live_render.set_renderable(self.renderable)
+ if self.console.is_jupyter: # pragma: no cover
+ try:
+ from IPython.display import display
+ from ipywidgets import Output
+ except ImportError:
+ import warnings
+
+ warnings.warn('install "ipywidgets" for Jupyter support')
+ else:
+ with self._lock:
+ if self.ipy_widget is None:
+ self.ipy_widget = Output()
+ display(self.ipy_widget)
+
+ with self.ipy_widget:
+ self.ipy_widget.clear_output(wait=True)
+ self.console.print(self._live_render.renderable)
+ elif self.console.is_terminal and not self.console.is_dumb_terminal:
+ with self._lock, self.console:
+ self.console.print(Control(""))
+ elif (
+ not self._started and not self.transient
+ ): # if it is finished allow files or dumb-terminals to see final result
+ with self.console:
+ self.console.print(Control(""))
+
+ def process_renderables(
+ self, renderables: List[ConsoleRenderable]
+ ) -> List[ConsoleRenderable]:
+ """Process renderables to restore cursor and display progress."""
+ self._live_render.vertical_overflow = self.vertical_overflow
+ if self.console.is_interactive:
+ # lock needs acquiring as user can modify live_render renderable at any time unlike in Progress.
+ with self._lock:
+ # determine the control command needed to clear previous rendering
+ reset = (
+ Control.home()
+ if self._alt_screen
+ else self._live_render.position_cursor()
+ )
+ renderables = [
+ reset,
+ *renderables,
+ self._live_render,
+ ]
+ elif (
+ not self._started and not self.transient
+ ): # if it is finished render the final output for files or dumb_terminals
+ renderables = [*renderables, self._live_render]
+
+ return renderables
+
+
+if __name__ == "__main__": # pragma: no cover
+ import random
+ import time
+ from itertools import cycle
+ from typing import Dict, List, Tuple
+
+ from .align import Align
+ from .console import Console
+ from .live import Live
+ from .panel import Panel
+ from .rule import Rule
+ from .syntax import Syntax
+ from .table import Table
+
+ console = Console()
+
+ syntax = Syntax(
+ '''def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ for value in iter_values:
+ yield False, previous_value
+ previous_value = value
+ yield True, previous_value''',
+ "python",
+ line_numbers=True,
+ )
+
+ table = Table("foo", "bar", "baz")
+ table.add_row("1", "2", "3")
+
+ progress_renderables = [
+ "You can make the terminal shorter and taller to see the live table hide"
+ "Text may be printed while the progress bars are rendering.",
+ Panel("In fact, [i]any[/i] renderable will work"),
+ "Such as [magenta]tables[/]...",
+ table,
+ "Pretty printed structures...",
+ {"type": "example", "text": "Pretty printed"},
+ "Syntax...",
+ syntax,
+ Rule("Give it a try!"),
+ ]
+
+ examples = cycle(progress_renderables)
+
+ exchanges = [
+ "SGD",
+ "MYR",
+ "EUR",
+ "USD",
+ "AUD",
+ "JPY",
+ "CNH",
+ "HKD",
+ "CAD",
+ "INR",
+ "DKK",
+ "GBP",
+ "RUB",
+ "NZD",
+ "MXN",
+ "IDR",
+ "TWD",
+ "THB",
+ "VND",
+ ]
+ with Live(console=console) as live_table:
+ exchange_rate_dict: Dict[Tuple[str, str], float] = {}
+
+ for index in range(100):
+ select_exchange = exchanges[index % len(exchanges)]
+
+ for exchange in exchanges:
+ if exchange == select_exchange:
+ continue
+ time.sleep(0.4)
+ if random.randint(0, 10) < 1:
+ console.log(next(examples))
+ exchange_rate_dict[(select_exchange, exchange)] = 200 / (
+ (random.random() * 320) + 1
+ )
+ if len(exchange_rate_dict) > len(exchanges) - 1:
+ exchange_rate_dict.pop(list(exchange_rate_dict.keys())[0])
+ table = Table(title="Exchange Rates")
+
+ table.add_column("Source Currency")
+ table.add_column("Destination Currency")
+ table.add_column("Exchange Rate")
+
+ for ((source, dest), exchange_rate) in exchange_rate_dict.items():
+ table.add_row(
+ source,
+ dest,
+ Text(
+ f"{exchange_rate:.4f}",
+ style="red" if exchange_rate < 1.0 else "green",
+ ),
+ )
+
+ live_table.update(Align.center(table))
diff --git a/rich/live_render.py b/rich/live_render.py
new file mode 100644
index 0000000..8fea58b
--- /dev/null
+++ b/rich/live_render.py
@@ -0,0 +1,93 @@
+from typing import Optional, Tuple
+
+from typing_extensions import Literal
+
+from ._loop import loop_last
+from .console import Console, ConsoleOptions, RenderableType, RenderResult
+from .control import Control
+from .segment import Segment
+from .style import StyleType
+from .text import Text
+
+VerticalOverflowMethod = Literal["crop", "ellipsis", "visible"]
+
+
+class LiveRender:
+ """Creates a renderable that may be updated.
+
+ Args:
+ renderable (RenderableType): Any renderable object.
+ style (StyleType, optional): An optional style to apply to the renderable. Defaults to "".
+ """
+
+ def __init__(
+ self,
+ renderable: RenderableType,
+ style: StyleType = "",
+ vertical_overflow: VerticalOverflowMethod = "ellipsis",
+ ) -> None:
+ self.renderable = renderable
+ self.style = style
+ self.vertical_overflow = vertical_overflow
+ self._shape: Optional[Tuple[int, int]] = None
+
+ def set_renderable(self, renderable: RenderableType) -> None:
+ """Set a new renderable.
+
+ Args:
+ renderable (RenderableType): Any renderable object, including str.
+ """
+ self.renderable = renderable
+
+ def position_cursor(self) -> Control:
+ """Get control codes to move cursor to beginning of live render.
+
+ Returns:
+ Control: A control instance that may be printed.
+ """
+ if self._shape is not None:
+ _, height = self._shape
+ return Control("\r\x1b[2K" + "\x1b[1A\x1b[2K" * (height - 1))
+ return Control("")
+
+ def restore_cursor(self) -> Control:
+ """Get control codes to clear the render and restore the cursor to its previous position.
+
+ Returns:
+ Control: A Control instance that may be printed.
+ """
+ if self._shape is not None:
+ _, height = self._shape
+ return Control("\r" + "\x1b[1A\x1b[2K" * height)
+ return Control("")
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ _Segment = Segment
+ style = console.get_style(self.style)
+ lines = console.render_lines(self.renderable, options, style=style, pad=False)
+ shape = _Segment.get_shape(lines)
+
+ _, height = shape
+ if height > options.size.height:
+ if self.vertical_overflow == "crop":
+ lines = lines[: options.size.height]
+ shape = _Segment.get_shape(lines)
+ elif self.vertical_overflow == "ellipsis":
+ lines = lines[: (options.size.height - 1)]
+ overflow_text = Text(
+ "...",
+ overflow="crop",
+ justify="center",
+ end="",
+ style="live.ellipsis",
+ )
+ lines.append(list(console.render(overflow_text)))
+ shape = _Segment.get_shape(lines)
+ self._shape = shape
+
+ for last, line in loop_last(lines):
+ yield from line
+ if not last:
+ yield _Segment.line()
diff --git a/rich/logging.py b/rich/logging.py
new file mode 100644
index 0000000..49c7498
--- /dev/null
+++ b/rich/logging.py
@@ -0,0 +1,256 @@
+import logging
+from datetime import datetime
+from logging import Handler, LogRecord
+from pathlib import Path
+from typing import ClassVar, List, Optional, Type, Union
+
+from . import get_console
+from ._log_render import LogRender, FormatTimeCallable
+from .console import Console, ConsoleRenderable
+from .highlighter import Highlighter, ReprHighlighter
+from .text import Text
+from .traceback import Traceback
+
+
+class RichHandler(Handler):
+ """A logging handler that renders output with Rich. The time / level / message and file are displayed in columns.
+ The level is color coded, and the message is syntax highlighted.
+
+ Note:
+ Be careful when enabling console markup in log messages if you have configured logging for libraries not
+ under your control. If a dependency writes messages containing square brackets, it may not produce the intended output.
+
+ Args:
+ level (Union[int, str], optional): Log level. Defaults to logging.NOTSET.
+ console (:class:`~rich.console.Console`, optional): Optional console instance to write logs.
+ Default will use a global console instance writing to stdout.
+ show_time (bool, optional): Show a column for the time. Defaults to True.
+ show_level (bool, optional): Show a column for the level. Defaults to True.
+ show_path (bool, optional): Show the path to the original log call. Defaults to True.
+ enable_link_path (bool, optional): Enable terminal link of path column to file. Defaults to True.
+ highlighter (Highlighter, optional): Highlighter to style log messages, or None to use ReprHighlighter. Defaults to None.
+ markup (bool, optional): Enable console markup in log messages. Defaults to False.
+ rich_tracebacks (bool, optional): Enable rich tracebacks with syntax highlighting and formatting. Defaults to False.
+ tracebacks_width (Optional[int], optional): Number of characters used to render tracebacks, or None for full width. Defaults to None.
+ tracebacks_extra_lines (int, optional): Additional lines of code to render tracebacks, or None for full width. Defaults to None.
+ tracebacks_theme (str, optional): Override pygments theme used in traceback.
+ tracebacks_word_wrap (bool, optional): Enable word wrapping of long tracebacks lines. Defaults to True.
+ tracebacks_show_locals (bool, optional): Enable display of locals in tracebacks. Defaults to False.
+ locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to 10.
+ locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
+ log_time_format (Union[str, TimeFormatterCallable], optional): If ``log_time`` is enabled, either string for strftime or callable that formats the time. Defaults to "[%x %X] ".
+ """
+
+ KEYWORDS: ClassVar[Optional[List[str]]] = [
+ "GET",
+ "POST",
+ "HEAD",
+ "PUT",
+ "DELETE",
+ "OPTIONS",
+ "TRACE",
+ "PATCH",
+ ]
+ HIGHLIGHTER_CLASS: ClassVar[Type[Highlighter]] = ReprHighlighter
+
+ def __init__(
+ self,
+ level: Union[int, str] = logging.NOTSET,
+ console: Console = None,
+ *,
+ show_time: bool = True,
+ show_level: bool = True,
+ show_path: bool = True,
+ enable_link_path: bool = True,
+ highlighter: Highlighter = None,
+ markup: bool = False,
+ rich_tracebacks: bool = False,
+ tracebacks_width: Optional[int] = None,
+ tracebacks_extra_lines: int = 3,
+ tracebacks_theme: Optional[str] = None,
+ tracebacks_word_wrap: bool = True,
+ tracebacks_show_locals: bool = False,
+ locals_max_length: int = 10,
+ locals_max_string: int = 80,
+ log_time_format: Union[str, FormatTimeCallable] = "[%x %X]",
+ ) -> None:
+ super().__init__(level=level)
+ self.console = console or get_console()
+ self.highlighter = highlighter or self.HIGHLIGHTER_CLASS()
+ self._log_render = LogRender(
+ show_time=show_time,
+ show_level=show_level,
+ show_path=show_path,
+ time_format=log_time_format,
+ level_width=None,
+ )
+ self.enable_link_path = enable_link_path
+ self.markup = markup
+ self.rich_tracebacks = rich_tracebacks
+ self.tracebacks_width = tracebacks_width
+ self.tracebacks_extra_lines = tracebacks_extra_lines
+ self.tracebacks_theme = tracebacks_theme
+ self.tracebacks_word_wrap = tracebacks_word_wrap
+ self.tracebacks_show_locals = tracebacks_show_locals
+ self.locals_max_length = locals_max_length
+ self.locals_max_string = locals_max_string
+
+ def get_level_text(self, record: LogRecord) -> Text:
+ """Get the level name from the record.
+
+ Args:
+ record (LogRecord): LogRecord instance.
+
+ Returns:
+ Text: A tuple of the style and level name.
+ """
+ level_name = record.levelname
+ level_text = Text.styled(
+ level_name.ljust(8), f"logging.level.{level_name.lower()}"
+ )
+ return level_text
+
+ def emit(self, record: LogRecord) -> None:
+ """Invoked by logging."""
+ message = self.format(record)
+
+ traceback = None
+ if (
+ self.rich_tracebacks
+ and record.exc_info
+ and record.exc_info != (None, None, None)
+ ):
+ exc_type, exc_value, exc_traceback = record.exc_info
+ assert exc_type is not None
+ assert exc_value is not None
+ traceback = Traceback.from_exception(
+ exc_type,
+ exc_value,
+ exc_traceback,
+ width=self.tracebacks_width,
+ extra_lines=self.tracebacks_extra_lines,
+ theme=self.tracebacks_theme,
+ word_wrap=self.tracebacks_word_wrap,
+ show_locals=self.tracebacks_show_locals,
+ locals_max_length=self.locals_max_length,
+ locals_max_string=self.locals_max_string,
+ )
+ message = record.getMessage()
+
+ message_renderable = self.render_message(record, message)
+ log_renderable = self.render(
+ record=record, traceback=traceback, message_renderable=message_renderable
+ )
+ self.console.print(log_renderable)
+
+ def render_message(self, record: LogRecord, message: str) -> "ConsoleRenderable":
+ """Render message text in to Text.
+
+ record (LogRecord): logging Record.
+ message (str): String cotaining log message.
+
+ Returns:
+ ConsoleRenderable: Renderable to display log message.
+ """
+ use_markup = (
+ getattr(record, "markup") if hasattr(record, "markup") else self.markup
+ )
+ message_text = Text.from_markup(message) if use_markup else Text(message)
+ if self.highlighter:
+ message_text = self.highlighter(message_text)
+ if self.KEYWORDS:
+ message_text.highlight_words(self.KEYWORDS, "logging.keyword")
+ return message_text
+
+ def render(
+ self,
+ *,
+ record: LogRecord,
+ traceback: Optional[Traceback],
+ message_renderable: "ConsoleRenderable",
+ ) -> "ConsoleRenderable":
+ """Render log for display.
+
+ Args:
+ record (LogRecord): logging Record.
+ traceback (Optional[Traceback]): Traceback instance or None for no Traceback.
+ message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents.
+
+ Returns:
+ ConsoleRenderable: Renderable to display log.
+ """
+ path = Path(record.pathname).name
+ level = self.get_level_text(record)
+ time_format = None if self.formatter is None else self.formatter.datefmt
+ log_time = datetime.fromtimestamp(record.created)
+
+ log_renderable = self._log_render(
+ self.console,
+ [message_renderable] if not traceback else [message_renderable, traceback],
+ log_time=log_time,
+ time_format=time_format,
+ level=level,
+ path=path,
+ line_no=record.lineno,
+ link_path=record.pathname if self.enable_link_path else None,
+ )
+ return log_renderable
+
+
+if __name__ == "__main__": # pragma: no cover
+ from time import sleep
+
+ FORMAT = "%(message)s"
+ # FORMAT = "%(asctime)-15s - %(level) - %(message)s"
+ logging.basicConfig(
+ level="NOTSET",
+ format=FORMAT,
+ datefmt="[%X]",
+ handlers=[RichHandler(rich_tracebacks=True, tracebacks_show_locals=True)],
+ )
+ log = logging.getLogger("rich")
+
+ log.info("Server starting...")
+ log.info("Listening on http://127.0.0.1:8080")
+ sleep(1)
+
+ log.info("GET /index.html 200 1298")
+ log.info("GET /imgs/backgrounds/back1.jpg 200 54386")
+ log.info("GET /css/styles.css 200 54386")
+ log.warning("GET /favicon.ico 404 242")
+ sleep(1)
+
+ log.debug(
+ "JSONRPC request\n--> %r\n<-- %r",
+ {
+ "version": "1.1",
+ "method": "confirmFruitPurchase",
+ "params": [["apple", "orange", "mangoes", "pomelo"], 1.123],
+ "id": "194521489",
+ },
+ {"version": "1.1", "result": True, "error": None, "id": "194521489"},
+ )
+ log.debug(
+ "Loading configuration file /adasd/asdasd/qeqwe/qwrqwrqwr/sdgsdgsdg/werwerwer/dfgerert/ertertert/ertetert/werwerwer"
+ )
+ log.error("Unable to find 'pomelo' in database!")
+ log.info("POST /jsonrpc/ 200 65532")
+ log.info("POST /admin/ 401 42234")
+ log.warning("password was rejected for admin site.")
+
+ def divide():
+ number = 1
+ divisor = 0
+ foos = ["foo"] * 100
+ log.debug("in divide")
+ try:
+ number / divisor
+ except:
+ log.exception("An error of some kind occurred!")
+
+ divide()
+ sleep(1)
+ log.critical("Out of memory!")
+ log.info("Server exited with code=-1")
+ log.info("[bold]EXITING...[/bold]", extra=dict(markup=True))
diff --git a/rich/markdown.py b/rich/markdown.py
new file mode 100644
index 0000000..761215d
--- /dev/null
+++ b/rich/markdown.py
@@ -0,0 +1,620 @@
+from typing import Any, ClassVar, Dict, List, Optional, Type, Union
+
+from commonmark.blocks import Parser
+
+from . import box
+from ._loop import loop_first
+from ._stack import Stack
+from .console import Console, ConsoleOptions, JustifyMethod, RenderResult, Segment
+from .containers import Renderables
+from .jupyter import JupyterMixin
+from .panel import Panel
+from .rule import Rule
+from .style import Style, StyleStack
+from .syntax import Syntax
+from .text import Text, TextType
+
+
+class MarkdownElement:
+
+ new_line: ClassVar[bool] = True
+
+ @classmethod
+ def create(cls, markdown: "Markdown", node: Any) -> "MarkdownElement":
+ """Factory to create markdown element,
+
+ Args:
+ markdown (Markdown): THe parent Markdown object.
+ node (Any): A node from Pygments.
+
+ Returns:
+ MarkdownElement: A new markdown element
+ """
+ return cls()
+
+ def on_enter(self, context: "MarkdownContext"):
+ """Called when the node is entered.
+
+ Args:
+ context (MarkdownContext): The markdown context.
+ """
+
+ def on_text(self, context: "MarkdownContext", text: TextType) -> None:
+ """Called when text is parsed.
+
+ Args:
+ context (MarkdownContext): The markdown context.
+ """
+
+ def on_leave(self, context: "MarkdownContext") -> None:
+ """Called when the parser leaves the element.
+
+ Args:
+ context (MarkdownContext): [description]
+ """
+
+ def on_child_close(
+ self, context: "MarkdownContext", child: "MarkdownElement"
+ ) -> bool:
+ """Called when a child element is closed.
+
+ This method allows a parent element to take over rendering of its children.
+
+ Args:
+ context (MarkdownContext): The markdown context.
+ child (MarkdownElement): The child markdown element.
+
+ Returns:
+ bool: Return True to render the element, or False to not render the element.
+ """
+ return True
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ return ()
+
+
+class UnknownElement(MarkdownElement):
+ """An unknown element.
+
+ Hopefully there will be no unknown elements, and we will have a MarkdownElement for
+ everything in the document.
+
+ """
+
+
+class TextElement(MarkdownElement):
+ """Base class for elements that render text."""
+
+ style_name = "none"
+
+ def on_enter(self, context: "MarkdownContext") -> None:
+ self.style = context.enter_style(self.style_name)
+ self.text = Text(justify="left")
+
+ def on_text(self, context: "MarkdownContext", text: TextType) -> None:
+ self.text.append(text, context.current_style if isinstance(text, str) else None)
+
+ def on_leave(self, context: "MarkdownContext") -> None:
+ context.leave_style()
+
+
+class Paragraph(TextElement):
+ """A Paragraph."""
+
+ style_name = "markdown.paragraph"
+ justify: JustifyMethod
+
+ @classmethod
+ def create(cls, markdown: "Markdown", node) -> "Paragraph":
+ return cls(justify=markdown.justify or "left")
+
+ def __init__(self, justify: JustifyMethod) -> None:
+ self.justify = justify
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ self.text.justify = self.justify
+ yield self.text
+
+
+class Heading(TextElement):
+ """A heading."""
+
+ @classmethod
+ def create(cls, markdown: "Markdown", node: Any) -> "Heading":
+ heading = cls(node.level)
+ return heading
+
+ def on_enter(self, context: "MarkdownContext") -> None:
+ self.text = Text()
+ context.enter_style(self.style_name)
+
+ def __init__(self, level: int) -> None:
+ self.level = level
+ self.style_name = f"markdown.h{level}"
+ super().__init__()
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ text = self.text
+ text.justify = "center"
+ if self.level == 1:
+ # Draw a border around h1s
+ yield Panel(
+ text,
+ box=box.DOUBLE,
+ style="markdown.h1.border",
+ )
+ else:
+ # Styled text for h2 and beyond
+ if self.level == 2:
+ yield Text("")
+ yield text
+
+
+class CodeBlock(TextElement):
+ """A code block with syntax highlighting."""
+
+ style_name = "markdown.code_block"
+
+ @classmethod
+ def create(cls, markdown: "Markdown", node: Any) -> "CodeBlock":
+ node_info = node.info or ""
+ lexer_name = node_info.partition(" ")[0]
+ return cls(lexer_name or "default", markdown.code_theme)
+
+ def __init__(self, lexer_name: str, theme: str) -> None:
+ self.lexer_name = lexer_name
+ self.theme = theme
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ code = str(self.text).rstrip()
+ syntax = Panel(
+ Syntax(code, self.lexer_name, theme=self.theme),
+ border_style="dim",
+ box=box.SQUARE,
+ )
+ yield syntax
+
+
+class BlockQuote(TextElement):
+ """A block quote."""
+
+ style_name = "markdown.block_quote"
+
+ def __init__(self) -> None:
+ self.elements: Renderables = Renderables()
+
+ def on_child_close(
+ self, context: "MarkdownContext", child: "MarkdownElement"
+ ) -> bool:
+ self.elements.append(child)
+ return False
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ render_options = options.update(width=options.max_width - 4)
+ lines = console.render_lines(self.elements, render_options, style=self.style)
+ style = self.style
+ new_line = Segment("\n")
+ padding = Segment("▌ ", style)
+ for line in lines:
+ yield padding
+ yield from line
+ yield new_line
+
+
+class HorizontalRule(MarkdownElement):
+ """A horizontal rule to divide sections."""
+
+ new_line = False
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ style = console.get_style("markdown.hr", default="none")
+ yield Rule(style=style)
+
+
+class ListElement(MarkdownElement):
+ """A list element."""
+
+ @classmethod
+ def create(cls, markdown: "Markdown", node: Any) -> "ListElement":
+ list_data = node.list_data
+ return cls(list_data["type"], list_data["start"])
+
+ def __init__(self, list_type: str, list_start: Optional[int]) -> None:
+ self.items: List[ListItem] = []
+ self.list_type = list_type
+ self.list_start = list_start
+
+ def on_child_close(
+ self, context: "MarkdownContext", child: "MarkdownElement"
+ ) -> bool:
+ assert isinstance(child, ListItem)
+ self.items.append(child)
+ return False
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ if self.list_type == "bullet":
+ for item in self.items:
+ yield from item.render_bullet(console, options)
+ else:
+ number = 1 if self.list_start is None else self.list_start
+ last_number = number + len(self.items)
+ for item in self.items:
+ yield from item.render_number(console, options, number, last_number)
+ number += 1
+
+
+class ListItem(TextElement):
+ """An item in a list."""
+
+ style_name = "markdown.item"
+
+ def __init__(self) -> None:
+ self.elements: Renderables = Renderables()
+
+ def on_child_close(
+ self, context: "MarkdownContext", child: "MarkdownElement"
+ ) -> bool:
+ self.elements.append(child)
+ return False
+
+ def render_bullet(self, console: Console, options: ConsoleOptions) -> RenderResult:
+ render_options = options.update(width=options.max_width - 3)
+ lines = console.render_lines(self.elements, render_options, style=self.style)
+ bullet_style = console.get_style("markdown.item.bullet", default="none")
+
+ bullet = Segment(" • ", bullet_style)
+ padding = Segment(" " * 3, bullet_style)
+ new_line = Segment("\n")
+ for first, line in loop_first(lines):
+ yield bullet if first else padding
+ yield from line
+ yield new_line
+
+ def render_number(
+ self, console: Console, options: ConsoleOptions, number: int, last_number: int
+ ) -> RenderResult:
+ number_width = len(str(last_number)) + 2
+ render_options = options.update(width=options.max_width - number_width)
+ lines = console.render_lines(self.elements, render_options, style=self.style)
+ number_style = console.get_style("markdown.item.number", default="none")
+
+ new_line = Segment("\n")
+ padding = Segment(" " * number_width, number_style)
+ numeral = Segment(f"{number}".rjust(number_width - 1) + " ", number_style)
+ for first, line in loop_first(lines):
+ yield numeral if first else padding
+ yield from line
+ yield new_line
+
+
+class ImageItem(TextElement):
+ """Renders a placeholder for an image."""
+
+ new_line = False
+
+ @classmethod
+ def create(cls, markdown: "Markdown", node: Any) -> "MarkdownElement":
+ """Factory to create markdown element,
+
+ Args:
+ markdown (Markdown): THe parent Markdown object.
+ node (Any): A node from Pygments.
+
+ Returns:
+ MarkdownElement: A new markdown element
+ """
+ return cls(node.destination, markdown.hyperlinks)
+
+ def __init__(self, destination: str, hyperlinks: bool) -> None:
+ self.destination = destination
+ self.hyperlinks = hyperlinks
+ self.link: Optional[str] = None
+ super().__init__()
+
+ def on_enter(self, context: "MarkdownContext") -> None:
+ self.link = context.current_style.link
+ self.text = Text(justify="left")
+ super().on_enter(context)
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ link_style = Style(link=self.link or self.destination or None)
+ title = self.text or Text(self.destination.strip("/").rsplit("/", 1)[-1])
+ if self.hyperlinks:
+ title.stylize(link_style)
+ yield Text.assemble("🌆 ", title, " ", end="")
+
+
+class MarkdownContext:
+ """Manages the console render state."""
+
+ def __init__(
+ self,
+ console: Console,
+ options: ConsoleOptions,
+ style: Style,
+ inline_code_lexer: str = None,
+ inline_code_theme: str = "monokai",
+ ) -> None:
+ self.console = console
+ self.options = options
+ self.style_stack: StyleStack = StyleStack(style)
+ self.stack: Stack[MarkdownElement] = Stack()
+
+ self._syntax: Optional[Syntax] = None
+ if inline_code_lexer is not None:
+ self._syntax = Syntax("", inline_code_lexer, theme=inline_code_theme)
+
+ @property
+ def current_style(self) -> Style:
+ """Current style which is the product of all styles on the stack."""
+ return self.style_stack.current
+
+ def on_text(self, text: str, node_type: str) -> None:
+ """Called when the parser visits text."""
+ if node_type == "code" and self._syntax is not None:
+ highlight_text = self._syntax.highlight(text)
+ highlight_text.rstrip()
+ self.stack.top.on_text(
+ self, Text.assemble(highlight_text, style=self.style_stack.current)
+ )
+ else:
+ self.stack.top.on_text(self, text)
+
+ def enter_style(self, style_name: Union[str, Style]) -> Style:
+ """Enter a style context."""
+ style = self.console.get_style(style_name, default="none")
+ self.style_stack.push(style)
+ return self.current_style
+
+ def leave_style(self) -> Style:
+ """Leave a style context."""
+ style = self.style_stack.pop()
+ return style
+
+
+class Markdown(JupyterMixin):
+ """A Markdown renderable.
+
+ Args:
+ markup (str): A string containing markdown.
+ code_theme (str, optional): Pygments theme for code blocks. Defaults to "monokai".
+ justify (JustifyMethod, optional): Justify value for paragraphs. Defaults to None.
+ style (Union[str, Style], optional): Optional style to apply to markdown.
+ hyperlinks (bool, optional): Enable hyperlinks. Defaults to ``True``.
+ inline_code_lexer: (str, optional): Lexer to use if inline code highlighting is
+ enabled. Defaults to "python".
+ inline_code_theme: (Optional[str], optional): Pygments theme for inline code
+ highlighting, or None for no highlighting. Defaults to None.
+ """
+
+ elements: ClassVar[Dict[str, Type[MarkdownElement]]] = {
+ "paragraph": Paragraph,
+ "heading": Heading,
+ "code_block": CodeBlock,
+ "block_quote": BlockQuote,
+ "thematic_break": HorizontalRule,
+ "list": ListElement,
+ "item": ListItem,
+ "image": ImageItem,
+ }
+ inlines = {"emph", "strong", "code", "strike"}
+
+ def __init__(
+ self,
+ markup: str,
+ code_theme: str = "monokai",
+ justify: JustifyMethod = None,
+ style: Union[str, Style] = "none",
+ hyperlinks: bool = True,
+ inline_code_lexer: str = None,
+ inline_code_theme: str = None,
+ ) -> None:
+ self.markup = markup
+ parser = Parser()
+ self.parsed = parser.parse(markup)
+ self.code_theme = code_theme
+ self.justify = justify
+ self.style = style
+ self.hyperlinks = hyperlinks
+ self.inline_code_lexer = inline_code_lexer
+ self.inline_code_theme = inline_code_theme or code_theme
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ """Render markdown to the console."""
+ style = console.get_style(self.style, default="none")
+ context = MarkdownContext(
+ console,
+ options,
+ style,
+ inline_code_lexer=self.inline_code_lexer,
+ inline_code_theme=self.inline_code_theme,
+ )
+ nodes = self.parsed.walker()
+ inlines = self.inlines
+ new_line = False
+ for current, entering in nodes:
+ node_type = current.t
+ if node_type in ("html_inline", "html_block", "text"):
+ context.on_text(current.literal.replace("\n", " "), node_type)
+ elif node_type == "linebreak":
+ if entering:
+ context.on_text("\n", node_type)
+ elif node_type == "softbreak":
+ if entering:
+ context.on_text(" ", node_type)
+ elif node_type == "link":
+ if entering:
+ link_style = console.get_style("markdown.link", default="none")
+ if self.hyperlinks:
+ link_style += Style(link=current.destination)
+ context.enter_style(link_style)
+ else:
+ context.leave_style()
+ if not self.hyperlinks:
+ context.on_text(" (", node_type)
+ style = Style(underline=True) + console.get_style(
+ "markdown.link_url", default="none"
+ )
+ context.enter_style(style)
+ context.on_text(current.destination, node_type)
+ context.leave_style()
+ context.on_text(")", node_type)
+ elif node_type in inlines:
+ if current.is_container():
+ if entering:
+ context.enter_style(f"markdown.{node_type}")
+ else:
+ context.leave_style()
+ else:
+ context.enter_style(f"markdown.{node_type}")
+ if current.literal:
+ context.on_text(current.literal, node_type)
+ context.leave_style()
+ else:
+ element_class = self.elements.get(node_type) or UnknownElement
+ if current.is_container():
+ if entering:
+ element = element_class.create(self, current)
+ context.stack.push(element)
+ element.on_enter(context)
+ else:
+ element = context.stack.pop()
+ if context.stack:
+ if context.stack.top.on_child_close(context, element):
+ if new_line:
+ yield Segment("\n")
+ yield from console.render(element, context.options)
+ element.on_leave(context)
+ else:
+ element.on_leave(context)
+ else:
+ element.on_leave(context)
+ yield from console.render(element, context.options)
+ new_line = element.new_line
+ else:
+ element = element_class.create(self, current)
+
+ context.stack.push(element)
+ element.on_enter(context)
+ if current.literal:
+ element.on_text(context, current.literal.rstrip())
+ context.stack.pop()
+ if new_line:
+ yield Segment("\n")
+ yield from console.render(element, context.options)
+ element.on_leave(context)
+ new_line = element.new_line
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ import argparse
+ import sys
+
+ parser = argparse.ArgumentParser(
+ description="Render Markdown to the console with Rich"
+ )
+ parser.add_argument(
+ "path",
+ metavar="PATH",
+ nargs="?",
+ help="path to markdown file",
+ )
+ parser.add_argument(
+ "-c",
+ "--force-color",
+ dest="force_color",
+ action="store_true",
+ default=None,
+ help="force color for non-terminals",
+ )
+ parser.add_argument(
+ "-t",
+ "--code-theme",
+ dest="code_theme",
+ default="monokai",
+ help="pygments code theme",
+ )
+ parser.add_argument(
+ "-i",
+ "--inline-code-lexer",
+ dest="inline_code_lexer",
+ default=None,
+ help="inline_code_lexer",
+ )
+ parser.add_argument(
+ "-y",
+ "--hyperlinks",
+ dest="hyperlinks",
+ action="store_true",
+ help="enable hyperlinks",
+ )
+ parser.add_argument(
+ "-w",
+ "--width",
+ type=int,
+ dest="width",
+ default=None,
+ help="width of output (default will auto-detect)",
+ )
+ parser.add_argument(
+ "-j",
+ "--justify",
+ dest="justify",
+ action="store_true",
+ help="enable full text justify",
+ )
+ parser.add_argument(
+ "-p",
+ "--page",
+ dest="page",
+ action="store_true",
+ help="use pager to scroll output",
+ )
+ args = parser.parse_args()
+
+ from rich.console import Console
+
+ if not args.path or args.path == "-":
+ markdown_body = sys.stdin.read()
+ else:
+ with open(args.path, "rt", encoding="utf-8") as markdown_file:
+ markdown_body = markdown_file.read()
+ markdown = Markdown(
+ markdown_body,
+ justify="full" if args.justify else "left",
+ code_theme=args.code_theme,
+ hyperlinks=args.hyperlinks,
+ inline_code_lexer=args.inline_code_lexer,
+ )
+ if args.page:
+ import pydoc
+ import io
+
+ console = Console(
+ file=io.StringIO(), force_terminal=args.force_color, width=args.width
+ )
+ console.print(markdown)
+ pydoc.pager(console.file.getvalue()) # type: ignore
+
+ else:
+ console = Console(force_terminal=args.force_color, width=args.width)
+ console.print(markdown)
diff --git a/rich/markup.py b/rich/markup.py
new file mode 100644
index 0000000..99d5663
--- /dev/null
+++ b/rich/markup.py
@@ -0,0 +1,179 @@
+import re
+from typing import Iterable, List, Match, NamedTuple, Optional, Tuple, Union
+
+from .errors import MarkupError
+from .style import Style
+from .text import Span, Text
+from ._emoji_replace import _emoji_replace
+
+
+RE_TAGS = re.compile(
+ r"""((\\*)\[([a-z#\/].*?)\])""",
+ re.VERBOSE,
+)
+
+
+class Tag(NamedTuple):
+ """A tag in console markup."""
+
+ name: str
+ """The tag name. e.g. 'bold'."""
+ parameters: Optional[str]
+ """Any additional parameters after the name."""
+
+ def __str__(self) -> str:
+ return (
+ self.name if self.parameters is None else f"{self.name} {self.parameters}"
+ )
+
+ @property
+ def markup(self) -> str:
+ """Get the string representation of this tag."""
+ return (
+ f"[{self.name}]"
+ if self.parameters is None
+ else f"[{self.name}={self.parameters}]"
+ )
+
+
+def escape(markup: str, _escape=re.compile(r"(\\*)(\[[a-z#\/].*?\])").sub) -> str:
+ """Escapes text so that it won't be interpreted as markup.
+
+ Args:
+ markup (str): Content to be inserted in to markup.
+
+ Returns:
+ str: Markup with square brackets escaped.
+ """
+
+ def escape_backslashes(match: Match[str]) -> str:
+ """Called by re.sub replace matches."""
+ backslashes, text = match.groups()
+ return f"{backslashes}{backslashes}\\{text}"
+
+ markup = _escape(escape_backslashes, markup)
+ return markup
+
+
+def _parse(markup: str) -> Iterable[Tuple[int, Optional[str], Optional[Tag]]]:
+ """Parse markup in to an iterable of tuples of (position, text, tag).
+
+ Args:
+ markup (str): A string containing console markup
+
+ """
+ position = 0
+ _divmod = divmod
+ _Tag = Tag
+ for match in RE_TAGS.finditer(markup):
+ full_text, escapes, tag_text = match.groups()
+ start, end = match.span()
+ if start > position:
+ yield start, markup[position:start], None
+ if escapes:
+ backslashes, escaped = _divmod(len(escapes), 2)
+ if backslashes:
+ # Literal backslashes
+ yield start, "\\" * backslashes, None
+ start += backslashes * 2
+ if escaped:
+ # Escape of tag
+ yield start, full_text[len(escapes) :], None
+ position = end
+ continue
+ text, equals, parameters = tag_text.partition("=")
+ yield start, None, _Tag(text, parameters if equals else None)
+ position = end
+ if position < len(markup):
+ yield position, markup[position:], None
+
+
+def render(markup: str, style: Union[str, Style] = "", emoji: bool = True) -> Text:
+ """Render console markup in to a Text instance.
+
+ Args:
+ markup (str): A string containing console markup.
+ emoji (bool, optional): Also render emoji code. Defaults to True.
+
+ Raises:
+ MarkupError: If there is a syntax error in the markup.
+
+ Returns:
+ Text: A test instance.
+ """
+ emoji_replace = _emoji_replace
+ if "[" not in markup:
+ return Text(emoji_replace(markup) if emoji else markup, style=style)
+ text = Text(style=style)
+ append = text.append
+ normalize = Style.normalize
+
+ style_stack: List[Tuple[int, Tag]] = []
+ pop = style_stack.pop
+
+ spans: List[Span] = []
+ append_span = spans.append
+
+ _Span = Span
+ _Tag = Tag
+
+ def pop_style(style_name: str) -> Tuple[int, Tag]:
+ """Pop tag matching given style name."""
+ for index, (_, tag) in enumerate(reversed(style_stack), 1):
+ if tag.name == style_name:
+ return pop(-index)
+ raise KeyError(style_name)
+
+ for position, plain_text, tag in _parse(markup):
+ if plain_text is not None:
+ append(emoji_replace(plain_text) if emoji else plain_text)
+ elif tag is not None:
+ if tag.name.startswith("/"): # Closing tag
+ style_name = tag.name[1:].strip()
+ if style_name: # explicit close
+ style_name = normalize(style_name)
+ try:
+ start, open_tag = pop_style(style_name)
+ except KeyError:
+ raise MarkupError(
+ f"closing tag '{tag.markup}' at position {position} doesn't match any open tag"
+ ) from None
+ else: # implicit close
+ try:
+ start, open_tag = pop()
+ except IndexError:
+ raise MarkupError(
+ f"closing tag '[/]' at position {position} has nothing to close"
+ ) from None
+
+ append_span(_Span(start, len(text), str(open_tag)))
+ else: # Opening tag
+ normalized_tag = _Tag(normalize(tag.name), tag.parameters)
+ style_stack.append((len(text), normalized_tag))
+
+ text_length = len(text)
+ while style_stack:
+ start, tag = style_stack.pop()
+ append_span(_Span(start, text_length, str(tag)))
+
+ text.spans = sorted(spans)
+ return text
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from rich.console import Console
+ from rich.text import Text
+
+ console = Console(highlight=False)
+
+ console.print("Hello [1], [1,2,3] ['hello']")
+ console.print("foo")
+ console.print("Hello [link=https://www.willmcgugan.com]W[b red]o[/]rld[/]!")
+
+ from rich import print
+
+ print(escape("[red]"))
+ print(escape(r"\[red]"))
+ print(escape(r"\\[red]"))
+ print(escape(r"\\\[red]"))
diff --git a/rich/measure.py b/rich/measure.py
new file mode 100644
index 0000000..8089bf0
--- /dev/null
+++ b/rich/measure.py
@@ -0,0 +1,149 @@
+from operator import itemgetter
+from typing import Iterable, NamedTuple, TYPE_CHECKING
+
+from . import errors
+from .protocol import is_renderable
+
+if TYPE_CHECKING:
+ from .console import Console, RenderableType
+
+
+class Measurement(NamedTuple):
+ """Stores the minimum and maximum widths (in characters) required to render an object."""
+
+ minimum: int
+ """Minimum number of cells required to render."""
+ maximum: int
+ """Maximum number of cells required to render."""
+
+ @property
+ def span(self) -> int:
+ """Get difference between maximum and minimum."""
+ return self.maximum - self.minimum
+
+ def normalize(self) -> "Measurement":
+ """Get measurement that ensures that minimum <= maximum and minimum >= 0
+
+ Returns:
+ Measurement: A normalized measurement.
+ """
+ minimum, maximum = self
+ minimum = min(max(0, minimum), maximum)
+ return Measurement(max(0, minimum), max(0, max(minimum, maximum)))
+
+ def with_maximum(self, width: int) -> "Measurement":
+ """Get a RenderableWith where the widths are <= width.
+
+ Args:
+ width (int): Maximum desired width.
+
+ Returns:
+ Measurement: New Measurement object.
+ """
+ minimum, maximum = self
+ return Measurement(min(minimum, width), min(maximum, width))
+
+ def with_minimum(self, width: int) -> "Measurement":
+ """Get a RenderableWith where the widths are >= width.
+
+ Args:
+ width (int): Minimum desired width.
+
+ Returns:
+ Measurement: New Measurement object.
+ """
+ minimum, maximum = self
+ width = max(0, width)
+ return Measurement(max(minimum, width), max(maximum, width))
+
+ def clamp(self, min_width: int = None, max_width: int = None) -> "Measurement":
+ """Clamp a measurement within the specified range.
+
+ Args:
+ min_width (int): Minimum desired width, or ``None`` for no minimum. Defaults to None.
+ max_width (int): Maximum desired width, or ``None`` for no maximum. Defaults to None.
+
+ Returns:
+ Measurement: New Measurement object.
+ """
+ measurement = self
+ if min_width is not None:
+ measurement = measurement.with_minimum(min_width)
+ if max_width is not None:
+ measurement = measurement.with_maximum(max_width)
+ return measurement
+
+ @classmethod
+ def get(
+ cls, console: "Console", renderable: "RenderableType", max_width: int = None
+ ) -> "Measurement":
+ """Get a measurement for a renderable.
+
+ Args:
+ console (~rich.console.Console): Console instance.
+ renderable (RenderableType): An object that may be rendered with Rich.
+ max_width (int, optional): The maximum width available, or None to use console.width.
+ Defaults to None.
+
+ Raises:
+ errors.NotRenderableError: If the object is not renderable.
+
+ Returns:
+ Measurement: Measurement object containing range of character widths required to render the object.
+ """
+ from rich.console import RichCast
+
+ _max_width = console.width if max_width is None else max_width
+ if _max_width < 1:
+ return Measurement(0, 0)
+ if isinstance(renderable, str):
+ renderable = console.render_str(renderable)
+
+ if isinstance(renderable, RichCast):
+ renderable = renderable.__rich__()
+
+ if is_renderable(renderable):
+ get_console_width = getattr(renderable, "__rich_measure__", None)
+ if get_console_width is not None:
+ render_width = (
+ get_console_width(console, _max_width)
+ .normalize()
+ .with_maximum(_max_width)
+ )
+ if render_width.maximum < 1:
+ return Measurement(0, 0)
+ return render_width.normalize()
+ else:
+ return Measurement(0, _max_width)
+ else:
+ raise errors.NotRenderableError(
+ f"Unable to get render width for {renderable!r}; "
+ "a str, Segment, or object with __rich_console__ method is required"
+ )
+
+
+def measure_renderables(
+ console: "Console", renderables: Iterable["RenderableType"], max_width: int
+) -> "Measurement":
+ """Get a measurement that would fit a number of renderables.
+
+ Args:
+ console (~rich.console.Console): Console instance.
+ renderables (Iterable[RenderableType]): One or more renderable objects.
+ max_width (int): The maximum width available.
+
+ Returns:
+ Measurement: Measurement object containing range of character widths required to
+ contain all given renderables.
+ """
+ if not renderables:
+ return Measurement(0, 0)
+ get_measurement = Measurement.get
+ measurements = [
+ get_measurement(console, renderable, max_width) for renderable in renderables
+ ]
+ measured_width = Measurement(
+ max(measurements, key=itemgetter(0)).minimum,
+ max(measurements, key=itemgetter(1)).maximum,
+ )
+ return measured_width
diff --git a/rich/padding.py b/rich/padding.py
new file mode 100644
index 0000000..da30816
--- /dev/null
+++ b/rich/padding.py
@@ -0,0 +1,124 @@
+from typing import cast, Tuple, TYPE_CHECKING, Union
+
+if TYPE_CHECKING:
+ from .console import (
+ Console,
+ ConsoleOptions,
+ RenderableType,
+ RenderResult,
+ )
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .style import Style
+from .segment import Segment
+
+
+PaddingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]]
+
+
+class Padding(JupyterMixin):
+ """Draw space around content.
+
+ Example:
+ >>> print(Padding("Hello", (2, 4), style="on blue"))
+
+ Args:
+ renderable (RenderableType): String or other renderable.
+ pad (Union[int, Tuple[int]]): Padding for top, right, bottom, and left borders.
+ May be specified with 1, 2, or 4 integers (CSS style).
+ style (Union[str, Style], optional): Style for padding characters. Defaults to "none".
+ expand (bool, optional): Expand padding to fit available width. Defaults to True.
+ """
+
+ def __init__(
+ self,
+ renderable: "RenderableType",
+ pad: "PaddingDimensions" = (0, 0, 0, 0),
+ *,
+ style: Union[str, Style] = "none",
+ expand: bool = True,
+ ):
+ self.renderable = renderable
+ self.top, self.right, self.bottom, self.left = self.unpack(pad)
+ self.style = style
+ self.expand = expand
+
+ @classmethod
+ def indent(cls, renderable: "RenderableType", level: int) -> "Padding":
+ """Make padding instance to render an indent.
+
+ Args:
+ renderable (RenderableType): String or other renderable.
+ level (int): Number of characters to indent.
+
+ Returns:
+ Padding: A Padding instance.
+ """
+
+ return Padding(renderable, pad=(0, 0, 0, level), expand=False)
+
+ @staticmethod
+ def unpack(pad: "PaddingDimensions") -> Tuple[int, int, int, int]:
+ """Unpack padding specified in CSS style."""
+ if isinstance(pad, int):
+ return (pad, pad, pad, pad)
+ if len(pad) == 1:
+ _pad = pad[0]
+ return (_pad, _pad, _pad, _pad)
+ if len(pad) == 2:
+ pad_top, pad_right = cast(Tuple[int, int], pad)
+ return (pad_top, pad_right, pad_top, pad_right)
+ if len(pad) == 4:
+ top, right, bottom, left = cast(Tuple[int, int, int, int], pad)
+ return (top, right, bottom, left)
+ raise ValueError(f"1, 2 or 4 integers required for padding; {len(pad)} given")
+
+ def __repr__(self) -> str:
+ return f"Padding({self.renderable!r}, ({self.top},{self.right},{self.bottom},{self.left}))"
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+
+ style = console.get_style(self.style)
+ if self.expand:
+ width = options.max_width
+ else:
+ width = min(
+ Measurement.get(console, self.renderable, options.max_width).maximum
+ + self.left
+ + self.right,
+ options.max_width,
+ )
+ child_options = options.update_width(width - self.left - self.right)
+ lines = console.render_lines(
+ self.renderable, child_options, style=style, pad=False
+ )
+ lines = Segment.set_shape(lines, child_options.max_width, style=style)
+
+ blank_line = Segment(" " * width + "\n", style)
+ top = [blank_line] * self.top
+ bottom = [blank_line] * self.bottom
+ left = Segment(" " * self.left, style) if self.left else None
+ right = Segment(" " * self.right, style) if self.right else None
+ new_line = Segment.line()
+ yield from top
+ for line in lines:
+ if left is not None:
+ yield left
+ yield from line
+ if right is not None:
+ yield right
+ yield new_line
+ yield from bottom
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
+ extra_width = self.left + self.right
+ if max_width - extra_width < 1:
+ return Measurement(max_width, max_width)
+ measure_min, measure_max = Measurement.get(
+ console, self.renderable, max(0, max_width - extra_width)
+ )
+ measurement = Measurement(measure_min + extra_width, measure_max + extra_width)
+ measurement = measurement.with_maximum(max_width)
+ return measurement
diff --git a/rich/pager.py b/rich/pager.py
new file mode 100644
index 0000000..52e77ef
--- /dev/null
+++ b/rich/pager.py
@@ -0,0 +1,33 @@
+from abc import ABC, abstractmethod
+import pydoc
+
+
+class Pager(ABC):
+ """Base class for a pager."""
+
+ @abstractmethod
+ def show(self, content: str) -> None:
+ """Show content in pager.
+
+ Args:
+ content (str): Content to be displayed.
+ """
+
+
+class SystemPager(Pager):
+ """Uses the pager installed on the system."""
+
+ _pager = lambda self, content: pydoc.pager(content)
+
+ def show(self, content: str) -> None:
+ """Use the same pager used by pydoc."""
+ self._pager(content)
+
+
+if __name__ == "__main__": # pragma: no cover
+ from .__main__ import make_test_card
+ from .console import Console
+
+ console = Console()
+ with console.pager(styles=True):
+ console.print(make_test_card())
diff --git a/rich/palette.py b/rich/palette.py
new file mode 100644
index 0000000..f295879
--- /dev/null
+++ b/rich/palette.py
@@ -0,0 +1,100 @@
+from math import sqrt
+from functools import lru_cache
+from typing import Sequence, Tuple, TYPE_CHECKING
+
+from .color_triplet import ColorTriplet
+
+if TYPE_CHECKING:
+ from rich.table import Table
+
+
+class Palette:
+ """A palette of available colors."""
+
+ def __init__(self, colors: Sequence[Tuple[int, int, int]]):
+ self._colors = colors
+
+ def __getitem__(self, number: int) -> ColorTriplet:
+ return ColorTriplet(*self._colors[number])
+
+ def __rich__(self) -> "Table":
+ from rich.color import Color
+ from rich.style import Style
+ from rich.text import Text
+ from rich.table import Table
+
+ table = Table(
+ "index",
+ "RGB",
+ "Color",
+ title="Palette",
+ caption=f"{len(self._colors)} colors",
+ highlight=True,
+ caption_justify="right",
+ )
+ for index, color in enumerate(self._colors):
+ table.add_row(
+ str(index),
+ repr(color),
+ Text(" " * 16, style=Style(bgcolor=Color.from_rgb(*color))),
+ )
+ return table
+
+ # This is somewhat inefficient and needs caching
+ @lru_cache(maxsize=1024)
+ def match(self, color: Tuple[int, int, int]) -> int:
+ """Find a color from a palette that most closely matches a given color.
+
+ Args:
+ color (Tuple[int, int, int]): RGB components in range 0 > 255.
+
+ Returns:
+ int: Index of closes matching color.
+ """
+ red1, green1, blue1 = color
+ _sqrt = sqrt
+ get_color = self._colors.__getitem__
+
+ def get_color_distance(index: int) -> float:
+ """Get the distance to a color."""
+ red2, green2, blue2 = get_color(index)
+ red_mean = (red1 + red2) // 2
+ red = red1 - red2
+ green = green1 - green2
+ blue = blue1 - blue2
+ return _sqrt(
+ (((512 + red_mean) * red * red) >> 8)
+ + 4 * green * green
+ + (((767 - red_mean) * blue * blue) >> 8)
+ )
+
+ min_index = min(range(len(self._colors)), key=get_color_distance)
+ return min_index
+
+
+if __name__ == "__main__": # pragma: no cover
+ import colorsys
+ from typing import Iterable
+ from rich.color import Color
+ from rich.console import Console, ConsoleOptions
+ from rich.segment import Segment
+ from rich.style import Style
+
+ class ColorBox:
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> Iterable[Segment]:
+ height = console.size.height - 3
+ for y in range(0, height):
+ for x in range(options.max_width):
+ h = x / options.max_width
+ l = y / (height + 1)
+ r1, g1, b1 = colorsys.hls_to_rgb(h, l, 1.0)
+ r2, g2, b2 = colorsys.hls_to_rgb(h, l + (1 / height / 2), 1.0)
+ bgcolor = Color.from_rgb(r1 * 255, g1 * 255, b1 * 255)
+ color = Color.from_rgb(r2 * 255, g2 * 255, b2 * 255)
+ yield Segment("▄", Style(color=color, bgcolor=bgcolor))
+ yield Segment.line()
+
+ console = Console()
+ console.print(ColorBox())
diff --git a/rich/panel.py b/rich/panel.py
new file mode 100644
index 0000000..2e54d9e
--- /dev/null
+++ b/rich/panel.py
@@ -0,0 +1,206 @@
+from typing import Optional, TYPE_CHECKING
+
+from .box import Box, ROUNDED
+
+from .align import AlignMethod
+from .jupyter import JupyterMixin
+from .measure import Measurement, measure_renderables
+from .padding import Padding, PaddingDimensions
+from .style import StyleType
+from .text import Text, TextType
+from .segment import Segment
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderableType, RenderResult
+
+
+class Panel(JupyterMixin):
+ """A console renderable that draws a border around its contents.
+
+ Example:
+ >>> console.print(Panel("Hello, World!"))
+
+ Args:
+ renderable (RenderableType): A console renderable object.
+ box (Box, optional): A Box instance that defines the look of the border (see :ref:`appendix_box`.
+ Defaults to box.ROUNDED.
+ safe_box (bool, optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
+ expand (bool, optional): If True the panel will stretch to fill the console
+ width, otherwise it will be sized to fit the contents. Defaults to True.
+ style (str, optional): The style of the panel (border and contents). Defaults to "none".
+ border_style (str, optional): The style of the border. Defaults to "none".
+ width (Optional[int], optional): Optional width of panel. Defaults to None to auto-detect.
+ padding (Optional[PaddingDimensions]): Optional padding around renderable. Defaults to 0.
+ highlight (bool, optional): Enable automatic highlighting of panel title (if str). Defaults to False.
+ """
+
+ def __init__(
+ self,
+ renderable: "RenderableType",
+ box: Box = ROUNDED,
+ *,
+ title: TextType = None,
+ title_align: AlignMethod = "center",
+ safe_box: Optional[bool] = None,
+ expand: bool = True,
+ style: StyleType = "none",
+ border_style: StyleType = "none",
+ width: Optional[int] = None,
+ padding: PaddingDimensions = (0, 1),
+ highlight: bool = False,
+ ) -> None:
+ self.renderable = renderable
+ self.box = box
+ self.title = title
+ self.title_align = title_align
+ self.safe_box = safe_box
+ self.expand = expand
+ self.style = style
+ self.border_style = border_style
+ self.width = width
+ self.padding = padding
+ self.highlight = highlight
+
+ @classmethod
+ def fit(
+ cls,
+ renderable: "RenderableType",
+ box: Box = ROUNDED,
+ *,
+ title: TextType = None,
+ title_align: AlignMethod = "center",
+ safe_box: Optional[bool] = None,
+ style: StyleType = "none",
+ border_style: StyleType = "none",
+ width: Optional[int] = None,
+ padding: PaddingDimensions = (0, 1),
+ ):
+ """An alternative constructor that sets expand=False."""
+ return cls(
+ renderable,
+ box,
+ title=title,
+ title_align=title_align,
+ safe_box=safe_box,
+ style=style,
+ border_style=border_style,
+ width=width,
+ padding=padding,
+ expand=False,
+ )
+
+ @property
+ def _title(self) -> Optional[Text]:
+ if self.title:
+ title_text = (
+ Text.from_markup(self.title)
+ if isinstance(self.title, str)
+ else self.title.copy()
+ )
+ title_text.end = ""
+ title_text.plain = title_text.plain.replace("\n", " ")
+ title_text.no_wrap = True
+ title_text.expand_tabs()
+ title_text.pad(1)
+ return title_text
+ return None
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ _padding = Padding.unpack(self.padding)
+ renderable = (
+ Padding(self.renderable, _padding) if any(_padding) else self.renderable
+ )
+ style = console.get_style(self.style)
+ border_style = style + console.get_style(self.border_style)
+ width = (
+ options.max_width
+ if self.width is None
+ else min(options.max_width, self.width)
+ )
+
+ safe_box: bool = console.safe_box if self.safe_box is None else self.safe_box # type: ignore
+ box = self.box.substitute(options, safe=safe_box)
+
+ title_text = self._title
+ if title_text is not None:
+ title_text.style = border_style
+
+ child_width = (
+ width - 2
+ if self.expand
+ else Measurement.get(console, renderable, width - 2).maximum
+ )
+ child_height = None if options.height is None else options.height - 2
+ if title_text is not None:
+ child_width = min(
+ options.max_width - 2, max(child_width, title_text.cell_len + 2)
+ )
+
+ width = child_width + 2
+ child_options = options.update(
+ width=child_width, height=child_height, highlight=self.highlight
+ )
+ lines = console.render_lines(renderable, child_options, style=style)
+
+ line_start = Segment(box.mid_left, border_style)
+ line_end = Segment(f"{box.mid_right}", border_style)
+ new_line = Segment.line()
+ if title_text is None or width <= 4:
+ yield Segment(box.get_top([width - 2]), border_style)
+ else:
+ title_text.align(self.title_align, width - 4, character=box.top)
+ yield Segment(box.top_left + box.top, border_style)
+ yield from console.render(title_text)
+ yield Segment(box.top + box.top_right, border_style)
+
+ yield new_line
+ for line in lines:
+ yield line_start
+ yield from line
+ yield line_end
+ yield new_line
+ yield Segment(box.get_bottom([width - 2]), border_style)
+ yield new_line
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
+ _title = self._title
+ _, right, _, left = Padding.unpack(self.padding)
+ padding = left + right
+ renderables = [self.renderable, _title] if _title else [self.renderable]
+
+ if self.width is None:
+ width = (
+ measure_renderables(
+ console, renderables, max_width - padding - 2
+ ).maximum
+ + padding
+ + 2
+ )
+ else:
+ width = self.width
+ return Measurement(width, width)
+
+
+if __name__ == "__main__": # pragma: no cover
+ from .console import Console
+
+ c = Console()
+
+ from .padding import Padding
+ from .box import ROUNDED, DOUBLE
+
+ p = Panel(
+ Panel.fit(
+ Text.from_markup("[bold magenta]Hello World!"),
+ box=ROUNDED,
+ safe_box=True,
+ style="on red",
+ ),
+ title="[b]Hello, World",
+ box=DOUBLE,
+ )
+
+ print(p)
+ c.print(p)
diff --git a/rich/pretty.py b/rich/pretty.py
new file mode 100644
index 0000000..8a05992
--- /dev/null
+++ b/rich/pretty.py
@@ -0,0 +1,585 @@
+import builtins
+import os
+import sys
+from array import array
+from collections import Counter, abc, defaultdict, deque
+from dataclasses import dataclass
+from itertools import islice
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ List,
+ Optional,
+ Set,
+ Tuple,
+)
+
+from rich.highlighter import ReprHighlighter
+
+from .abc import RichRenderable
+from . import get_console
+from ._pick import pick_bool
+from .cells import cell_len
+from .highlighter import ReprHighlighter
+from .jupyter import JupyterRenderable
+from .measure import Measurement
+from .text import Text
+
+if TYPE_CHECKING:
+ from .console import (
+ Console,
+ ConsoleOptions,
+ HighlighterType,
+ JustifyMethod,
+ OverflowMethod,
+ RenderResult,
+ )
+
+
+def install(
+ console: "Console" = None,
+ overflow: "OverflowMethod" = "ignore",
+ crop: bool = False,
+ indent_guides: bool = False,
+ max_length: int = None,
+ max_string: int = None,
+ expand_all: bool = False,
+) -> None:
+ """Install automatic pretty printing in the Python REPL.
+
+ Args:
+ console (Console, optional): Console instance or ``None`` to use global console. Defaults to None.
+ overflow (Optional[OverflowMethod], optional): Overflow method. Defaults to "ignore".
+ crop (Optional[bool], optional): Enable cropping of long lines. Defaults to False.
+ indent_guides (bool, optional): Enable indentation guides. Defaults to False.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
+ expand_all (bool, optional): Expand all containers. Defaults to False
+ """
+ from rich import get_console
+ from .console import ConsoleRenderable # needed here to prevent circular import
+
+ console = console or get_console()
+ assert console is not None
+
+ def display_hook(value: Any) -> None:
+ """Replacement sys.displayhook which prettifies objects with Rich."""
+ if value is not None:
+ assert console is not None
+ builtins._ = None # type: ignore
+ console.print(
+ value
+ if isinstance(value, RichRenderable)
+ else Pretty(
+ value,
+ overflow=overflow,
+ indent_guides=indent_guides,
+ max_length=max_length,
+ max_string=max_string,
+ expand_all=expand_all,
+ ),
+ crop=crop,
+ )
+ builtins._ = value # type: ignore
+
+ def ipy_display_hook(value: Any) -> None: # pragma: no cover
+ assert console is not None
+ # always skip rich generated jupyter renderables or None values
+ if isinstance(value, JupyterRenderable) or value is None:
+ return
+ # on jupyter rich display, if using one of the special representations dont use rich
+ if console.is_jupyter and any(attr.startswith("_repr_") for attr in dir(value)):
+ return
+
+ # certain renderables should start on a new line
+ if isinstance(value, ConsoleRenderable):
+ console.line()
+
+ console.print(
+ value
+ if isinstance(value, RichRenderable)
+ else Pretty(
+ value,
+ overflow=overflow,
+ indent_guides=indent_guides,
+ max_length=max_length,
+ max_string=max_string,
+ expand_all=expand_all,
+ margin=12,
+ ),
+ crop=crop,
+ )
+
+ try: # pragma: no cover
+ ip = get_ipython() # type: ignore
+ from IPython.core.formatters import BaseFormatter
+
+ # replace plain text formatter with rich formatter
+ rich_formatter = BaseFormatter()
+ rich_formatter.for_type(object, func=ipy_display_hook)
+ ip.display_formatter.formatters["text/plain"] = rich_formatter
+ except Exception:
+ sys.displayhook = display_hook
+
+
+class Pretty:
+ """A rich renderable that pretty prints an object.
+
+ Args:
+ _object (Any): An object to pretty print.
+ highlighter (HighlighterType, optional): Highlighter object to apply to result, or None for ReprHighlighter. Defaults to None.
+ indent_size (int, optional): Number of spaces in indent. Defaults to 4.
+ justify (JustifyMethod, optional): Justify method, or None for default. Defaults to None.
+ overflow (OverflowMethod, optional): Overflow method, or None for default. Defaults to None.
+ no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to False.
+ indent_guides (bool, optional): Enable indentation guides. Defaults to False.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
+ expand_all (bool, optional): Expand all containers. Defaults to False.
+ margin (int, optional): Subtrace a margin from width to force containers to expand earlier. Defaults to 0.
+ insert_line (bool, optional): Insert a new line if the output has multiple new lines. Defaults to False.
+ """
+
+ def __init__(
+ self,
+ _object: Any,
+ highlighter: "HighlighterType" = None,
+ *,
+ indent_size: int = 4,
+ justify: "JustifyMethod" = None,
+ overflow: Optional["OverflowMethod"] = None,
+ no_wrap: Optional[bool] = False,
+ indent_guides: bool = False,
+ max_length: int = None,
+ max_string: int = None,
+ expand_all: bool = False,
+ margin: int = 0,
+ insert_line: bool = False,
+ ) -> None:
+ self._object = _object
+ self.highlighter = highlighter or ReprHighlighter()
+ self.indent_size = indent_size
+ self.justify = justify
+ self.overflow = overflow
+ self.no_wrap = no_wrap
+ self.indent_guides = indent_guides
+ self.max_length = max_length
+ self.max_string = max_string
+ self.expand_all = expand_all
+ self.margin = margin
+ self.insert_line = insert_line
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ pretty_str = pretty_repr(
+ self._object,
+ max_width=options.max_width - self.margin,
+ indent_size=self.indent_size,
+ max_length=self.max_length,
+ max_string=self.max_string,
+ expand_all=self.expand_all,
+ )
+ pretty_text = Text(
+ pretty_str,
+ justify=self.justify or options.justify,
+ overflow=self.overflow or options.overflow,
+ no_wrap=pick_bool(self.no_wrap, options.no_wrap),
+ style="pretty",
+ )
+ pretty_text = self.highlighter(pretty_text)
+ if self.indent_guides and not options.ascii_only:
+ pretty_text = pretty_text.with_indent_guides(
+ self.indent_size, style="repr.indent"
+ )
+ if self.insert_line and "\n" in pretty_text:
+ yield ""
+ yield pretty_text
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
+ pretty_str = pretty_repr(
+ self._object,
+ max_width=max_width,
+ indent_size=self.indent_size,
+ max_length=self.max_length,
+ max_string=self.max_string,
+ )
+ text_width = max(cell_len(line) for line in pretty_str.splitlines())
+ return Measurement(text_width, text_width)
+
+
+def _get_braces_for_defaultdict(_object: defaultdict) -> Tuple[str, str, str]:
+ return (
+ f"defaultdict({_object.default_factory!r}, {{",
+ "})",
+ f"defaultdict({_object.default_factory!r}, {{}})",
+ )
+
+
+def _get_braces_for_array(_object: array) -> Tuple[str, str, str]:
+ return (f"array({_object.typecode!r}, [", "])", "array({_object.typecode!r})")
+
+
+_BRACES: Dict[type, Callable[[Any], Tuple[str, str, str]]] = {
+ os._Environ: lambda _object: ("environ({", "})", "environ({})"),
+ array: _get_braces_for_array,
+ defaultdict: _get_braces_for_defaultdict,
+ Counter: lambda _object: ("Counter({", "})", "Counter()"),
+ deque: lambda _object: ("deque([", "])", "deque()"),
+ dict: lambda _object: ("{", "}", "{}"),
+ frozenset: lambda _object: ("frozenset({", "})", "frozenset()"),
+ list: lambda _object: ("[", "]", "[]"),
+ set: lambda _object: ("{", "}", "set()"),
+ tuple: lambda _object: ("(", ")", "()"),
+}
+_CONTAINERS = tuple(_BRACES.keys())
+_MAPPING_CONTAINERS = (dict, os._Environ)
+
+
+@dataclass
+class Node:
+ """A node in a repr tree. May be atomic or a container."""
+
+ key_repr: str = ""
+ value_repr: str = ""
+ open_brace: str = ""
+ close_brace: str = ""
+ empty: str = ""
+ last: bool = False
+ is_tuple: bool = False
+ children: Optional[List["Node"]] = None
+
+ @property
+ def separator(self) -> str:
+ """Get separator between items."""
+ return "" if self.last else ","
+
+ def iter_tokens(self) -> Iterable[str]:
+ """Generate tokens for this node."""
+ if self.key_repr:
+ yield self.key_repr
+ yield ": "
+ if self.value_repr:
+ yield self.value_repr
+ elif self.children is not None:
+ if self.children:
+ yield self.open_brace
+ if self.is_tuple and len(self.children) == 1:
+ yield from self.children[0].iter_tokens()
+ yield ","
+ else:
+ for child in self.children:
+ yield from child.iter_tokens()
+ if not child.last:
+ yield ", "
+ yield self.close_brace
+ else:
+ yield self.empty
+
+ def check_length(self, start_length: int, max_length: int) -> bool:
+ """Check the length fits within a limit.
+
+ Args:
+ start_length (int): Starting length of the line (indent, prefix, suffix).
+ max_length (int): Maximum length.
+
+ Returns:
+ bool: True if the node can be rendered within max length, otherwise False.
+ """
+ total_length = start_length
+ for token in self.iter_tokens():
+ total_length += cell_len(token)
+ if total_length > max_length:
+ return False
+ return True
+
+ def __str__(self) -> str:
+ repr_text = "".join(self.iter_tokens())
+ return repr_text
+
+ def render(
+ self, max_width: int = 80, indent_size: int = 4, expand_all: bool = False
+ ) -> str:
+ """Render the node to a pretty repr.
+
+ Args:
+ max_width (int, optional): Maximum width of the repr. Defaults to 80.
+ indent_size (int, optional): Size of indents. Defaults to 4.
+ expand_all (bool, optional): Expand all levels. Defaults to False.
+
+ Returns:
+ str: A repr string of the original object.
+ """
+ lines = [_Line(node=self, is_root=True)]
+ line_no = 0
+ while line_no < len(lines):
+ line = lines[line_no]
+ if line.expandable and not line.expanded:
+ if expand_all or not line.check_length(max_width):
+ lines[line_no : line_no + 1] = line.expand(indent_size)
+ line_no += 1
+
+ repr_str = "\n".join(str(line) for line in lines)
+ return repr_str
+
+
+@dataclass
+class _Line:
+ """A line in repr output."""
+
+ is_root: bool = False
+ node: Optional[Node] = None
+ text: str = ""
+ suffix: str = ""
+ whitespace: str = ""
+ expanded: bool = False
+
+ @property
+ def expandable(self) -> bool:
+ """Check if the line may be expanded."""
+ return bool(self.node is not None and self.node.children)
+
+ def check_length(self, max_length: int) -> bool:
+ """Check this line fits within a given number of cells."""
+ start_length = (
+ len(self.whitespace) + cell_len(self.text) + cell_len(self.suffix)
+ )
+ assert self.node is not None
+ return self.node.check_length(start_length, max_length)
+
+ def expand(self, indent_size: int) -> Iterable["_Line"]:
+ """Expand this line by adding children on their own line."""
+ node = self.node
+ assert node is not None
+ whitespace = self.whitespace
+ assert node.children
+ if node.key_repr:
+ yield _Line(
+ text=f"{node.key_repr}: {node.open_brace}", whitespace=whitespace
+ )
+ else:
+ yield _Line(text=node.open_brace, whitespace=whitespace)
+ child_whitespace = self.whitespace + " " * indent_size
+ tuple_of_one = node.is_tuple and len(node.children) == 1
+ for child in node.children:
+ separator = "," if tuple_of_one else child.separator
+ line = _Line(
+ node=child,
+ whitespace=child_whitespace,
+ suffix=separator,
+ )
+ yield line
+
+ yield _Line(
+ text=node.close_brace,
+ whitespace=whitespace,
+ suffix="," if (tuple_of_one and not self.is_root) else node.separator,
+ )
+
+ def __str__(self) -> str:
+ return f"{self.whitespace}{self.text}{self.node or ''}{self.suffix}"
+
+
+def traverse(_object: Any, max_length: int = None, max_string: int = None) -> Node:
+ """Traverse object and generate a tree.
+
+ Args:
+ _object (Any): Object to be traversed.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of string before truncating, or None to disable truncating.
+ Defaults to None.
+
+ Returns:
+ Node: The root of a tree structure which can be used to render a pretty repr.
+ """
+
+ def to_repr(obj: Any) -> str:
+ """Get repr string for an object, but catch errors."""
+ if (
+ max_string is not None
+ and isinstance(obj, (bytes, str))
+ and len(obj) > max_string
+ ):
+ truncated = len(obj) - max_string
+ obj_repr = f"{obj[:max_string]!r}+{truncated}"
+ else:
+ try:
+ obj_repr = repr(obj)
+ except Exception as error:
+ obj_repr = f"<repr-error '{error}'>"
+ return obj_repr
+
+ visited_ids: Set[int] = set()
+ push_visited = visited_ids.add
+ pop_visited = visited_ids.remove
+
+ def _traverse(obj: Any, root: bool = False) -> Node:
+ """Walk the object depth first."""
+ obj_type = type(obj)
+ if obj_type in _CONTAINERS:
+ obj_id = id(obj)
+
+ if obj_id in visited_ids:
+ # Recursion detected
+ return Node(value_repr="...")
+ push_visited(obj_id)
+ open_brace, close_brace, empty = _BRACES[obj_type](obj)
+
+ if obj:
+ children: List[Node] = []
+ node = Node(
+ open_brace=open_brace,
+ close_brace=close_brace,
+ children=children,
+ last=root,
+ )
+ append = children.append
+ num_items = len(obj)
+ last_item_index = num_items - 1
+
+ if isinstance(obj, _MAPPING_CONTAINERS):
+ iter_items = iter(obj.items())
+ if max_length is not None:
+ iter_items = islice(iter_items, max_length)
+ for index, (key, child) in enumerate(iter_items):
+ child_node = _traverse(child)
+ child_node.key_repr = to_repr(key)
+ child_node.last = index == last_item_index
+ append(child_node)
+ else:
+ iter_values = iter(obj)
+ if max_length is not None:
+ iter_values = islice(iter_values, max_length)
+ for index, child in enumerate(iter_values):
+ child_node = _traverse(child)
+ child_node.last = index == last_item_index
+ append(child_node)
+ if max_length is not None and num_items > max_length:
+ append(Node(value_repr=f"... +{num_items-max_length}", last=True))
+ else:
+ node = Node(empty=empty, children=[], last=root)
+
+ pop_visited(obj_id)
+ else:
+ node = Node(value_repr=to_repr(obj), last=root)
+ node.is_tuple = isinstance(obj, tuple)
+ return node
+
+ node = _traverse(_object, root=True)
+ return node
+
+
+def pretty_repr(
+ _object: Any,
+ *,
+ max_width: int = 80,
+ indent_size: int = 4,
+ max_length: int = None,
+ max_string: int = None,
+ expand_all: bool = False,
+) -> str:
+ """Prettify repr string by expanding on to new lines to fit within a given width.
+
+ Args:
+ _object (Any): Object to repr.
+ max_width (int, optional): Desired maximum width of repr string. Defaults to 80.
+ indent_size (int, optional): Number of spaces to indent. Defaults to 4.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of string before truncating, or None to disable truncating.
+ Defaults to None.
+ expand_all (bool, optional): Expand all containers regardless of available width. Defaults to False.
+
+ Returns:
+ str: A possibly multi-line representation of the object.
+ """
+
+ if isinstance(_object, Node):
+ node = _object
+ else:
+ node = traverse(_object, max_length=max_length, max_string=max_string)
+ repr_str = node.render(
+ max_width=max_width, indent_size=indent_size, expand_all=expand_all
+ )
+ return repr_str
+
+
+def pprint(
+ _object: Any,
+ *,
+ console: "Console" = None,
+ indent_guides: bool = True,
+ max_length: int = None,
+ max_string: int = None,
+ expand_all: bool = False,
+):
+ """A convenience function for pretty printing.
+
+ Args:
+ _object (Any): Object to pretty print.
+ console (Console, optional): Console instance, or None to use default. Defaults to None.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of strings before truncating, or None to disable. Defaults to None.
+ indent_guides (bool, optional): Enable indentation guides. Defaults to True.
+ expand_all (bool, optional): Expand all containers. Defaults to False.
+ """
+ _console = get_console() if console is None else console
+ _console.print(
+ Pretty(
+ _object,
+ max_length=max_length,
+ max_string=max_string,
+ indent_guides=indent_guides,
+ expand_all=expand_all,
+ overflow="ignore",
+ ),
+ soft_wrap=True,
+ )
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ class BrokenRepr:
+ def __repr__(self):
+ 1 / 0
+
+ d = defaultdict(int)
+ d["foo"] = 5
+ data = {
+ "foo": [
+ 1,
+ "Hello World!",
+ 100.123,
+ 323.232,
+ 432324.0,
+ {5, 6, 7, (1, 2, 3, 4), 8},
+ ],
+ "bar": frozenset({1, 2, 3}),
+ "defaultdict": defaultdict(
+ list, {"crumble": ["apple", "rhubarb", "butter", "sugar", "flour"]}
+ ),
+ "counter": Counter(
+ [
+ "apple",
+ "orange",
+ "pear",
+ "kumquat",
+ "kumquat",
+ "durian" * 100,
+ ]
+ ),
+ "atomic": (False, True, None),
+ "Broken": BrokenRepr(),
+ }
+ data["foo"].append(data) # type: ignore
+
+ from rich import print
+
+ print(Pretty(data, indent_guides=True, max_string=20))
diff --git a/rich/progress.py b/rich/progress.py
new file mode 100644
index 0000000..2fd1f3d
--- /dev/null
+++ b/rich/progress.py
@@ -0,0 +1,1019 @@
+import sys
+from abc import ABC, abstractmethod
+from collections import deque
+from collections.abc import Sized
+from dataclasses import dataclass, field
+from datetime import timedelta
+from math import ceil
+from threading import Event, RLock, Thread
+from typing import (
+ Any,
+ Callable,
+ Deque,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ NewType,
+ Optional,
+ Sequence,
+ Tuple,
+ TypeVar,
+ Union,
+)
+
+from . import filesize, get_console
+from .console import (
+ Console,
+ JustifyMethod,
+ RenderableType,
+ RenderGroup,
+)
+from .jupyter import JupyterMixin
+from .highlighter import Highlighter
+from .live import Live
+from .progress_bar import ProgressBar
+from .spinner import Spinner
+from .style import StyleType
+from .table import Column, Table
+from .text import Text, TextType
+
+TaskID = NewType("TaskID", int)
+
+ProgressType = TypeVar("ProgressType")
+
+GetTimeCallable = Callable[[], float]
+
+
+class _TrackThread(Thread):
+ """A thread to periodically update progress."""
+
+ def __init__(self, progress: "Progress", task_id: "TaskID", update_period: float):
+ self.progress = progress
+ self.task_id = task_id
+ self.update_period = update_period
+ self.done = Event()
+
+ self.completed = 0
+ super().__init__()
+
+ def run(self) -> None:
+ task_id = self.task_id
+ advance = self.progress.advance
+ update_period = self.update_period
+ last_completed = 0
+ wait = self.done.wait
+ while not wait(update_period):
+ completed = self.completed
+ if last_completed != completed:
+ advance(task_id, completed - last_completed)
+ last_completed = completed
+
+ self.progress.update(self.task_id, completed=self.completed, refresh=True)
+
+ def __enter__(self) -> "_TrackThread":
+ self.start()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+ self.done.set()
+ self.join()
+
+
+def track(
+ sequence: Union[Sequence[ProgressType], Iterable[ProgressType]],
+ description="Working...",
+ total: int = None,
+ auto_refresh=True,
+ console: Optional[Console] = None,
+ transient: bool = False,
+ get_time: Callable[[], float] = None,
+ refresh_per_second: float = 10,
+ style: StyleType = "bar.back",
+ complete_style: StyleType = "bar.complete",
+ finished_style: StyleType = "bar.finished",
+ pulse_style: StyleType = "bar.pulse",
+ update_period: float = 0.1,
+ disable: bool = False,
+) -> Iterable[ProgressType]:
+ """Track progress by iterating over a sequence.
+
+ Args:
+ 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.
+ transient: (bool, optional): Clear the progress on exit. Defaults to False.
+ console (Console, optional): Console to write to. Default creates internal Console instance.
+ refresh_per_second (float): Number of times per second to refresh the progress information. Defaults to 10.
+ style (StyleType, optional): Style for the bar background. Defaults to "bar.back".
+ complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete".
+ finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
+ pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
+ update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.1.
+ disable (bool, optional): Disable display of progress.
+ Returns:
+ Iterable[ProgressType]: An iterable of the values in the sequence.
+
+ """
+
+ columns: List["ProgressColumn"] = (
+ [TextColumn("[progress.description]{task.description}")] if description else []
+ )
+ columns.extend(
+ (
+ BarColumn(
+ style=style,
+ complete_style=complete_style,
+ finished_style=finished_style,
+ pulse_style=pulse_style,
+ ),
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
+ TimeRemainingColumn(),
+ )
+ )
+ progress = Progress(
+ *columns,
+ auto_refresh=auto_refresh,
+ console=console,
+ transient=transient,
+ get_time=get_time,
+ refresh_per_second=refresh_per_second or 10,
+ disable=disable,
+ )
+
+ with progress:
+ yield from progress.track(
+ sequence, total=total, description=description, update_period=update_period
+ )
+
+
+class ProgressColumn(ABC):
+ """Base class for a widget to use in progress display."""
+
+ max_refresh: Optional[float] = None
+
+ def __init__(self, table_column: Column = None) -> None:
+ self._table_column = table_column
+ self._renderable_cache: Dict[TaskID, Tuple[float, RenderableType]] = {}
+ self._update_time: Optional[float] = None
+
+ def get_table_column(self) -> Column:
+ """Get a table column, used to build tasks table."""
+ return self._table_column or Column()
+
+ def __call__(self, task: "Task") -> RenderableType:
+ """Called by the Progress object to return a renderable for the given task.
+
+ Args:
+ task (Task): An object containing information regarding the task.
+
+ Returns:
+ RenderableType: Anything renderable (including str).
+ """
+ current_time = task.get_time() # type: ignore
+ if self.max_refresh is not None and not task.completed:
+ try:
+ timestamp, renderable = self._renderable_cache[task.id]
+ except KeyError:
+ pass
+ else:
+ if timestamp + self.max_refresh > current_time:
+ return renderable
+
+ renderable = self.render(task)
+ self._renderable_cache[task.id] = (current_time, renderable)
+ return renderable
+
+ @abstractmethod
+ def render(self, task: "Task") -> RenderableType:
+ """Should return a renderable object."""
+
+
+class RenderableColumn(ProgressColumn):
+ """A column to insert an arbitrary column.
+
+ Args:
+ renderable (RenderableType, optional): Any renderable. Defaults to empty string.
+ """
+
+ def __init__(self, renderable: RenderableType = "", *, table_column: Column = None):
+ self.renderable = renderable
+ super().__init__(table_column=table_column)
+
+ def render(self, task: "Task") -> RenderableType:
+ return self.renderable
+
+
+class SpinnerColumn(ProgressColumn):
+ """A column with a 'spinner' animation.
+
+ Args:
+ spinner_name (str, optional): Name of spinner animation. Defaults to "dots".
+ style (StyleType, optional): Style of spinner. Defaults to "progress.spinner".
+ speed (float, optional): Speed factor of spinner. Defaults to 1.0.
+ finished_text (TextType, optional): Text used when task is finished. Defaults to " ".
+ """
+
+ def __init__(
+ self,
+ spinner_name: str = "dots",
+ style: Optional[StyleType] = "progress.spinner",
+ speed: float = 1.0,
+ finished_text: TextType = " ",
+ table_column: Column = None,
+ ):
+ self.spinner = Spinner(spinner_name, style=style, speed=speed)
+ self.finished_text = (
+ Text.from_markup(finished_text)
+ if isinstance(finished_text, str)
+ else finished_text
+ )
+ super().__init__(table_column=table_column)
+
+ def set_spinner(
+ self,
+ spinner_name: str,
+ spinner_style: Optional[StyleType] = "progress.spinner",
+ speed: float = 1.0,
+ ):
+ """Set a new spinner.
+
+ Args:
+ spinner_name (str): Spinner name, see python -m rich.spinner.
+ spinner_style (Optional[StyleType], optional): Spinner style. Defaults to "progress.spinner".
+ speed (float, optional): Speed factor of spinner. Defaults to 1.0.
+ """
+ self.spinner = Spinner(spinner_name, style=spinner_style, speed=speed)
+
+ def render(self, task: "Task") -> Text:
+ text = (
+ self.finished_text
+ if task.finished
+ else self.spinner.render(task.get_time())
+ )
+ return text
+
+
+class TextColumn(ProgressColumn):
+ """A column containing text."""
+
+ def __init__(
+ self,
+ text_format: str,
+ style: StyleType = "none",
+ justify: JustifyMethod = "left",
+ markup: bool = True,
+ highlighter: Highlighter = None,
+ table_column: Column = None,
+ ) -> None:
+ self.text_format = text_format
+ self.justify = justify
+ self.style = style
+ self.markup = markup
+ self.highlighter = highlighter
+ super().__init__(table_column=table_column or Column(no_wrap=True))
+
+ def render(self, task: "Task") -> Text:
+ _text = self.text_format.format(task=task)
+ if self.markup:
+ text = Text.from_markup(_text, style=self.style, justify=self.justify)
+ else:
+ text = Text(_text, style=self.style, justify=self.justify)
+ if self.highlighter:
+ self.highlighter.highlight(text)
+ return text
+
+
+class BarColumn(ProgressColumn):
+ """Renders a visual progress bar.
+
+ Args:
+ bar_width (Optional[int], optional): Width of bar or None for full width. Defaults to 40.
+ style (StyleType, optional): Style for the bar background. Defaults to "bar.back".
+ complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete".
+ finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
+ pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
+ """
+
+ def __init__(
+ self,
+ bar_width: Optional[int] = 40,
+ style: StyleType = "bar.back",
+ complete_style: StyleType = "bar.complete",
+ finished_style: StyleType = "bar.finished",
+ pulse_style: StyleType = "bar.pulse",
+ table_column: Column = None,
+ ) -> None:
+ self.bar_width = bar_width
+ self.style = style
+ self.complete_style = complete_style
+ self.finished_style = finished_style
+ self.pulse_style = pulse_style
+ super().__init__(table_column=table_column)
+
+ def render(self, task: "Task") -> ProgressBar:
+ """Gets a progress bar widget for a task."""
+ return ProgressBar(
+ total=max(0, task.total),
+ completed=max(0, task.completed),
+ width=None if self.bar_width is None else max(1, self.bar_width),
+ pulse=not task.started,
+ animation_time=task.get_time(),
+ style=self.style,
+ complete_style=self.complete_style,
+ finished_style=self.finished_style,
+ pulse_style=self.pulse_style,
+ )
+
+
+class TimeElapsedColumn(ProgressColumn):
+ """Renders time elapsed."""
+
+ def render(self, task: "Task") -> Text:
+ """Show time remaining."""
+ elapsed = task.finished_time if task.finished else task.elapsed
+ if elapsed is None:
+ return Text("-:--:--", style="progress.elapsed")
+ delta = timedelta(seconds=int(elapsed))
+ return Text(str(delta), style="progress.elapsed")
+
+
+class TimeRemainingColumn(ProgressColumn):
+ """Renders estimated time remaining."""
+
+ # Only refresh twice a second to prevent jitter
+ max_refresh = 0.5
+
+ def render(self, task: "Task") -> Text:
+ """Show time remaining."""
+ remaining = task.time_remaining
+ if remaining is None:
+ return Text("-:--:--", style="progress.remaining")
+ remaining_delta = timedelta(seconds=int(remaining))
+ return Text(str(remaining_delta), style="progress.remaining")
+
+
+class FileSizeColumn(ProgressColumn):
+ """Renders completed filesize."""
+
+ def render(self, task: "Task") -> Text:
+ """Show data completed."""
+ data_size = filesize.decimal(int(task.completed))
+ return Text(data_size, style="progress.filesize")
+
+
+class TotalFileSizeColumn(ProgressColumn):
+ """Renders total filesize."""
+
+ def render(self, task: "Task") -> Text:
+ """Show data completed."""
+ data_size = filesize.decimal(int(task.total))
+ return Text(data_size, style="progress.filesize.total")
+
+
+class DownloadColumn(ProgressColumn):
+ """Renders file size downloaded and total, e.g. '0.5/2.3 GB'.
+
+ Args:
+ binary_units (bool, optional): Use binary units, KiB, MiB etc. Defaults to False.
+ """
+
+ def __init__(self, binary_units: bool = False, table_column: Column = None) -> None:
+ self.binary_units = binary_units
+ super().__init__(table_column=table_column)
+
+ def render(self, task: "Task") -> Text:
+ """Calculate common unit for completed and total."""
+ completed = int(task.completed)
+ total = int(task.total)
+ if self.binary_units:
+ unit, suffix = filesize.pick_unit_and_suffix(
+ total,
+ ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"],
+ 1024,
+ )
+ else:
+ unit, suffix = filesize.pick_unit_and_suffix(
+ total, ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"], 1000
+ )
+ completed_ratio = completed / unit
+ total_ratio = total / unit
+ precision = 0 if unit == 1 else 1
+ completed_str = f"{completed_ratio:,.{precision}f}"
+ total_str = f"{total_ratio:,.{precision}f}"
+ download_status = f"{completed_str}/{total_str} {suffix}"
+ download_text = Text(download_status, style="progress.download")
+ return download_text
+
+
+class TransferSpeedColumn(ProgressColumn):
+ """Renders human readable transfer speed."""
+
+ def render(self, task: "Task") -> Text:
+ """Show data transfer speed."""
+ speed = task.speed
+ if speed is None:
+ return Text("?", style="progress.data.speed")
+ data_speed = filesize.decimal(int(speed))
+ return Text(f"{data_speed}/s", style="progress.data.speed")
+
+
+class ProgressSample(NamedTuple):
+ """Sample of progress for a given time."""
+
+ timestamp: float
+ """Timestamp of sample."""
+ completed: float
+ """Number of steps completed."""
+
+
+@dataclass
+class Task:
+ """Information regarding a progress task.
+
+ This object should be considered read-only outside of the :class:`~Progress` class.
+
+ """
+
+ id: TaskID
+ """Task ID associated with this task (used in Progress methods)."""
+
+ description: str
+ """str: Description of the task."""
+
+ total: float
+ """str: Total number of steps in this task."""
+
+ completed: float
+ """float: Number of steps completed"""
+
+ _get_time: GetTimeCallable
+ """Callable to get the current time."""
+
+ finished_time: Optional[float] = None
+ """float: Time task was finished."""
+
+ visible: bool = True
+ """bool: Indicates if this task is visible in the progress display."""
+
+ fields: Dict[str, Any] = field(default_factory=dict)
+ """dict: Arbitrary fields passed in via Progress.update."""
+
+ start_time: Optional[float] = field(default=None, init=False, repr=False)
+ """Optional[float]: Time this task was started, or None if not started."""
+
+ stop_time: Optional[float] = field(default=None, init=False, repr=False)
+ """Optional[float]: Time this task was stopped, or None if not stopped."""
+
+ _progress: Deque[ProgressSample] = field(
+ default_factory=deque, init=False, repr=False
+ )
+
+ _lock: RLock = field(repr=False, default_factory=RLock)
+ """Thread lock."""
+
+ def get_time(self) -> float:
+ """float: Get the current time, in seconds."""
+ return self._get_time() # type: ignore
+
+ @property
+ def started(self) -> bool:
+ """bool: Check if the task as started."""
+ return self.start_time is not None
+
+ @property
+ def remaining(self) -> float:
+ """float: Get the number of steps remaining."""
+ return self.total - self.completed
+
+ @property
+ def elapsed(self) -> Optional[float]:
+ """Optional[float]: Time elapsed since task was started, or ``None`` if the task hasn't started."""
+ if self.start_time is None:
+ return None
+ if self.stop_time is not None:
+ return self.stop_time - self.start_time
+ return self.get_time() - self.start_time
+
+ @property
+ def finished(self) -> bool:
+ """Check if the task has finished."""
+ return self.finished_time is not None
+
+ @property
+ def percentage(self) -> float:
+ """float: Get progress of task as a percentage."""
+ if not self.total:
+ return 0.0
+ completed = (self.completed / self.total) * 100.0
+ completed = min(100.0, max(0.0, completed))
+ return completed
+
+ @property
+ def speed(self) -> Optional[float]:
+ """Optional[float]: Get the estimated speed in steps per second."""
+ if self.start_time is None:
+ return None
+ with self._lock:
+ progress = self._progress
+ if not progress:
+ return None
+ total_time = progress[-1].timestamp - progress[0].timestamp
+ if total_time == 0:
+ return None
+ iter_progress = iter(progress)
+ next(iter_progress)
+ total_completed = sum(sample.completed for sample in iter_progress)
+ speed = total_completed / total_time
+ return speed
+
+ @property
+ def time_remaining(self) -> Optional[float]:
+ """Optional[float]: Get estimated time to completion, or ``None`` if no data."""
+ if self.finished:
+ return 0.0
+ speed = self.speed
+ if not speed:
+ return None
+ estimate = ceil(self.remaining / speed)
+ return estimate
+
+ def _reset(self) -> None:
+ """Reset progress."""
+ self._progress.clear()
+ self.finished_time = None
+
+
+class Progress(JupyterMixin):
+ """Renders an auto-updating progress bar(s).
+
+ Args:
+ console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout.
+ auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()`.
+ refresh_per_second (Optional[float], optional): Number of times per second to refresh the progress information or None to use default (10). Defaults to None.
+ speed_estimate_period: (float, optional): Period (in seconds) used to calculate the speed estimate. Defaults to 30.
+ transient: (bool, optional): Clear the progress on exit. Defaults to False.
+ redirect_stdout: (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True.
+ redirect_stderr: (bool, optional): Enable redirection of stderr. Defaults to True.
+ get_time: (Callable, optional): A callable that gets the current time, or None to use Console.get_time. Defaults to None.
+ disable (bool, optional): Disable progress display. Defaults to False
+ expand (bool, optional): Expand tasks table to fit width. Defaults to False.
+ """
+
+ def __init__(
+ self,
+ *columns: Union[str, ProgressColumn],
+ console: Console = None,
+ auto_refresh: bool = True,
+ refresh_per_second: float = 10,
+ speed_estimate_period: float = 30.0,
+ transient: bool = False,
+ redirect_stdout: bool = True,
+ redirect_stderr: bool = True,
+ get_time: GetTimeCallable = None,
+ disable: bool = False,
+ expand: bool = False,
+ ) -> None:
+ assert (
+ refresh_per_second is None or refresh_per_second > 0 # type: ignore
+ ), "refresh_per_second must be > 0"
+ self._lock = RLock()
+ self.columns = columns or (
+ TextColumn("[progress.description]{task.description}"),
+ BarColumn(),
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
+ TimeRemainingColumn(),
+ )
+ self.speed_estimate_period = speed_estimate_period
+
+ self.disable = disable
+ self.expand = expand
+ self._tasks: Dict[TaskID, Task] = {}
+ self._task_index: TaskID = TaskID(0)
+ self.live = Live(
+ console=console or get_console(),
+ auto_refresh=auto_refresh,
+ refresh_per_second=refresh_per_second,
+ transient=transient,
+ redirect_stdout=redirect_stdout,
+ redirect_stderr=redirect_stderr,
+ get_renderable=self.get_renderable,
+ )
+ self.get_time = get_time or self.console.get_time
+ self.print = self.console.print
+ self.log = self.console.log
+
+ @property
+ def console(self) -> Console:
+ return self.live.console
+
+ @property
+ def tasks(self) -> List[Task]:
+ """Get a list of Task instances."""
+ with self._lock:
+ return list(self._tasks.values())
+
+ @property
+ def task_ids(self) -> List[TaskID]:
+ """A list of task IDs."""
+ with self._lock:
+ return list(self._tasks.keys())
+
+ @property
+ def finished(self) -> bool:
+ """Check if all tasks have been completed."""
+ with self._lock:
+ if not self._tasks:
+ return True
+ return all(task.finished for task in self._tasks.values())
+
+ def start(self) -> None:
+ """Start the progress display."""
+ self.live.start(refresh=True)
+
+ def stop(self) -> None:
+ """Stop the progress display."""
+ self.live.stop()
+
+ def __enter__(self) -> "Progress":
+ self.start()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+ self.stop()
+
+ def track(
+ self,
+ sequence: Union[Iterable[ProgressType], Sequence[ProgressType]],
+ total: int = None,
+ task_id: Optional[TaskID] = None,
+ description="Working...",
+ update_period: float = 0.1,
+ ) -> Iterable[ProgressType]:
+ """Track progress by iterating over a sequence.
+
+ Args:
+ sequence (Sequence[ProgressType]): A sequence of values you want to iterate over and track progress.
+ 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.
+ update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.1.
+
+ Returns:
+ Iterable[ProgressType]: An iterable of values taken from the provided 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)
+
+ if self.live.auto_refresh:
+ with _TrackThread(self, task_id, update_period) as track_thread:
+ for value in sequence:
+ yield value
+ track_thread.completed += 1
+ else:
+ advance = self.advance
+ refresh = self.refresh
+ for value in sequence:
+ yield value
+ advance(task_id, 1)
+ refresh()
+
+ def start_task(self, task_id: TaskID) -> None:
+ """Start a task.
+
+ Starts a task (used when calculating elapsed time). You may need to call this manually,
+ if you called ``add_task`` with ``start=False``.
+
+ Args:
+ task_id (TaskID): ID of task.
+ """
+ with self._lock:
+ task = self._tasks[task_id]
+ if task.start_time is None:
+ task.start_time = self.get_time()
+
+ def stop_task(self, task_id: TaskID) -> None:
+ """Stop a task.
+
+ This will freeze the elapsed time on the task.
+
+ Args:
+ task_id (TaskID): ID of task.
+ """
+ with self._lock:
+ task = self._tasks[task_id]
+ current_time = self.get_time()
+ if task.start_time is None:
+ task.start_time = current_time
+ task.stop_time = current_time
+
+ def update(
+ self,
+ task_id: TaskID,
+ *,
+ total: float = None,
+ completed: float = None,
+ advance: float = None,
+ description: str = None,
+ visible: bool = None,
+ refresh: bool = False,
+ **fields: Any,
+ ) -> None:
+ """Update information associated with a task.
+
+ Args:
+ task_id (TaskID): Task id (returned by add_task).
+ total (float, optional): Updates task.total if not None.
+ completed (float, optional): Updates task.completed if not None.
+ advance (float, optional): Add a value to task.completed if not None.
+ description (str, optional): Change task description if not None.
+ visible (bool, optional): Set visible flag if not None.
+ refresh (bool): Force a refresh of progress information. Default is False.
+ **fields (Any): Additional data fields required for rendering.
+ """
+ with self._lock:
+ task = self._tasks[task_id]
+ completed_start = task.completed
+
+ if total is not None:
+ task.total = total
+ task._reset()
+ if advance is not None:
+ task.completed += advance
+ if completed is not None:
+ task.completed = completed
+ if description is not None:
+ task.description = description
+ if visible is not None:
+ task.visible = visible
+ task.fields.update(fields)
+ update_completed = task.completed - completed_start
+
+ if refresh:
+ self.refresh()
+
+ current_time = self.get_time()
+ old_sample_time = current_time - self.speed_estimate_period
+ _progress = task._progress
+
+ popleft = _progress.popleft
+ while _progress and _progress[0].timestamp < old_sample_time:
+ popleft()
+ while len(_progress) > 1000:
+ popleft()
+ if update_completed > 0:
+ _progress.append(ProgressSample(current_time, update_completed))
+ if task.completed >= task.total and task.finished_time is None:
+ task.finished_time = task.elapsed
+
+ def reset(
+ self,
+ task_id: TaskID,
+ *,
+ start: bool = True,
+ total: Optional[int] = None,
+ completed: int = 0,
+ visible: Optional[bool] = None,
+ description: Optional[str] = None,
+ **fields: Any,
+ ) -> None:
+ """Reset a task so completed is 0 and the clock is reset.
+
+ Args:
+ task_id (TaskID): ID of task.
+ start (bool, optional): Start the task after reset. Defaults to True.
+ total (int, optional): New total steps in task, or None to use current total. Defaults to None.
+ completed (int, optional): Number of steps completed. Defaults to 0.
+ **fields (str): Additional data fields required for rendering.
+ """
+ current_time = self.get_time()
+ with self._lock:
+ task = self._tasks[task_id]
+ task._reset()
+ task.start_time = current_time if start else None
+ if total is not None:
+ task.total = total
+ task.completed = completed
+ if visible is not None:
+ task.visible = visible
+ if fields:
+ task.fields = fields
+ if description is not None:
+ task.description = description
+ task.finished_time = None
+ 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.
+ """
+ current_time = self.get_time()
+ with self._lock:
+ task = self._tasks[task_id]
+ completed_start = task.completed
+ task.completed += advance
+ update_completed = task.completed - completed_start
+ old_sample_time = current_time - self.speed_estimate_period
+ _progress = task._progress
+
+ popleft = _progress.popleft
+ while _progress and _progress[0].timestamp < old_sample_time:
+ popleft()
+ while len(_progress) > 1000:
+ popleft()
+ _progress.append(ProgressSample(current_time, update_completed))
+ if task.completed >= task.total and task.finished_time is None:
+ task.finished_time = task.elapsed
+
+ def refresh(self) -> None:
+ """Refresh (render) the progress information."""
+ if not self.disable:
+ self.live.refresh()
+
+ def get_renderable(self) -> RenderableType:
+ """Get a renderable for the progress display."""
+ renderable = RenderGroup(*self.get_renderables())
+ return renderable
+
+ def get_renderables(self) -> Iterable[RenderableType]:
+ """Get a number of renderables for the progress display."""
+ table = self.make_tasks_table(self.tasks)
+ yield table
+
+ def make_tasks_table(self, tasks: Iterable[Task]) -> Table:
+ """Get a table to render the Progress display.
+
+ Args:
+ tasks (Iterable[Task]): An iterable of Task instances, one per row of the table.
+
+ Returns:
+ Table: A table instance.
+ """
+
+ table_columns = (
+ (
+ Column(no_wrap=True)
+ if isinstance(_column, str)
+ else _column.get_table_column().copy()
+ )
+ for _column in self.columns
+ )
+ table = Table.grid(*table_columns, padding=(0, 1), expand=self.expand)
+
+ for task in tasks:
+ if task.visible:
+ table.add_row(
+ *(
+ (
+ column.format(task=task)
+ if isinstance(column, str)
+ else column(task)
+ )
+ for column in self.columns
+ )
+ )
+ return table
+
+ def __rich__(self) -> RenderableType:
+ """Makes the Progress class itself renderable."""
+ return self.get_renderable()
+
+ def add_task(
+ self,
+ description: str,
+ start: bool = True,
+ total: int = 100,
+ completed: int = 0,
+ visible: bool = True,
+ **fields: Any,
+ ) -> TaskID:
+ """Add a new 'task' to the Progress display.
+
+ Args:
+ description (str): A description of the task.
+ start (bool, optional): Start the task immediately (to calculate elapsed time). If set to False,
+ you will need to call `start` manually. Defaults to True.
+ total (int, optional): Number of total steps in the progress if know. Defaults to 100.
+ completed (int, optional): Number of steps completed so far.. Defaults to 0.
+ visible (bool, optional): Enable display of the task. Defaults to True.
+ **fields (str): Additional data fields required for rendering.
+
+ Returns:
+ TaskID: An ID you can use when calling `update`.
+ """
+ with self._lock:
+ task = Task(
+ self._task_index,
+ description,
+ total,
+ completed,
+ visible=visible,
+ fields=fields,
+ _get_time=self.get_time,
+ _lock=self._lock,
+ )
+ self._tasks[self._task_index] = task
+ if start:
+ self.start_task(self._task_index)
+ self.refresh()
+ try:
+ return self._task_index
+ finally:
+ self._task_index = TaskID(int(self._task_index) + 1)
+
+ def remove_task(self, task_id: TaskID) -> None:
+ """Delete a task if it exists.
+
+ Args:
+ task_id (TaskID): A task ID.
+
+ """
+ with self._lock:
+ del self._tasks[task_id]
+
+
+if __name__ == "__main__": # pragma: no coverage
+
+ import random
+ import time
+
+ from .panel import Panel
+ from .rule import Rule
+ from .syntax import Syntax
+ from .table import Table
+
+ syntax = Syntax(
+ '''def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ for value in iter_values:
+ yield False, previous_value
+ previous_value = value
+ yield True, previous_value''',
+ "python",
+ line_numbers=True,
+ )
+
+ table = Table("foo", "bar", "baz")
+ table.add_row("1", "2", "3")
+
+ progress_renderables = [
+ "Text may be printed while the progress bars are rendering.",
+ Panel("In fact, [i]any[/i] renderable will work"),
+ "Such as [magenta]tables[/]...",
+ table,
+ "Pretty printed structures...",
+ {"type": "example", "text": "Pretty printed"},
+ "Syntax...",
+ syntax,
+ Rule("Give it a try!"),
+ ]
+
+ from itertools import cycle
+
+ examples = cycle(progress_renderables)
+
+ console = Console(record=True)
+
+ with Progress(
+ SpinnerColumn(),
+ TextColumn("[progress.description]{task.description}"),
+ BarColumn(),
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
+ TimeRemainingColumn(),
+ TimeElapsedColumn(),
+ console=console,
+ transient=True,
+ ) as progress:
+
+ task1 = progress.add_task("[red]Downloading", total=1000)
+ task2 = progress.add_task("[green]Processing", total=1000)
+ task3 = progress.add_task("[yellow]Thinking", total=1000, start=False)
+
+ while not progress.finished:
+ progress.update(task1, advance=0.5)
+ progress.update(task2, advance=0.3)
+ time.sleep(0.01)
+ if random.randint(0, 100) < 1:
+ progress.log(next(examples))
diff --git a/rich/progress_bar.py b/rich/progress_bar.py
new file mode 100644
index 0000000..63b0168
--- /dev/null
+++ b/rich/progress_bar.py
@@ -0,0 +1,214 @@
+import math
+from functools import lru_cache
+from time import monotonic
+from typing import Iterable, List, Optional
+
+from .color import Color, blend_rgb
+from .color_triplet import ColorTriplet
+from .console import Console, ConsoleOptions, RenderResult
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .segment import Segment
+from .style import Style, StyleType
+
+# Number of characters before 'pulse' animation repeats
+PULSE_SIZE = 20
+
+
+class ProgressBar(JupyterMixin):
+ """Renders a (progress) bar. Used by rich.progress.
+
+ Args:
+ total (float, optional): Number of steps in the bar. Defaults to 100.
+ completed (float, optional): Number of steps completed. Defaults to 0.
+ width (int, optional): Width of the bar, or ``None`` for maximum width. Defaults to None.
+ pulse (bool, optional): Enable pulse effect. Defaults to False.
+ style (StyleType, optional): Style for the bar background. Defaults to "bar.back".
+ complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete".
+ finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
+ pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
+ animation_time (Optional[float], optional): Time in seconds to use for animation, or None to use system time.
+ """
+
+ def __init__(
+ self,
+ total: float = 100,
+ completed: float = 0,
+ width: int = None,
+ pulse: bool = False,
+ style: StyleType = "bar.back",
+ complete_style: StyleType = "bar.complete",
+ finished_style: StyleType = "bar.finished",
+ pulse_style: StyleType = "bar.pulse",
+ animation_time: float = None,
+ ):
+ self.total = total
+ self.completed = completed
+ self.width = width
+ self.pulse = pulse
+ self.style = style
+ self.complete_style = complete_style
+ self.finished_style = finished_style
+ self.pulse_style = pulse_style
+ self.animation_time = animation_time
+
+ self._pulse_segments: Optional[List[Segment]] = None
+
+ def __repr__(self) -> str:
+ return f"<Bar {self.completed!r} of {self.total!r}>"
+
+ @property
+ def percentage_completed(self) -> float:
+ """Calculate percentage complete."""
+ completed = (self.completed / self.total) * 100.0
+ completed = min(100, max(0.0, completed))
+ return completed
+
+ @lru_cache(maxsize=16)
+ def _get_pulse_segments(
+ self,
+ fore_style: Style,
+ back_style: Style,
+ color_system: str,
+ no_color: bool,
+ ascii: bool = False,
+ ) -> List[Segment]:
+ """Get a list of segments to render a pulse animation.
+
+ Returns:
+ List[Segment]: A list of segments, one segment per character.
+ """
+ bar = "-" if ascii else "━"
+ segments: List[Segment] = []
+ if color_system not in ("standard", "eight_bit", "truecolor") or no_color:
+ segments += [Segment(bar, fore_style)] * (PULSE_SIZE // 2)
+ segments += [Segment(" " if no_color else bar, back_style)] * (
+ PULSE_SIZE - (PULSE_SIZE // 2)
+ )
+ return segments
+
+ append = segments.append
+ fore_color = (
+ fore_style.color.get_truecolor()
+ if fore_style.color
+ else ColorTriplet(255, 0, 255)
+ )
+ back_color = (
+ back_style.color.get_truecolor()
+ if back_style.color
+ else ColorTriplet(0, 0, 0)
+ )
+ cos = math.cos
+ pi = math.pi
+ _Segment = Segment
+ _Style = Style
+ from_triplet = Color.from_triplet
+
+ for index in range(PULSE_SIZE):
+ position = index / PULSE_SIZE
+ fade = 0.5 + cos((position * pi * 2)) / 2.0
+ color = blend_rgb(fore_color, back_color, cross_fade=fade)
+ append(_Segment(bar, _Style(color=from_triplet(color))))
+ return segments
+
+ def update(self, completed: float, total: float = None) -> None:
+ """Update progress with new values.
+
+ Args:
+ completed (float): Number of steps completed.
+ total (float, optional): Total number of steps, or ``None`` to not change. Defaults to None.
+ """
+ self.completed = completed
+ self.total = total if total is not None else self.total
+
+ def _render_pulse(
+ self, console: Console, width: int, ascii: bool = False
+ ) -> Iterable[Segment]:
+ """Renders the pulse animation.
+
+ Args:
+ console (Console): Console instance.
+ width (int): Width in characters of pulse animation.
+
+ Returns:
+ RenderResult: [description]
+
+ Yields:
+ Iterator[Segment]: Segments to render pulse
+ """
+ fore_style = console.get_style(self.pulse_style, default="white")
+ back_style = console.get_style(self.style, default="black")
+
+ pulse_segments = self._get_pulse_segments(
+ fore_style, back_style, console.color_system, console.no_color, ascii=ascii
+ )
+ segment_count = len(pulse_segments)
+ current_time = (
+ monotonic() if self.animation_time is None else self.animation_time
+ )
+ segments = pulse_segments * (int(width / segment_count) + 2)
+ offset = int(-current_time * 15) % segment_count
+ segments = segments[offset : offset + width]
+ yield from segments
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+
+ width = min(self.width or options.max_width, options.max_width)
+ ascii = options.legacy_windows or options.ascii_only
+ if self.pulse:
+ yield from self._render_pulse(console, width, ascii=ascii)
+ return
+
+ completed = min(self.total, max(0, self.completed))
+
+ bar = "-" if ascii else "━"
+ half_bar_right = " " if ascii else "╸"
+ half_bar_left = " " if ascii else "╺"
+ complete_halves = (
+ int(width * 2 * completed / self.total) if self.total else width * 2
+ )
+ bar_count = complete_halves // 2
+ half_bar_count = complete_halves % 2
+ style = console.get_style(self.style)
+ complete_style = console.get_style(
+ self.complete_style if self.completed < self.total else self.finished_style
+ )
+ _Segment = Segment
+ if bar_count:
+ yield _Segment(bar * bar_count, complete_style)
+ if half_bar_count:
+ yield _Segment(half_bar_right * half_bar_count, complete_style)
+
+ if not console.no_color:
+ remaining_bars = width - bar_count - half_bar_count
+ if remaining_bars and console.color_system is not None:
+ if not half_bar_count and bar_count:
+ yield _Segment(half_bar_left, style)
+ remaining_bars -= 1
+ if remaining_bars:
+ yield _Segment(bar * remaining_bars, style)
+
+ def __rich_measure__(self, console: Console, max_width: int) -> Measurement:
+ return (
+ Measurement(self.width, self.width)
+ if self.width is not None
+ else Measurement(4, max_width)
+ )
+
+
+if __name__ == "__main__": # pragma: no cover
+ console = Console()
+ bar = ProgressBar(width=50, total=100)
+
+ import time
+
+ console.show_cursor(False)
+ for n in range(0, 101, 1):
+ bar.update(n)
+ console.print(bar)
+ console.file.write("\r")
+ time.sleep(0.05)
+ console.show_cursor(True)
+ console.print()
diff --git a/rich/prompt.py b/rich/prompt.py
new file mode 100644
index 0000000..7db5d2d
--- /dev/null
+++ b/rich/prompt.py
@@ -0,0 +1,378 @@
+from typing import Any, Generic, List, Optional, TextIO, TypeVar, Union, overload
+
+from . import get_console
+from .console import Console
+from .text import Text, TextType
+
+PromptType = TypeVar("PromptType")
+DefaultType = TypeVar("DefaultType")
+
+
+class PromptError(Exception):
+ """Exception base class for prompt related errors."""
+
+
+class InvalidResponse(PromptError):
+ """Exception to indicate a response was invalid. Raise this within process_response() to indicate an error
+ and provide an error message.
+
+ Args:
+ message (Union[str, Text]): Error message.
+ """
+
+ def __init__(self, message: TextType) -> None:
+ self.message = message
+
+ def __rich__(self) -> TextType:
+ return self.message
+
+
+class PromptBase(Generic[PromptType]):
+ """Ask the user for input until a valid response is received. This is the base class, see one of
+ the concrete classes for examples.
+
+ Args:
+ prompt (TextType, optional): Prompt text. Defaults to "".
+ console (Console, optional): A Console instance or None to use global console. Defaults to None.
+ password (bool, optional): Enable password input. Defaults to False.
+ choices (List[str], optional): A list of valid choices. Defaults to None.
+ show_default (bool, optional): Show default in prompt. Defaults to True.
+ show_choices (bool, optional): Show choices in prompt. Defaults to True.
+ """
+
+ response_type: type = str
+
+ validate_error_message = "[prompt.invalid]Please enter a valid value"
+ illegal_choice_message = (
+ "[prompt.invalid.choice]Please select one of the available options"
+ )
+ prompt_suffix = ": "
+
+ choices: Optional[List[str]] = None
+
+ def __init__(
+ self,
+ prompt: TextType = "",
+ *,
+ console: Console = None,
+ password: bool = False,
+ choices: List[str] = None,
+ show_default: bool = True,
+ show_choices: bool = True,
+ ) -> None:
+ self.console = console or get_console()
+ self.prompt = (
+ Text.from_markup(prompt, style="prompt")
+ if isinstance(prompt, str)
+ else prompt
+ )
+ self.password = password
+ if choices is not None:
+ self.choices = choices
+ self.show_default = show_default
+ self.show_choices = show_choices
+
+ @classmethod
+ @overload
+ def ask(
+ cls,
+ prompt: TextType = "",
+ *,
+ console: Console = None,
+ password: bool = False,
+ choices: List[str] = None,
+ show_default: bool = True,
+ show_choices: bool = True,
+ default: DefaultType,
+ stream: TextIO = None,
+ ) -> Union[DefaultType, PromptType]:
+ ...
+
+ @classmethod
+ @overload
+ def ask(
+ cls,
+ prompt: TextType = "",
+ *,
+ console: Console = None,
+ password: bool = False,
+ choices: List[str] = None,
+ show_default: bool = True,
+ show_choices: bool = True,
+ stream: TextIO = None,
+ ) -> PromptType:
+ ...
+
+ @classmethod
+ def ask(
+ cls,
+ prompt: TextType = "",
+ *,
+ console: Console = None,
+ password: bool = False,
+ choices: List[str] = None,
+ show_default: bool = True,
+ show_choices: bool = True,
+ default: Any = ...,
+ stream: TextIO = None,
+ ) -> Any:
+ """Shortcut to construct and run a prompt loop and return the result.
+
+ Example:
+ >>> filename = Prompt.ask("Enter a filename")
+
+ Args:
+ prompt (TextType, optional): Prompt text. Defaults to "".
+ console (Console, optional): A Console instance or None to use global console. Defaults to None.
+ password (bool, optional): Enable password input. Defaults to False.
+ choices (List[str], optional): A list of valid choices. Defaults to None.
+ show_default (bool, optional): Show default in prompt. Defaults to True.
+ show_choices (bool, optional): Show choices in prompt. Defaults to True.
+ stream (TextIO, optional): Optional text file open for reading to get input. Defaults to None.
+ """
+ _prompt = cls(
+ prompt,
+ console=console,
+ password=password,
+ choices=choices,
+ show_default=show_default,
+ show_choices=show_choices,
+ )
+ return _prompt(default=default, stream=stream)
+
+ def render_default(self, default: DefaultType) -> Text:
+ """Turn the supplied default in to a Text instance.
+
+ Args:
+ default (DefaultType): Default value.
+
+ Returns:
+ Text: Text containing rendering of default value.
+ """
+ return Text(f"({default})", "prompt.default")
+
+ def make_prompt(self, default: DefaultType) -> Text:
+ """Make prompt text.
+
+ Args:
+ default (DefaultType): Default value.
+
+ Returns:
+ Text: Text to display in prompt.
+ """
+ prompt = self.prompt.copy()
+ prompt.end = ""
+
+ if self.show_choices and self.choices:
+ _choices = "/".join(self.choices)
+ choices = f"[{_choices}]"
+ prompt.append(" ")
+ prompt.append(choices, "prompt.choices")
+
+ if (
+ default != ...
+ and self.show_default
+ and isinstance(default, (str, self.response_type))
+ ):
+ prompt.append(" ")
+ _default = self.render_default(default)
+ prompt.append(_default)
+
+ prompt.append(self.prompt_suffix)
+
+ return prompt
+
+ @classmethod
+ def get_input(
+ cls,
+ console: Console,
+ prompt: TextType,
+ password: bool,
+ stream: TextIO = None,
+ ) -> str:
+ """Get input from user.
+
+ Args:
+ console (Console): Console instance.
+ prompt (TextType): Prompt text.
+ password (bool): Enable password entry.
+
+ Returns:
+ str: String from user.
+ """
+ return console.input(prompt, password=password, stream=stream)
+
+ def check_choice(self, value: str) -> bool:
+ """Check value is in the list of valid choices.
+
+ Args:
+ value (str): Value entered by user.
+
+ Returns:
+ bool: True if choice was valid, otherwise False.
+ """
+ assert self.choices is not None
+ return value.strip() in self.choices
+
+ def process_response(self, value: str) -> PromptType:
+ """Process response from user, convert to prompt type.
+
+ Args:
+ value (str): String typed by user.
+
+ Raises:
+ InvalidResponse: If ``value`` is invalid.
+
+ Returns:
+ PromptType: The value to be returned from ask method.
+ """
+ value = value.strip()
+ try:
+ return_value = self.response_type(value)
+ except ValueError:
+ raise InvalidResponse(self.validate_error_message)
+
+ if self.choices is not None and not self.check_choice(value):
+ raise InvalidResponse(self.illegal_choice_message)
+
+ return return_value
+
+ def on_validate_error(self, value: str, error: InvalidResponse) -> None:
+ """Called to handle validation error.
+
+ Args:
+ value (str): String entered by user.
+ error (InvalidResponse): Exception instance the initiated the error.
+ """
+ self.console.print(error)
+
+ def pre_prompt(self) -> None:
+ """Hook to display something before the prompt."""
+
+ @overload
+ def __call__(self, *, stream: TextIO = None) -> PromptType:
+ ...
+
+ @overload
+ def __call__(
+ self, *, default: DefaultType, stream: TextIO = None
+ ) -> Union[PromptType, DefaultType]:
+ ...
+
+ def __call__(self, *, default: Any = ..., stream: TextIO = None) -> Any:
+ """Run the prompt loop.
+
+ Args:
+ default (Any, optional): Optional default value.
+
+ Returns:
+ PromptType: Processed value.
+ """
+ while True:
+ self.pre_prompt()
+ prompt = self.make_prompt(default)
+ value = self.get_input(self.console, prompt, self.password, stream=stream)
+ if value == "" and default != ...:
+ return default
+ try:
+ return_value = self.process_response(value)
+ except InvalidResponse as error:
+ self.on_validate_error(value, error)
+ continue
+ else:
+ return return_value
+
+
+class Prompt(PromptBase[str]):
+ """A prompt that returns a str.
+
+ Example:
+ >>> name = Prompt.ask("Enter your name")
+
+
+ """
+
+ response_type = str
+
+
+class IntPrompt(PromptBase[int]):
+ """A prompt that returns an integer.
+
+ Example:
+ >>> burrito_count = IntPrompt.ask("How many burritos do you want to order")
+
+ """
+
+ response_type = int
+ validate_error_message = "[prompt.invalid]Please enter a valid integer number"
+
+
+class FloatPrompt(PromptBase[int]):
+ """A prompt that returns a float.
+
+ Example:
+ >>> temperature = FloatPrompt.ask("Enter desired temperature")
+
+ """
+
+ response_type = float
+ validate_error_message = "[prompt.invalid]Please enter a number"
+
+
+class Confirm(PromptBase[bool]):
+ """A yes / no confirmation prompt.
+
+ Example:
+ >>> if Confirm.ask("Continue"):
+ run_job()
+
+ """
+
+ response_type = bool
+ validate_error_message = "[prompt.invalid]Please enter Y or N"
+ choices = ["y", "n"]
+
+ def render_default(self, default: DefaultType) -> Text:
+ """Render the default as (y) or (n) rather than True/False."""
+ assert self.choices is not None
+ yes, no = self.choices
+ return Text(f"({yes})" if default else f"({no})", style="prompt.default")
+
+ def process_response(self, value: str) -> bool:
+ """Convert choices to a bool."""
+ value = value.strip().lower()
+ if value not in self.choices:
+ raise InvalidResponse(self.validate_error_message)
+ assert self.choices is not None
+ return value == self.choices[0]
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from rich import print
+
+ if Confirm.ask("Run [i]prompt[/i] tests?", default=True):
+ while True:
+ result = IntPrompt.ask(
+ ":rocket: Enter a number between [b]1[/b] and [b]10[/b]", default=5
+ )
+ if result >= 1 and result <= 10:
+ break
+ print(":pile_of_poo: [prompt.invalid]Number must be between 1 and 10")
+ print(f"number={result}")
+
+ while True:
+ password = Prompt.ask(
+ "Please enter a password [cyan](must be at least 5 characters)",
+ password=True,
+ )
+ if len(password) >= 5:
+ break
+ print("[prompt.invalid]password too short")
+ print(f"password={password!r}")
+
+ fruit = Prompt.ask("Enter a fruit", choices=["apple", "orange", "pear"])
+ print(f"fruit={fruit!r}")
+
+ else:
+ print("[b]OK :loudly_crying_face:")
diff --git a/rich/protocol.py b/rich/protocol.py
new file mode 100644
index 0000000..6468e53
--- /dev/null
+++ b/rich/protocol.py
@@ -0,0 +1,8 @@
+from typing import Any
+
+from .abc import RichRenderable
+
+
+def is_renderable(check_object: Any) -> bool:
+ """Check if an object may be rendered by Rich."""
+ return isinstance(check_object, str) or isinstance(check_object, RichRenderable)
diff --git a/rich/py.typed b/rich/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/rich/py.typed
diff --git a/rich/rule.py b/rich/rule.py
new file mode 100644
index 0000000..d14394f
--- /dev/null
+++ b/rich/rule.py
@@ -0,0 +1,115 @@
+from typing import Union
+
+from .align import AlignMethod
+from .cells import cell_len, set_cell_size
+from .console import Console, ConsoleOptions, RenderResult
+from .jupyter import JupyterMixin
+from .style import Style
+from .text import Text
+
+
+class Rule(JupyterMixin):
+ """A console renderable to draw a horizontal rule (line).
+
+ Args:
+ title (Union[str, Text], optional): Text to render in the rule. Defaults to "".
+ characters (str, optional): Character(s) used to draw the line. Defaults to "─".
+ style (StyleType, optional): Style of Rule. Defaults to "rule.line".
+ end (str, optional): Character at end of Rule. defaults to "\\\\n"
+ align (str, optional): How to align the title, one of "left", "center", or "right". Defaults to "center".
+ """
+
+ def __init__(
+ self,
+ title: Union[str, Text] = "",
+ *,
+ characters: str = "─",
+ style: Union[str, Style] = "rule.line",
+ end: str = "\n",
+ align: AlignMethod = "center",
+ ) -> None:
+ if cell_len(characters) < 1:
+ raise ValueError(
+ "'characters' argument must have a cell width of at least 1"
+ )
+ if align not in ("left", "center", "right"):
+ raise ValueError(
+ f'invalid value for align, expected "left", "center", "right" (not {align!r})'
+ )
+ self.title = title
+ self.characters = characters
+ self.style = style
+ self.end = end
+ self.align = align
+
+ def __repr__(self) -> str:
+ return f"Rule({self.title!r}, {self.characters!r})"
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ width = options.max_width
+
+ # Python3.6 doesn't have an isascii method on str
+ isascii = getattr(str, "isascii", None) or (
+ lambda s: all(ord(c) < 128 for c in s)
+ )
+ characters = (
+ "-"
+ if (options.ascii_only and not isascii(self.characters))
+ else self.characters
+ )
+
+ chars_len = cell_len(characters)
+ if not self.title:
+ rule_text = Text(characters * ((width // chars_len) + 1), self.style)
+ rule_text.truncate(width)
+ rule_text.plain = set_cell_size(rule_text.plain, width)
+ yield rule_text
+ return
+
+ if isinstance(self.title, Text):
+ title_text = self.title
+ else:
+ title_text = console.render_str(self.title, style="rule.text")
+
+ title_text.plain = title_text.plain.replace("\n", " ")
+ title_text.expand_tabs()
+ rule_text = Text(end=self.end)
+
+ if self.align == "center":
+ title_text.truncate(width - 4, overflow="ellipsis")
+ side_width = (width - cell_len(title_text.plain)) // 2
+ left = Text(characters * (side_width // chars_len + 1))
+ left.truncate(side_width - 1)
+ right_length = width - cell_len(left.plain) - cell_len(title_text.plain)
+ right = Text(characters * (side_width // chars_len + 1))
+ right.truncate(right_length)
+ rule_text.append(left.plain + " ", self.style)
+ rule_text.append(title_text)
+ rule_text.append(" " + right.plain, self.style)
+ elif self.align == "left":
+ title_text.truncate(width - 2, overflow="ellipsis")
+ rule_text.append(title_text)
+ rule_text.append(" ")
+ rule_text.append(characters * (width - rule_text.cell_len), self.style)
+ elif self.align == "right":
+ title_text.truncate(width - 2, overflow="ellipsis")
+ rule_text.append(characters * (width - title_text.cell_len - 1), self.style)
+ rule_text.append(" ")
+ rule_text.append(title_text)
+
+ rule_text.plain = set_cell_size(rule_text.plain, width)
+ yield rule_text
+
+
+if __name__ == "__main__": # pragma: no cover
+ from rich.console import Console
+ import sys
+
+ try:
+ text = sys.argv[1]
+ except IndexError:
+ text = "Hello, World"
+ console = Console()
+ console.print(Rule(title=text))
diff --git a/rich/scope.py b/rich/scope.py
new file mode 100644
index 0000000..4ab9525
--- /dev/null
+++ b/rich/scope.py
@@ -0,0 +1,86 @@
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Tuple
+
+from .highlighter import ReprHighlighter
+from .panel import Panel
+from .pretty import Pretty
+from .table import Table
+from .text import Text, TextType
+
+if TYPE_CHECKING:
+ from .console import ConsoleRenderable
+
+
+def render_scope(
+ scope: Mapping,
+ *,
+ title: TextType = None,
+ sort_keys: bool = True,
+ indent_guides: bool = False,
+ max_length: int = None,
+ max_string: int = None,
+) -> "ConsoleRenderable":
+ """Render python variables in a given scope.
+
+ Args:
+ scope (Mapping): A mapping containing variable names and values.
+ title (str, optional): Optional title. Defaults to None.
+ sort_keys (bool, optional): Enable sorting of items. Defaults to True.
+ indent_guides (bool, optional): Enable indentaton guides. Defaults to False.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
+
+ Returns:
+ ConsoleRenderable: A renderable object.
+ """
+ highlighter = ReprHighlighter()
+ items_table = Table.grid(padding=(0, 1), expand=False)
+ items_table.add_column(justify="right")
+
+ def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]:
+ """Sort special variables first, then alphabetically."""
+ key, _ = item
+ return (not key.startswith("__"), key.lower())
+
+ items = sorted(scope.items(), key=sort_items) if sort_keys else scope.items()
+ for key, value in items:
+ key_text = Text.assemble(
+ (key, "scope.key.special" if key.startswith("__") else "scope.key"),
+ (" =", "scope.equals"),
+ )
+ items_table.add_row(
+ key_text,
+ Pretty(
+ value,
+ highlighter=highlighter,
+ indent_guides=indent_guides,
+ max_length=max_length,
+ max_string=max_string,
+ ),
+ )
+ return Panel.fit(
+ items_table,
+ title=title,
+ border_style="scope.border",
+ padding=(0, 1),
+ )
+
+
+if __name__ == "__main__": # pragma: no cover
+ from rich import print
+
+ print()
+
+ def test(foo, bar):
+ list_of_things = [1, 2, 3, None, 4, True, False, "Hello World"]
+ dict_of_things = {
+ "version": "1.1",
+ "method": "confirmFruitPurchase",
+ "params": [["apple", "orange", "mangoes", "pomelo"], 1.123],
+ "id": "194521489",
+ }
+ print(render_scope(locals(), title="[i]locals", sort_keys=False))
+
+ test(20.3423, 3.1427)
+ print()
diff --git a/rich/screen.py b/rich/screen.py
new file mode 100644
index 0000000..3ca5355
--- /dev/null
+++ b/rich/screen.py
@@ -0,0 +1,40 @@
+from typing import TYPE_CHECKING
+
+from .segment import Segment
+from .style import StyleType
+from ._loop import loop_last
+
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderResult, RenderableType
+
+
+class Screen:
+ """A renderable that fills the terminal screen and crops excess.
+
+ Args:
+ renderable (RenderableType): Child renderable.
+ style (StyleType, optional): Optional background style. Defaults to None.
+ """
+
+ def __init__(
+ self, renderable: "RenderableType" = None, style: StyleType = None
+ ) -> None:
+ self.renderable = renderable
+ self.style = style
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ width, height = options.size
+ style = console.get_style(self.style) if self.style else None
+ render_options = options.update(width=width, height=height)
+ lines = console.render_lines(
+ self.renderable or "", render_options, style=style, pad=True
+ )
+ lines = Segment.set_shape(lines, width, height, style=style)
+ new_line = Segment.line()
+ for last, line in loop_last(lines):
+ yield from line
+ if not last:
+ yield new_line
diff --git a/rich/segment.py b/rich/segment.py
new file mode 100644
index 0000000..ff4996a
--- /dev/null
+++ b/rich/segment.py
@@ -0,0 +1,392 @@
+from typing import Dict, NamedTuple, Optional
+
+from .cells import cell_len, set_cell_size
+from .style import Style
+
+from itertools import filterfalse, zip_longest
+from operator import attrgetter
+from typing import Iterable, List, Tuple
+
+
+class Segment(NamedTuple):
+ """A piece of text with associated style. Segments are produced by the Console render process and
+ are ultimately converted in to strings to be written to the terminal.
+
+ Args:
+ text (str): A piece of text.
+ style (:class:`~rich.style.Style`, optional): An optional style to apply to the text.
+ is_control (bool, optional): Boolean that marks segment as containing non-printable control codes.
+ """
+
+ text: str = ""
+ """Raw text."""
+ style: Optional[Style] = None
+ """An optional style."""
+ is_control: bool = False
+ """True if the segment contains control codes, otherwise False."""
+
+ def __repr__(self) -> str:
+ """Simplified repr."""
+ if self.is_control:
+ return f"Segment.control({self.text!r}, {self.style!r})"
+ else:
+ return f"Segment({self.text!r}, {self.style!r})"
+
+ def __bool__(self) -> bool:
+ """Check if the segment contains text."""
+ return bool(self.text)
+
+ @property
+ def cell_length(self) -> int:
+ """Get cell length of segment."""
+ return 0 if self.is_control else cell_len(self.text)
+
+ @classmethod
+ def control(cls, text: str, style: Optional[Style] = None) -> "Segment":
+ """Create a Segment with control codes.
+
+ Args:
+ text (str): Text containing non-printable control codes.
+ style (Optional[style]): Optional style.
+
+ Returns:
+ Segment: A Segment instance with ``is_control=True``.
+ """
+ return cls(text, style, is_control=True)
+
+ @classmethod
+ def make_control(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
+ """Convert all segments in to control segments.
+
+ Returns:
+ Iterable[Segments]: Segments with is_control=True
+ """
+ return [cls(text, style, True) for text, style, _ in segments]
+
+ @classmethod
+ def line(cls, is_control: bool = False) -> "Segment":
+ """Make a new line segment."""
+ return cls("\n", is_control=is_control)
+
+ @classmethod
+ def apply_style(
+ cls,
+ segments: Iterable["Segment"],
+ style: Style = None,
+ post_style: Style = None,
+ ) -> Iterable["Segment"]:
+ """Apply style(s) to an iterable of segments.
+
+ Returns an iterable of segments where the style is replaced by ``style + segment.style + post_style``.
+
+ Args:
+ segments (Iterable[Segment]): Segments to process.
+ style (Style, optional): Base style. Defaults to None.
+ post_style (Style, optional): Style to apply on top of segment style. Defaults to None.
+
+ Returns:
+ Iterable[Segments]: A new iterable of segments (possibly the same iterable).
+ """
+ if style:
+ apply = style.__add__
+ segments = (
+ cls(text, None if is_control else apply(_style), is_control)
+ for text, _style, is_control in segments
+ )
+ if post_style:
+ segments = (
+ cls(
+ text,
+ None
+ if is_control
+ else (_style + post_style if _style else post_style),
+ is_control,
+ )
+ for text, _style, is_control in segments
+ )
+ return segments
+
+ @classmethod
+ def filter_control(
+ cls, segments: Iterable["Segment"], is_control=False
+ ) -> Iterable["Segment"]:
+ """Filter segments by ``is_control`` attribute.
+
+ Args:
+ segments (Iterable[Segment]): An iterable of Segment instances.
+ is_control (bool, optional): is_control flag to match in search.
+
+ Returns:
+ Iterable[Segment]: And iterable of Segment instances.
+
+ """
+ if is_control:
+ return filter(attrgetter("is_control"), segments)
+ else:
+ return filterfalse(attrgetter("is_control"), segments)
+
+ @classmethod
+ def split_lines(cls, segments: Iterable["Segment"]) -> Iterable[List["Segment"]]:
+ """Split a sequence of segments in to a list of lines.
+
+ Args:
+ segments (Iterable[Segment]): Segments potentially containing line feeds.
+
+ Yields:
+ Iterable[List[Segment]]: Iterable of segment lists, one per line.
+ """
+ line: List[Segment] = []
+ append = line.append
+
+ for segment in segments:
+ if "\n" in segment.text and not segment.is_control:
+ text, style, _ = segment
+ while text:
+ _text, new_line, text = text.partition("\n")
+ if _text:
+ append(cls(_text, style))
+ if new_line:
+ yield line
+ line = []
+ append = line.append
+ else:
+ append(segment)
+ if line:
+ yield line
+
+ @classmethod
+ def split_and_crop_lines(
+ cls,
+ segments: Iterable["Segment"],
+ length: int,
+ style: Style = None,
+ pad: bool = True,
+ include_new_lines: bool = True,
+ ) -> Iterable[List["Segment"]]:
+ """Split segments in to lines, and crop lines greater than a given length.
+
+ Args:
+ segments (Iterable[Segment]): An iterable of segments, probably
+ generated from console.render.
+ length (int): Desired line length.
+ style (Style, optional): Style to use for any padding.
+ pad (bool): Enable padding of lines that are less than `length`.
+
+ Returns:
+ Iterable[List[Segment]]: An iterable of lines of segments.
+ """
+ line: List[Segment] = []
+ append = line.append
+
+ adjust_line_length = cls.adjust_line_length
+ new_line_segment = cls("\n")
+
+ for segment in segments:
+ if "\n" in segment.text and not segment.is_control:
+ text, style, _ = segment
+ while text:
+ _text, new_line, text = text.partition("\n")
+ if _text:
+ append(cls(_text, style))
+ if new_line:
+ cropped_line = adjust_line_length(
+ line, length, style=style, pad=pad
+ )
+ if include_new_lines:
+ cropped_line.append(new_line_segment)
+ yield cropped_line
+ del line[:]
+ else:
+ append(segment)
+ if line:
+ yield adjust_line_length(line, length, style=style, pad=pad)
+
+ @classmethod
+ def adjust_line_length(
+ cls, line: List["Segment"], length: int, style: Style = None, pad: bool = True
+ ) -> List["Segment"]:
+ """Adjust a line to a given width (cropping or padding as required).
+
+ Args:
+ segments (Iterable[Segment]): A list of segments in a single line.
+ length (int): The desired width of the line.
+ style (Style, optional): The style of padding if used (space on the end). Defaults to None.
+ pad (bool, optional): Pad lines with spaces if they are shorter than `length`. Defaults to True.
+
+ Returns:
+ List[Segment]: A line of segments with the desired length.
+ """
+ line_length = sum(segment.cell_length for segment in line)
+ new_line: List[Segment]
+
+ if line_length < length:
+ if pad:
+ new_line = line + [cls(" " * (length - line_length), style)]
+ else:
+ new_line = line[:]
+ elif line_length > length:
+ new_line = []
+ append = new_line.append
+ line_length = 0
+ for segment in line:
+ segment_length = segment.cell_length
+ if line_length + segment_length < length or segment.is_control:
+ append(segment)
+ line_length += segment_length
+ else:
+ text, segment_style, _ = segment
+ text = set_cell_size(text, length - line_length)
+ append(cls(text, segment_style))
+ break
+ else:
+ new_line = line[:]
+ return new_line
+
+ @classmethod
+ def get_line_length(cls, line: List["Segment"]) -> int:
+ """Get the length of list of segments.
+
+ Args:
+ line (List[Segment]): A line encoded as a list of Segments (assumes no '\\\\n' characters),
+
+ Returns:
+ int: The length of the line.
+ """
+ return sum(segment.cell_length for segment in line)
+
+ @classmethod
+ def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]:
+ """Get the shape (enclosing rectangle) of a list of lines.
+
+ Args:
+ lines (List[List[Segment]]): A list of lines (no '\\\\n' characters).
+
+ Returns:
+ Tuple[int, int]: Width and height in characters.
+ """
+ get_line_length = cls.get_line_length
+ max_width = max(get_line_length(line) for line in lines) if lines else 0
+ return (max_width, len(lines))
+
+ @classmethod
+ def set_shape(
+ cls,
+ lines: List[List["Segment"]],
+ width: int,
+ height: int = None,
+ style: Style = None,
+ ) -> List[List["Segment"]]:
+ """Set the shape of a list of lines (enclosing rectangle).
+
+ Args:
+ lines (List[List[Segment]]): A list of lines.
+ width (int): Desired width.
+ height (int, optional): Desired height or None for no change.
+ style (Style, optional): Style of any padding added. Defaults to None.
+
+ Returns:
+ List[List[Segment]]: New list of lines that fits width x height.
+ """
+ if height is None:
+ height = len(lines)
+ new_lines: List[List[Segment]] = []
+ pad_line = [Segment(" " * width, style)]
+ append = new_lines.append
+ adjust_line_length = cls.adjust_line_length
+ line: Optional[List[Segment]]
+ iter_lines = iter(lines)
+ for _ in range(height):
+ line = next(iter_lines, None)
+ if line is None:
+ append(pad_line)
+ else:
+ append(adjust_line_length(line, width, style=style))
+ return new_lines
+
+ @classmethod
+ def simplify(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
+ """Simplify an iterable of segments by combining contiguous segments with the same style.
+
+ Args:
+ segments (Iterable[Segment]): An iterable of segments.
+
+ Returns:
+ Iterable[Segment]: A possibly smaller iterable of segments that will render the same way.
+ """
+ iter_segments = iter(segments)
+ try:
+ last_segment = next(iter_segments)
+ except StopIteration:
+ return
+
+ _Segment = Segment
+ for segment in iter_segments:
+ if last_segment.style == segment.style and not segment.is_control:
+ last_segment = _Segment(
+ last_segment.text + segment.text, last_segment.style
+ )
+ else:
+ yield last_segment
+ last_segment = segment
+ yield last_segment
+
+ @classmethod
+ def strip_links(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
+ """Remove all links from an iterable of styles.
+
+ Args:
+ segments (Iterable[Segment]): An iterable segments.
+
+ Yields:
+ Segment: Segments with link removed.
+ """
+ for segment in segments:
+ if segment.is_control or segment.style is None:
+ yield segment
+ else:
+ text, style, _is_control = segment
+ yield cls(text, style.update_link(None) if style else None)
+
+ @classmethod
+ def strip_styles(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
+ """Remove all styles from an iterable of segments.
+
+ Args:
+ segments (Iterable[Segment]): An iterable segments.
+
+ Yields:
+ Segment: Segments with styles replace with None
+ """
+ for text, _style, is_control in segments:
+ yield cls(text, None, is_control)
+
+ @classmethod
+ def remove_color(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
+ """Remove all color from an iterable of segments.
+
+ Args:
+ segments (Iterable[Segment]): An iterable segments.
+
+ Yields:
+ Segment: Segments with colorless style.
+ """
+
+ cache: Dict[Style, Style] = {}
+ for text, style, is_control in segments:
+ if style:
+ colorless_style = cache.get(style)
+ if colorless_style is None:
+ colorless_style = style.without_color
+ cache[style] = colorless_style
+ yield cls(text, colorless_style, is_control)
+ else:
+ yield cls(text, None, is_control)
+
+
+if __name__ == "__main__": # pragma: no cover
+ lines = [[Segment("Hello")]]
+ lines = Segment.set_shape(lines, 50, 4, style=Style.parse("on blue"))
+ for line in lines:
+ print(line)
+
+ print(Style.parse("on blue") + Style.parse("on red"))
diff --git a/rich/spinner.py b/rich/spinner.py
new file mode 100644
index 0000000..b236e0b
--- /dev/null
+++ b/rich/spinner.py
@@ -0,0 +1,88 @@
+from typing import cast, List, Optional, TYPE_CHECKING
+
+from ._spinners import SPINNERS
+from .console import Console
+from .measure import Measurement
+from .style import StyleType
+from .text import Text, TextType
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderResult
+
+
+class Spinner:
+ def __init__(
+ self, name: str, text: TextType = "", *, style: StyleType = None, speed=1.0
+ ) -> None:
+ """A spinner animation.
+
+ Args:
+ name (str): Name of spinner (run python -m rich.spinner).
+ text (TextType, optional): Text to display at the right of the spinner. Defaults to "".
+ style (StyleType, optional): Style for sinner amimation. Defaults to None.
+ speed (float, optional): Speed factor for animation. Defaults to 1.0.
+
+ Raises:
+ KeyError: If name isn't one of the supported spinner animations.
+ """
+ try:
+ spinner = SPINNERS[name]
+ except KeyError:
+ raise KeyError(f"no spinner called {name!r}")
+ self.text = text
+ self.frames = cast(List[str], spinner["frames"])[:]
+ self.interval = cast(float, spinner["interval"])
+ self.start_time: Optional[float] = None
+ self.style = style
+ self.speed = speed
+ self.time = 0.0
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ time = console.get_time()
+ if self.start_time is None:
+ self.start_time = time
+ text = self.render(time - self.start_time)
+ yield text
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
+ text = self.render(0)
+ return Measurement.get(console, text, max_width)
+
+ def render(self, time: float) -> Text:
+ """Render the spinner for a given time.
+
+ Args:
+ time (float): Time in seconds.
+
+ Returns:
+ Text: A Text instance containing animation frame.
+ """
+ frame_no = int((time * self.speed) / (self.interval / 1000.0))
+ frame = Text(self.frames[frame_no % len(self.frames)], style=self.style or "")
+ return Text.assemble(frame, " ", self.text) if self.text else frame
+
+
+if __name__ == "__main__": # pragma: no cover
+ from time import sleep
+
+ from .columns import Columns
+ from .panel import Panel
+ from .live import Live
+
+ all_spinners = Columns(
+ [
+ Spinner(spinner_name, text=Text(repr(spinner_name), style="green"))
+ for spinner_name in sorted(SPINNERS.keys())
+ ],
+ column_first=True,
+ expand=True,
+ )
+
+ with Live(
+ Panel(all_spinners, title="Spinners", border_style="blue"),
+ refresh_per_second=20,
+ ) as live:
+ while True:
+ sleep(0.1)
diff --git a/rich/status.py b/rich/status.py
new file mode 100644
index 0000000..62f1ba9
--- /dev/null
+++ b/rich/status.py
@@ -0,0 +1,131 @@
+from typing import Optional
+
+from .console import Console, RenderableType
+from .jupyter import JupyterMixin
+from .live import Live
+from .spinner import Spinner
+from .style import StyleType
+from .table import Table
+
+
+class Status(JupyterMixin):
+ """Displays a status indicator with a 'spinner' animation.
+
+ Args:
+ status (RenderableType): A status renderable (str or Text typically).
+ console (Console, optional): Console instance to use, or None for global console. Defaults to None.
+ spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots".
+ spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner".
+ speed (float, optional): Speed factor for spinner animation. Defaults to 1.0.
+ refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5.
+ """
+
+ def __init__(
+ self,
+ status: RenderableType,
+ *,
+ console: Console = None,
+ spinner: str = "dots",
+ spinner_style: StyleType = "status.spinner",
+ speed: float = 1.0,
+ refresh_per_second: float = 12.5,
+ ):
+ self.status = status
+ self.spinner = spinner
+ self.spinner_style = spinner_style
+ self.speed = speed
+ self._spinner = Spinner(spinner, style=spinner_style, speed=speed)
+ self._live = Live(
+ self.renderable,
+ console=console,
+ refresh_per_second=refresh_per_second,
+ transient=True,
+ )
+ self.update(
+ status=status, spinner=spinner, spinner_style=spinner_style, speed=speed
+ )
+
+ @property
+ def renderable(self) -> Table:
+ """Get the renderable for the status (a table with spinner and status)."""
+ table = Table.grid(padding=1)
+ table.add_row(self._spinner, self.status)
+ return table
+
+ @property
+ def console(self) -> "Console":
+ """Get the Console used by the Status objects."""
+ return self._live.console
+
+ def update(
+ self,
+ status: Optional[RenderableType] = None,
+ *,
+ spinner: Optional[str] = None,
+ spinner_style: Optional[StyleType] = None,
+ speed: Optional[float] = None,
+ ):
+ """Update status.
+
+ Args:
+ status (Optional[RenderableType], optional): New status renderable or None for no change. Defaults to None.
+ spinner (Optional[str], optional): New spinner or None for no change. Defaults to None.
+ spinner_style (Optional[StyleType], optional): New spinner style or None for no change. Defaults to None.
+ speed (Optional[float], optional): Speed factor for spinner animation or None for no change. Defaults to None.
+ """
+ if status is not None:
+ self.status = status
+ if spinner is not None:
+ self.spinner = spinner
+ if spinner_style is not None:
+ self.spinner_style = spinner_style
+ if speed is not None:
+ self.speed = speed
+ self._spinner = Spinner(
+ self.spinner, style=self.spinner_style, speed=self.speed
+ )
+ self._live.update(self.renderable, refresh=True)
+
+ def start(self) -> None:
+ """Start the status animation."""
+ self._live.start()
+
+ def stop(self) -> None:
+ """Stop the spinner animation."""
+ self._live.stop()
+
+ def __rich__(self) -> RenderableType:
+ return self.renderable
+
+ def __enter__(self) -> "Status":
+ self.start()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+ self.stop()
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from time import sleep
+
+ from .console import Console
+
+ console = Console()
+ with console.status("[magenta]Covid detector booting up") as status:
+ sleep(3)
+ console.log("Importing advanced AI")
+ sleep(3)
+ console.log("Advanced Covid AI Ready")
+ sleep(3)
+ status.update(status="[bold blue] Scanning for Covid", spinner="earth")
+ sleep(3)
+ console.log("Found 10,000,000,000 copies of Covid32.exe")
+ sleep(3)
+ status.update(
+ status="[bold red]Moving Covid32.exe to Trash",
+ spinner="bouncingBall",
+ spinner_style="yellow",
+ )
+ sleep(5)
+ console.print("[bold green]Covid deleted successfully")
diff --git a/rich/style.py b/rich/style.py
new file mode 100644
index 0000000..a6b756b
--- /dev/null
+++ b/rich/style.py
@@ -0,0 +1,694 @@
+import sys
+from functools import lru_cache
+from random import randint
+from time import time
+from typing import Any, Dict, Iterable, List, Optional, Type, Union
+
+from . import errors
+from .color import Color, ColorParseError, ColorSystem, blend_rgb
+from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme
+
+# Style instances and style definitions are often interchangeable
+StyleType = Union[str, "Style"]
+
+
+class _Bit:
+ """A descriptor to get/set a style attribute bit."""
+
+ __slots__ = ["bit"]
+
+ def __init__(self, bit_no: int) -> None:
+ self.bit = 1 << bit_no
+
+ def __get__(self, obj: "Style", objtype: Type["Style"]) -> Optional[bool]:
+ if obj._set_attributes & self.bit:
+ return obj._attributes & self.bit != 0
+ return None
+
+
+class Style:
+ """A terminal style.
+
+ A terminal style consists of a color (`color`), a background color (`bgcolor`), and a number of attributes, such
+ as bold, italic etc. The attributes have 3 states: they can either be on
+ (``True``), off (``False``), or not set (``None``).
+
+ Args:
+ color (Union[Color, str], optional): Color of terminal text. Defaults to None.
+ bgcolor (Union[Color, str], optional): Color of terminal background. Defaults to None.
+ bold (bool, optional): Enable bold text. Defaults to None.
+ dim (bool, optional): Enable dim text. Defaults to None.
+ italic (bool, optional): Enable italic text. Defaults to None.
+ underline (bool, optional): Enable underlined text. Defaults to None.
+ blink (bool, optional): Enabled blinking text. Defaults to None.
+ blink2 (bool, optional): Enable fast blinking text. Defaults to None.
+ reverse (bool, optional): Enabled reverse text. Defaults to None.
+ conceal (bool, optional): Enable concealed text. Defaults to None.
+ strike (bool, optional): Enable strikethrough text. Defaults to None.
+ underline2 (bool, optional): Enable doubly underlined text. Defaults to None.
+ frame (bool, optional): Enable framed text. Defaults to None.
+ encircle (bool, optional): Enable encircled text. Defaults to None.
+ overline (bool, optional): Enable overlined text. Defaults to None.
+ link (str, link): Link URL. Defaults to None.
+
+ """
+
+ _color: Optional[Color]
+ _bgcolor: Optional[Color]
+ _attributes: int
+ _set_attributes: int
+ _hash: int
+ _null: bool
+
+ __slots__ = [
+ "_color",
+ "_bgcolor",
+ "_attributes",
+ "_set_attributes",
+ "_link",
+ "_link_id",
+ "_ansi",
+ "_style_definition",
+ "_hash",
+ "_null",
+ ]
+
+ # maps bits on to SGR parameter
+ _style_map = {
+ 0: "1",
+ 1: "2",
+ 2: "3",
+ 3: "4",
+ 4: "5",
+ 5: "6",
+ 6: "7",
+ 7: "8",
+ 8: "9",
+ 9: "21",
+ 10: "51",
+ 11: "52",
+ 12: "53",
+ }
+
+ def __init__(
+ self,
+ *,
+ color: Union[Color, str] = None,
+ bgcolor: Union[Color, str] = None,
+ bold: bool = None,
+ dim: bool = None,
+ italic: bool = None,
+ underline: bool = None,
+ blink: bool = None,
+ blink2: bool = None,
+ reverse: bool = None,
+ conceal: bool = None,
+ strike: bool = None,
+ underline2: bool = None,
+ frame: bool = None,
+ encircle: bool = None,
+ overline: bool = None,
+ link: str = None,
+ ):
+ self._ansi: Optional[str] = None
+ self._style_definition: Optional[str] = None
+
+ def _make_color(color: Union[Color, str]) -> Color:
+ return color if isinstance(color, Color) else Color.parse(color)
+
+ self._color = None if color is None else _make_color(color)
+ self._bgcolor = None if bgcolor is None else _make_color(bgcolor)
+ self._set_attributes = sum(
+ (
+ bold is not None,
+ dim is not None and 2,
+ italic is not None and 4,
+ underline is not None and 8,
+ blink is not None and 16,
+ blink2 is not None and 32,
+ reverse is not None and 64,
+ conceal is not None and 128,
+ strike is not None and 256,
+ underline2 is not None and 512,
+ frame is not None and 1024,
+ encircle is not None and 2048,
+ overline is not None and 4096,
+ )
+ )
+ self._attributes = (
+ sum(
+ (
+ bold and 1 or 0,
+ dim and 2 or 0,
+ italic and 4 or 0,
+ underline and 8 or 0,
+ blink and 16 or 0,
+ blink2 and 32 or 0,
+ reverse and 64 or 0,
+ conceal and 128 or 0,
+ strike and 256 or 0,
+ underline2 and 512 or 0,
+ frame and 1024 or 0,
+ encircle and 2048 or 0,
+ overline and 4096 or 0,
+ )
+ )
+ if self._set_attributes
+ else 0
+ )
+
+ self._link = link
+ self._link_id = f"{time()}-{randint(0, 999999)}" if link else ""
+ self._hash = hash(
+ (
+ self._color,
+ self._bgcolor,
+ self._attributes,
+ self._set_attributes,
+ link,
+ )
+ )
+ self._null = not (self._set_attributes or color or bgcolor or link)
+
+ @classmethod
+ def null(cls) -> "Style":
+ """Create an 'null' style, equivalent to Style(), but more performant."""
+ return NULL_STYLE
+
+ @classmethod
+ def from_color(cls, color: Color = None, bgcolor: Color = None) -> "Style":
+ """Create a new style with colors and no attributes.
+
+ Returns:
+ color (Optional[Color]): A (foreground) color, or None for no color. Defaults to None.
+ bgcolor (Optional[Color]): A (background) color, or None for no color. Defaults to None.
+ """
+ style = cls.__new__(Style)
+ style._ansi = None
+ style._style_definition = None
+ style._color = color
+ style._bgcolor = bgcolor
+ style._set_attributes = 0
+ style._attributes = 0
+ style._link = None
+ style._link_id = ""
+ style._hash = hash(
+ (
+ color,
+ bgcolor,
+ None,
+ None,
+ None,
+ )
+ )
+ style._null = not (color or bgcolor)
+ return style
+
+ bold = _Bit(0)
+ dim = _Bit(1)
+ italic = _Bit(2)
+ underline = _Bit(3)
+ blink = _Bit(4)
+ blink2 = _Bit(5)
+ reverse = _Bit(6)
+ conceal = _Bit(7)
+ strike = _Bit(8)
+ underline2 = _Bit(9)
+ frame = _Bit(10)
+ encircle = _Bit(11)
+ overline = _Bit(12)
+
+ @property
+ def link_id(self) -> str:
+ """Get a link id, used in ansi code for links."""
+ return self._link_id
+
+ def __str__(self) -> str:
+ """Re-generate style definition from attributes."""
+ if self._style_definition is None:
+ attributes: List[str] = []
+ append = attributes.append
+ bits = self._set_attributes
+ if bits & 0b0000000001111:
+ if bits & 1:
+ append("bold" if self.bold else "not bold")
+ if bits & (1 << 1):
+ append("dim" if self.dim else "not dim")
+ if bits & (1 << 2):
+ append("italic" if self.italic else "not italic")
+ if bits & (1 << 3):
+ append("underline" if self.underline else "not underline")
+ if bits & 0b0000111110000:
+ if bits & (1 << 4):
+ append("blink" if self.blink else "not blink")
+ if bits & (1 << 5):
+ append("blink2" if self.blink2 else "not blink2")
+ if bits & (1 << 6):
+ append("reverse" if self.reverse else "not reverse")
+ if bits & (1 << 7):
+ append("conceal" if self.conceal else "not conceal")
+ if bits & (1 << 8):
+ append("strike" if self.strike else "not strike")
+ if bits & 0b1111000000000:
+ if bits & (1 << 9):
+ append("underline2" if self.underline2 else "not underline2")
+ if bits & (1 << 10):
+ append("frame" if self.frame else "not frame")
+ if bits & (1 << 11):
+ append("encircle" if self.encircle else "not encircle")
+ if bits & (1 << 12):
+ append("overline" if self.overline else "not overline")
+ if self._color is not None:
+ append(self._color.name)
+ if self._bgcolor is not None:
+ append("on")
+ append(self._bgcolor.name)
+ if self._link:
+ append("link")
+ append(self._link)
+ self._style_definition = " ".join(attributes) or "none"
+ return self._style_definition
+
+ def __bool__(self) -> bool:
+ """A Style is false if it has no attributes, colors, or links."""
+ return not self._null
+
+ def _make_ansi_codes(self, color_system: ColorSystem) -> str:
+ """Generate ANSI codes for this style.
+
+ Args:
+ color_system (ColorSystem): Color system.
+
+ Returns:
+ str: String containing codes.
+ """
+ if self._ansi is None:
+ sgr: List[str] = []
+ append = sgr.append
+ _style_map = self._style_map
+ attributes = self._attributes & self._set_attributes
+ if attributes:
+ if attributes & 1:
+ append(_style_map[0])
+ if attributes & 2:
+ append(_style_map[1])
+ if attributes & 4:
+ append(_style_map[2])
+ if attributes & 8:
+ append(_style_map[3])
+ if attributes & 0b0000111110000:
+ for bit in range(4, 9):
+ if attributes & (1 << bit):
+ append(_style_map[bit])
+ if attributes & 0b1111000000000:
+ for bit in range(9, 13):
+ if attributes & (1 << bit):
+ append(_style_map[bit])
+ if self._color is not None:
+ sgr.extend(self._color.downgrade(color_system).get_ansi_codes())
+ if self._bgcolor is not None:
+ sgr.extend(
+ self._bgcolor.downgrade(color_system).get_ansi_codes(
+ foreground=False
+ )
+ )
+ self._ansi = ";".join(sgr)
+ return self._ansi
+
+ @classmethod
+ @lru_cache(maxsize=1024)
+ def normalize(cls, style: str) -> str:
+ """Normalize a style definition so that styles with the same effect have the same string
+ representation.
+
+ Args:
+ style (str): A style definition.
+
+ Returns:
+ str: Normal form of style definition.
+ """
+ try:
+ return str(cls.parse(style))
+ except errors.StyleSyntaxError:
+ return style.strip().lower()
+
+ @classmethod
+ def pick_first(cls, *values: Optional[StyleType]) -> StyleType:
+ """Pick first non-None style."""
+ for value in values:
+ if value is not None:
+ return value
+ raise ValueError("expected at least one non-None style")
+
+ def __repr__(self) -> str:
+ """Render a named style differently from an anonymous style."""
+ return f'Style.parse("{self}")'
+
+ def __eq__(self, other: Any) -> bool:
+ if not isinstance(other, Style):
+ return NotImplemented
+ return (
+ self._color == other._color
+ and self._bgcolor == other._bgcolor
+ and self._set_attributes == other._set_attributes
+ and self._attributes == other._attributes
+ and self._link == other._link
+ )
+
+ def __hash__(self) -> int:
+ return self._hash
+
+ @property
+ def color(self) -> Optional[Color]:
+ """The foreground color or None if it is not set."""
+ return self._color
+
+ @property
+ def bgcolor(self) -> Optional[Color]:
+ """The background color or None if it is not set."""
+ return self._bgcolor
+
+ @property
+ def link(self) -> Optional[str]:
+ """Link text, if set."""
+ return self._link
+
+ @property
+ def transparent_background(self) -> bool:
+ """Check if the style specified a transparent background."""
+ return self.bgcolor is None or self.bgcolor.is_default
+
+ @property
+ def background_style(self) -> "Style":
+ """A Style with background only."""
+ return Style(bgcolor=self.bgcolor)
+
+ @property
+ def without_color(self) -> "Style":
+ """Get a copy of the style with color removed."""
+ if self._null:
+ return NULL_STYLE
+ style = self.__new__(Style)
+ style._ansi = None
+ style._style_definition = None
+ style._color = None
+ style._bgcolor = None
+ style._attributes = self._attributes
+ style._set_attributes = self._set_attributes
+ style._link = self._link
+ style._link_id = f"{time()}-{randint(0, 999999)}" if self._link else ""
+ style._hash = self._hash
+ style._null = False
+ return style
+
+ @classmethod
+ @lru_cache(maxsize=4096)
+ def parse(cls, style_definition: str) -> "Style":
+ """Parse a style definition.
+
+ Args:
+ style_definition (str): A string containing a style.
+
+ Raises:
+ errors.StyleSyntaxError: If the style definition syntax is invalid.
+
+ Returns:
+ `Style`: A Style instance.
+ """
+ if style_definition.strip() == "none" or not style_definition:
+ return cls.null()
+
+ style_attributes = {
+ "dim": "dim",
+ "d": "dim",
+ "bold": "bold",
+ "b": "bold",
+ "italic": "italic",
+ "i": "italic",
+ "underline": "underline",
+ "u": "underline",
+ "blink": "blink",
+ "blink2": "blink2",
+ "reverse": "reverse",
+ "r": "reverse",
+ "conceal": "conceal",
+ "c": "conceal",
+ "strike": "strike",
+ "s": "strike",
+ "underline2": "underline2",
+ "uu": "underline2",
+ "frame": "frame",
+ "encircle": "encircle",
+ "overline": "overline",
+ "o": "overline",
+ }
+ color: Optional[str] = None
+ bgcolor: Optional[str] = None
+ attributes: Dict[str, Optional[bool]] = {}
+ link: Optional[str] = None
+
+ words = iter(style_definition.split())
+ for original_word in words:
+ word = original_word.lower()
+ if word == "on":
+ word = next(words, "")
+ if not word:
+ raise errors.StyleSyntaxError("color expected after 'on'")
+ try:
+ Color.parse(word) is None
+ except ColorParseError as error:
+ raise errors.StyleSyntaxError(
+ f"unable to parse {word!r} as background color; {error}"
+ ) from None
+ bgcolor = word
+
+ elif word == "not":
+ word = next(words, "")
+ attribute = style_attributes.get(word)
+ if attribute is None:
+ raise errors.StyleSyntaxError(
+ f"expected style attribute after 'not', found {word!r}"
+ )
+ attributes[attribute] = False
+
+ elif word == "link":
+ word = next(words, "")
+ if not word:
+ raise errors.StyleSyntaxError("URL expected after 'link'")
+ link = word
+
+ elif word in style_attributes:
+ attributes[style_attributes[word]] = True
+
+ else:
+ try:
+ Color.parse(word)
+ except ColorParseError as error:
+ raise errors.StyleSyntaxError(
+ f"unable to parse {word!r} as color; {error}"
+ ) from None
+ color = word
+ style = Style(color=color, bgcolor=bgcolor, link=link, **attributes)
+ return style
+
+ @lru_cache(maxsize=1024)
+ def get_html_style(self, theme: TerminalTheme = None) -> str:
+ """Get a CSS style rule."""
+ theme = theme or DEFAULT_TERMINAL_THEME
+ css: List[str] = []
+ append = css.append
+
+ color = self.color
+ bgcolor = self.bgcolor
+ if self.reverse:
+ color, bgcolor = bgcolor, color
+ if self.dim:
+ foreground_color = (
+ theme.foreground_color if color is None else color.get_truecolor(theme)
+ )
+ color = Color.from_triplet(
+ blend_rgb(foreground_color, theme.background_color, 0.5)
+ )
+ if color is not None:
+ theme_color = color.get_truecolor(theme)
+ append(f"color: {theme_color.hex}")
+ if bgcolor is not None:
+ theme_color = bgcolor.get_truecolor(theme, foreground=False)
+ append(f"background-color: {theme_color.hex}")
+ if self.bold:
+ append("font-weight: bold")
+ if self.italic:
+ append("font-style: italic")
+ if self.underline:
+ append("text-decoration: underline")
+ if self.strike:
+ append("text-decoration: line-through")
+ if self.overline:
+ append("text-decoration: overline")
+ return "; ".join(css)
+
+ @classmethod
+ def combine(cls, styles: Iterable["Style"]) -> "Style":
+ """Combine styles and get result.
+
+ Args:
+ styles (Iterable[Style]): Styles to combine.
+
+ Returns:
+ Style: A new style instance.
+ """
+ iter_styles = iter(styles)
+ return sum(iter_styles, next(iter_styles))
+
+ @classmethod
+ def chain(cls, *styles: "Style") -> "Style":
+ """Combine styles from positional argument in to a single style.
+
+ Args:
+ *styles (Iterable[Style]): Styles to combine.
+
+ Returns:
+ Style: A new style instance.
+ """
+ iter_styles = iter(styles)
+ return sum(iter_styles, next(iter_styles))
+
+ def copy(self) -> "Style":
+ """Get a copy of this style.
+
+ Returns:
+ Style: A new Style instance with identical attributes.
+ """
+ if self._null:
+ return NULL_STYLE
+ style = self.__new__(Style)
+ style._ansi = self._ansi
+ style._style_definition = self._style_definition
+ style._color = self._color
+ style._bgcolor = self._bgcolor
+ style._attributes = self._attributes
+ style._set_attributes = self._set_attributes
+ style._link = self._link
+ style._link_id = f"{time()}-{randint(0, 999999)}" if self._link else ""
+ style._hash = self._hash
+ style._null = False
+ return style
+
+ def update_link(self, link: str = None) -> "Style":
+ """Get a copy with a different value for link.
+
+ Args:
+ link (str, optional): New value for link. Defaults to None.
+
+ Returns:
+ Style: A new Style instance.
+ """
+ style = self.__new__(Style)
+ style._ansi = self._ansi
+ style._style_definition = self._style_definition
+ style._color = self._color
+ style._bgcolor = self._bgcolor
+ style._attributes = self._attributes
+ style._set_attributes = self._set_attributes
+ style._link = link
+ style._link_id = f"{time()}-{randint(0, 999999)}" if link else ""
+ style._hash = self._hash
+ style._null = False
+ return style
+
+ def render(
+ self,
+ text: str = "",
+ *,
+ color_system: Optional[ColorSystem] = ColorSystem.TRUECOLOR,
+ legacy_windows: bool = False,
+ ) -> str:
+ """Render the ANSI codes for the style.
+
+ Args:
+ text (str, optional): A string to style. Defaults to "".
+ color_system (Optional[ColorSystem], optional): Color system to render to. Defaults to ColorSystem.TRUECOLOR.
+
+ Returns:
+ str: A string containing ANSI style codes.
+ """
+ if not text or color_system is None:
+ return text
+ attrs = self._make_ansi_codes(color_system)
+ rendered = f"\x1b[{attrs}m{text}\x1b[0m" if attrs else text
+ if self._link and not legacy_windows:
+ rendered = (
+ f"\x1b]8;id={self._link_id};{self._link}\x1b\\{rendered}\x1b]8;;\x1b\\"
+ )
+ return rendered
+
+ def test(self, text: Optional[str] = None) -> None:
+ """Write text with style directly to terminal.
+
+ This method is for testing purposes only.
+
+ Args:
+ text (Optional[str], optional): Text to style or None for style name.
+
+ """
+ text = text or str(self)
+ sys.stdout.write(f"{self.render(text)}\n")
+
+ def __add__(self, style: Optional["Style"]) -> "Style":
+ if not (isinstance(style, Style) or style is None):
+ return NotImplemented # type: ignore
+ if style is None or style._null:
+ return self
+ if self._null:
+ return style
+ new_style = self.__new__(Style)
+ new_style._ansi = None
+ new_style._style_definition = None
+ new_style._color = style._color or self._color
+ new_style._bgcolor = style._bgcolor or self._bgcolor
+ new_style._attributes = (self._attributes & ~style._set_attributes) | (
+ style._attributes & style._set_attributes
+ )
+ new_style._set_attributes = self._set_attributes | style._set_attributes
+ new_style._link = style._link or self._link
+ new_style._link_id = style._link_id or self._link_id
+ new_style._hash = style._hash
+ new_style._null = self._null or style._null
+ return new_style
+
+
+NULL_STYLE = Style()
+
+
+class StyleStack:
+ """A stack of styles."""
+
+ __slots__ = ["_stack"]
+
+ def __init__(self, default_style: "Style") -> None:
+ self._stack: List[Style] = [default_style]
+
+ def __repr__(self) -> str:
+ return f"<stylestack {self._stack!r}>"
+
+ @property
+ def current(self) -> Style:
+ """Get the Style at the top of the stack."""
+ return self._stack[-1]
+
+ def push(self, style: Style) -> None:
+ """Push a new style on to the stack.
+
+ Args:
+ style (Style): New style to combine with current style.
+ """
+ self._stack.append(self._stack[-1] + style)
+
+ def pop(self) -> Style:
+ """Pop last style and discard.
+
+ Returns:
+ Style: New current style (also available as stack.current)
+ """
+ self._stack.pop()
+ return self._stack[-1]
diff --git a/rich/styled.py b/rich/styled.py
new file mode 100644
index 0000000..f163122
--- /dev/null
+++ b/rich/styled.py
@@ -0,0 +1,40 @@
+from typing import TYPE_CHECKING
+
+from .measure import Measurement
+from .segment import Segment
+from .style import StyleType
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderResult, RenderableType
+
+
+class Styled:
+ """Apply a style to a renderable.
+
+ Args:
+ renderable (RenderableType): Any renderable.
+ style (StyleType): A style to apply across the entire renderable.
+ """
+
+ def __init__(self, renderable: "RenderableType", style: "StyleType") -> None:
+ self.renderable = renderable
+ self.style = style
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ style = console.get_style(self.style)
+ rendered_segments = console.render(self.renderable, options)
+ segments = Segment.apply_style(rendered_segments, style)
+ return segments
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
+ return Measurement.get(console, self.renderable, max_width)
+
+
+if __name__ == "__main__": # pragma: no cover
+ from rich import print
+ from rich.panel import Panel
+
+ panel = Styled(Panel("hello"), "on blue")
+ print(panel)
diff --git a/rich/syntax.py b/rich/syntax.py
new file mode 100644
index 0000000..7bb749f
--- /dev/null
+++ b/rich/syntax.py
@@ -0,0 +1,669 @@
+import os.path
+import platform
+import textwrap
+from abc import ABC, abstractmethod
+from typing import Any, Dict, Iterable, Optional, Set, Tuple, Type, Union
+
+from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename
+from pygments.style import Style as PygmentsStyle
+from pygments.styles import get_style_by_name
+from pygments.token import (
+ Comment,
+ Error,
+ Generic,
+ Keyword,
+ Name,
+ Number,
+ Operator,
+ String,
+ Token,
+ Whitespace,
+)
+from pygments.util import ClassNotFound
+
+from ._loop import loop_first
+from .color import Color, blend_rgb
+from .console import Console, ConsoleOptions, JustifyMethod, RenderResult, Segment
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .style import Style
+from .text import Text
+
+TokenType = Tuple[str, ...]
+
+WINDOWS = platform.system() == "Windows"
+DEFAULT_THEME = "monokai"
+
+# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py
+# A few modifications were made
+
+ANSI_LIGHT: Dict[TokenType, Style] = {
+ Token: Style(),
+ Whitespace: Style(color="white"),
+ Comment: Style(dim=True),
+ Comment.Preproc: Style(color="cyan"),
+ Keyword: Style(color="blue"),
+ Keyword.Type: Style(color="cyan"),
+ Operator.Word: Style(color="magenta"),
+ Name.Builtin: Style(color="cyan"),
+ Name.Function: Style(color="green"),
+ Name.Namespace: Style(color="cyan", underline=True),
+ Name.Class: Style(color="green", underline=True),
+ Name.Exception: Style(color="cyan"),
+ Name.Decorator: Style(color="magenta", bold=True),
+ Name.Variable: Style(color="red"),
+ Name.Constant: Style(color="red"),
+ Name.Attribute: Style(color="cyan"),
+ Name.Tag: Style(color="bright_blue"),
+ String: Style(color="yellow"),
+ Number: Style(color="blue"),
+ Generic.Deleted: Style(color="bright_red"),
+ Generic.Inserted: Style(color="green"),
+ Generic.Heading: Style(bold=True),
+ Generic.Subheading: Style(color="magenta", bold=True),
+ Generic.Prompt: Style(bold=True),
+ Generic.Error: Style(color="bright_red"),
+ Error: Style(color="red", underline=True),
+}
+
+ANSI_DARK: Dict[TokenType, Style] = {
+ Token: Style(),
+ Whitespace: Style(color="bright_black"),
+ Comment: Style(dim=True),
+ Comment.Preproc: Style(color="bright_cyan"),
+ Keyword: Style(color="bright_blue"),
+ Keyword.Type: Style(color="bright_cyan"),
+ Operator.Word: Style(color="bright_magenta"),
+ Name.Builtin: Style(color="bright_cyan"),
+ Name.Function: Style(color="bright_green"),
+ Name.Namespace: Style(color="bright_cyan", underline=True),
+ Name.Class: Style(color="bright_green", underline=True),
+ Name.Exception: Style(color="bright_cyan"),
+ Name.Decorator: Style(color="bright_magenta", bold=True),
+ Name.Variable: Style(color="bright_red"),
+ Name.Constant: Style(color="bright_red"),
+ Name.Attribute: Style(color="bright_cyan"),
+ Name.Tag: Style(color="bright_blue"),
+ String: Style(color="yellow"),
+ Number: Style(color="bright_blue"),
+ Generic.Deleted: Style(color="bright_red"),
+ Generic.Inserted: Style(color="bright_green"),
+ Generic.Heading: Style(bold=True),
+ Generic.Subheading: Style(color="bright_magenta", bold=True),
+ Generic.Prompt: Style(bold=True),
+ Generic.Error: Style(color="bright_red"),
+ Error: Style(color="red", underline=True),
+}
+
+RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK}
+
+
+class SyntaxTheme(ABC):
+ """Base class for a syntax theme."""
+
+ @abstractmethod
+ def get_style_for_token(self, token_type: TokenType) -> Style:
+ """Get a style for a given Pygments token."""
+ raise NotImplementedError # pragma: no cover
+
+ @abstractmethod
+ def get_background_style(self) -> Style:
+ """Get the background color."""
+ raise NotImplementedError # pragma: no cover
+
+
+class PygmentsSyntaxTheme(SyntaxTheme):
+ """Syntax theme that delagates to Pygments theme."""
+
+ def __init__(self, theme: Union[str, Type[PygmentsStyle]]) -> None:
+ self._style_cache: Dict[TokenType, Style] = {}
+ if isinstance(theme, str):
+ try:
+ self._pygments_style_class = get_style_by_name(theme)
+ except ClassNotFound:
+ self._pygments_style_class = get_style_by_name("default")
+ else:
+ self._pygments_style_class = theme
+
+ self._background_color = self._pygments_style_class.background_color
+ self._background_style = Style(bgcolor=self._background_color)
+
+ def get_style_for_token(self, token_type: TokenType) -> Style:
+ """Get a style from a Pygments class."""
+ try:
+ return self._style_cache[token_type]
+ except KeyError:
+ try:
+ pygments_style = self._pygments_style_class.style_for_token(token_type)
+ except KeyError:
+ style = Style.null()
+ else:
+ color = pygments_style["color"]
+ bgcolor = pygments_style["bgcolor"]
+ style = Style(
+ color="#" + color if color else "#000000",
+ bgcolor="#" + bgcolor if bgcolor else self._background_color,
+ bold=pygments_style["bold"],
+ italic=pygments_style["italic"],
+ underline=pygments_style["underline"],
+ )
+ self._style_cache[token_type] = style
+ return style
+
+ def get_background_style(self) -> Style:
+ return self._background_style
+
+
+class ANSISyntaxTheme(SyntaxTheme):
+ """Syntax theme to use standard colors."""
+
+ def __init__(self, style_map: Dict[TokenType, Style]) -> None:
+ self.style_map = style_map
+ self._missing_style = Style.null()
+ self._background_style = Style.null()
+ self._style_cache: Dict[TokenType, Style] = {}
+
+ def get_style_for_token(self, token_type: TokenType) -> Style:
+ """Look up style in the style map."""
+ try:
+ return self._style_cache[token_type]
+ except KeyError:
+ # Styles form a hierarchy
+ # We need to go from most to least specific
+ # e.g. ("foo", "bar", "baz") to ("foo", "bar") to ("foo",)
+ get_style = self.style_map.get
+ token = tuple(token_type)
+ style = self._missing_style
+ while token:
+ _style = get_style(token)
+ if _style is not None:
+ style = _style
+ break
+ token = token[:-1]
+ self._style_cache[token_type] = style
+ return style
+
+ def get_background_style(self) -> Style:
+ return self._background_style
+
+
+class Syntax(JupyterMixin):
+ """Construct a Syntax object to render syntax highlighted code.
+
+ Args:
+ code (str): Code to highlight.
+ 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 "monokai".
+ dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False.
+ line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.
+ start_line (int, optional): Starting number for line numbers. Defaults to 1.
+ line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render.
+ highlight_lines (Set[int]): A set of line numbers to highlight.
+ code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
+ tab_size (int, optional): Size of tabs. Defaults to 4.
+ word_wrap (bool, optional): Enable word wrapping.
+ background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
+ indent_guides (bool, optional): Show indent guides. Defaults to False.
+ """
+
+ _pygments_style_class: Type[PygmentsStyle]
+ _theme: SyntaxTheme
+
+ @classmethod
+ def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme:
+ """Get a syntax theme instance."""
+ if isinstance(name, SyntaxTheme):
+ return name
+ theme: SyntaxTheme
+ if name in RICH_SYNTAX_THEMES:
+ theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name])
+ else:
+ theme = PygmentsSyntaxTheme(name)
+ return theme
+
+ def __init__(
+ self,
+ code: str,
+ lexer_name: str,
+ *,
+ theme: Union[str, SyntaxTheme] = DEFAULT_THEME,
+ dedent: bool = False,
+ line_numbers: bool = False,
+ start_line: int = 1,
+ line_range: Tuple[int, int] = None,
+ highlight_lines: Set[int] = None,
+ code_width: Optional[int] = None,
+ tab_size: int = 4,
+ word_wrap: bool = False,
+ background_color: str = None,
+ indent_guides: bool = False,
+ ) -> None:
+ self.code = code
+ self.lexer_name = lexer_name
+ self.dedent = dedent
+ self.line_numbers = line_numbers
+ self.start_line = start_line
+ self.line_range = line_range
+ self.highlight_lines = highlight_lines or set()
+ self.code_width = code_width
+ self.tab_size = tab_size
+ self.word_wrap = word_wrap
+ self.background_color = background_color
+ self.indent_guides = indent_guides
+
+ self._theme = self.get_theme(theme)
+
+ @classmethod
+ def from_path(
+ cls,
+ path: str,
+ encoding: str = "utf-8",
+ theme: Union[str, SyntaxTheme] = DEFAULT_THEME,
+ dedent: bool = False,
+ line_numbers: bool = False,
+ line_range: Tuple[int, int] = None,
+ start_line: int = 1,
+ highlight_lines: Set[int] = None,
+ code_width: Optional[int] = None,
+ tab_size: int = 4,
+ word_wrap: bool = False,
+ background_color: str = None,
+ indent_guides: bool = False,
+ ) -> "Syntax":
+ """Construct a Syntax object from a file.
+
+ Args:
+ path (str): Path to file to highlight.
+ encoding (str): Encoding of file.
+ 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.
+ line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.
+ start_line (int, optional): Starting number for line numbers. Defaults to 1.
+ line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render.
+ highlight_lines (Set[int]): A set of line numbers to highlight.
+ code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
+ tab_size (int, optional): Size of tabs. Defaults to 4.
+ word_wrap (bool, optional): Enable word wrapping of code.
+ background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
+ indent_guides (bool, optional): Show indent guides. Defaults to False.
+
+ Returns:
+ [Syntax]: A Syntax object that may be printed to the console
+ """
+ with open(path, "rt", encoding=encoding) as code_file:
+ code = code_file.read()
+
+ lexer = None
+ lexer_name = "default"
+ try:
+ _, ext = os.path.splitext(path)
+ if ext:
+ extension = ext.lstrip(".").lower()
+ lexer = get_lexer_by_name(extension)
+ lexer_name = lexer.name
+ except ClassNotFound:
+ pass
+
+ if lexer is None:
+ try:
+ lexer_name = guess_lexer_for_filename(path, code).name
+ except ClassNotFound:
+ pass
+
+ return cls(
+ code,
+ lexer_name,
+ theme=theme,
+ dedent=dedent,
+ line_numbers=line_numbers,
+ line_range=line_range,
+ start_line=start_line,
+ highlight_lines=highlight_lines,
+ code_width=code_width,
+ tab_size=tab_size,
+ word_wrap=word_wrap,
+ background_color=background_color,
+ indent_guides=indent_guides,
+ )
+
+ def _get_base_style(self) -> Style:
+ """Get the base style."""
+ default_style = (
+ Style(bgcolor=self.background_color)
+ if self.background_color is not None
+ else self._theme.get_background_style()
+ )
+ return default_style
+
+ def _get_token_color(self, token_type: TokenType) -> Optional[Color]:
+ """Get a color (if any) for the given token.
+
+ Args:
+ token_type (TokenType): A token type tuple from Pygments.
+
+ Returns:
+ Optional[Color]: Color from theme, or None for no color.
+ """
+ style = self._theme.get_style_for_token(token_type)
+ return style.color
+
+ def highlight(self, code: str, line_range: Tuple[int, int] = None) -> Text:
+ """Highlight code and return a Text instance.
+
+ Args:
+ code (str): Code to highlight.
+ line_range(Tuple[int, int], optional): Optional line range to highlight.
+
+ Returns:
+ Text: A text instance containing highlighted syntax.
+ """
+
+ base_style = self._get_base_style()
+ justify: JustifyMethod = (
+ "default" if base_style.transparent_background else "left"
+ )
+
+ text = Text(
+ justify=justify,
+ style=base_style,
+ tab_size=self.tab_size,
+ no_wrap=not self.word_wrap,
+ )
+ _get_theme_style = self._theme.get_style_for_token
+ try:
+ lexer = get_lexer_by_name(self.lexer_name)
+ except ClassNotFound:
+ text.append(code)
+ else:
+ if line_range:
+ # More complicated path to only stylize a portion of the code
+ # This speeds up further operations as there are less spans to process
+ line_start, line_end = line_range
+
+ def line_tokenize() -> Iterable[Tuple[Any, str]]:
+ """Split tokens to one per line."""
+ for token_type, token in lexer.get_tokens(code):
+ while token:
+ line_token, new_line, token = token.partition("\n")
+ yield token_type, line_token + new_line
+
+ def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]:
+ """Convert tokens to spans."""
+ tokens = iter(line_tokenize())
+ line_no = 0
+ _line_start = line_start - 1
+
+ # Skip over tokens until line start
+ while line_no < _line_start:
+ _token_type, token = next(tokens)
+ yield (token, None)
+ if token.endswith("\n"):
+ line_no += 1
+ # Generate spans until line end
+ for token_type, token in tokens:
+ yield (token, _get_theme_style(token_type))
+ if token.endswith("\n"):
+ line_no += 1
+ if line_no >= line_end:
+ break
+
+ text.append_tokens(tokens_to_spans())
+
+ else:
+ text.append_tokens(
+ (token, _get_theme_style(token_type))
+ for token_type, token in lexer.get_tokens(code)
+ )
+ if self.background_color is not None:
+ text.stylize(f"on {self.background_color}")
+ return text
+
+ def _get_line_numbers_color(self, blend: float = 0.3) -> Color:
+ background_color = self._theme.get_background_style().bgcolor
+ if background_color is None or background_color.is_system_defined:
+ return background_color or Color.default()
+ foreground_color = self._get_token_color(Token.Text)
+ if foreground_color is None or foreground_color.is_system_defined:
+ return foreground_color or Color.default()
+ new_color = blend_rgb(
+ background_color.get_truecolor(),
+ foreground_color.get_truecolor(),
+ cross_fade=blend,
+ )
+ return Color.from_triplet(new_color)
+
+ @property
+ def _numbers_column_width(self) -> int:
+ """Get the number of characters used to render the numbers column."""
+ column_width = 0
+ if self.line_numbers:
+ column_width = len(str(self.start_line + self.code.count("\n"))) + 2
+ return column_width
+
+ def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]:
+ """Get background, number, and highlight styles for line numbers."""
+ background_style = self._get_base_style()
+ if background_style.transparent_background:
+ return Style.null(), Style(dim=True), Style.null()
+ if console.color_system in ("256", "truecolor"):
+ number_style = Style.chain(
+ background_style,
+ self._theme.get_style_for_token(Token.Text),
+ Style(color=self._get_line_numbers_color()),
+ )
+ highlight_number_style = Style.chain(
+ background_style,
+ self._theme.get_style_for_token(Token.Text),
+ Style(bold=True, color=self._get_line_numbers_color(0.9)),
+ )
+ else:
+ number_style = background_style + Style(dim=True)
+ highlight_number_style = background_style + Style(dim=False)
+ return background_style, number_style, highlight_number_style
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
+ if self.code_width is not None:
+ width = self.code_width + self._numbers_column_width
+ return Measurement(self._numbers_column_width, width)
+ return Measurement(self._numbers_column_width, max_width)
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+
+ transparent_background = self._get_base_style().transparent_background
+ code_width = (
+ (options.max_width - self._numbers_column_width - 1)
+ if self.code_width is None
+ else self.code_width
+ )
+
+ line_offset = 0
+ if self.line_range:
+ start_line, end_line = self.line_range
+ line_offset = max(0, start_line - 1)
+
+ code = textwrap.dedent(self.code) if self.dedent else self.code
+ code = code.expandtabs(self.tab_size)
+ text = self.highlight(code, self.line_range)
+ text.remove_suffix("\n")
+
+ (
+ background_style,
+ number_style,
+ highlight_number_style,
+ ) = self._get_number_styles(console)
+
+ if not self.line_numbers:
+ # Simple case of just rendering text
+ yield from console.render(text, options=options.update(width=code_width))
+ return
+
+ lines = text.split("\n")
+ if self.line_range:
+ lines = lines[line_offset:end_line]
+
+ if self.indent_guides and not options.ascii_only:
+ style = (
+ self._get_base_style()
+ + self._theme.get_style_for_token(Comment)
+ + Style(dim=True)
+ )
+ lines = (
+ Text("\n")
+ .join(lines)
+ .with_indent_guides(self.tab_size, style=style)
+ .split("\n")
+ )
+
+ numbers_column_width = self._numbers_column_width
+ render_options = options.update(width=code_width)
+
+ highlight_line = self.highlight_lines.__contains__
+ _Segment = Segment
+ padding = _Segment(" " * numbers_column_width + " ", background_style)
+ new_line = _Segment("\n")
+
+ line_pointer = "> " if options.legacy_windows else "❱ "
+
+ for line_no, line in enumerate(lines, self.start_line + line_offset):
+ if self.word_wrap:
+ wrapped_lines = console.render_lines(
+ line,
+ render_options,
+ style=background_style,
+ pad=not transparent_background,
+ )
+ else:
+ segments = list(line.render(console, end=""))
+ if options.no_wrap:
+ wrapped_lines = [segments]
+ else:
+ wrapped_lines = [
+ _Segment.adjust_line_length(
+ segments,
+ render_options.max_width,
+ style=background_style,
+ pad=not transparent_background,
+ )
+ ]
+ for first, wrapped_line in loop_first(wrapped_lines):
+ if first:
+ line_column = str(line_no).rjust(numbers_column_width - 2) + " "
+ if highlight_line(line_no):
+ yield _Segment(line_pointer, Style(color="red"))
+ yield _Segment(line_column, highlight_number_style)
+ else:
+ yield _Segment(" ", highlight_number_style)
+ yield _Segment(line_column, number_style)
+ else:
+ yield padding
+ yield from wrapped_line
+ yield new_line
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ import argparse
+ import sys
+
+ parser = argparse.ArgumentParser(
+ description="Render syntax to the console with Rich"
+ )
+ parser.add_argument(
+ "path",
+ metavar="PATH",
+ nargs="?",
+ help="path to file",
+ )
+ parser.add_argument(
+ "-c",
+ "--force-color",
+ dest="force_color",
+ action="store_true",
+ default=None,
+ help="force color for non-terminals",
+ )
+ parser.add_argument(
+ "-i",
+ "--indent-guides",
+ dest="indent_guides",
+ action="store_true",
+ default=False,
+ help="display indent guides",
+ )
+ parser.add_argument(
+ "-l",
+ "--line-numbers",
+ dest="line_numbers",
+ action="store_true",
+ help="render line numbers",
+ )
+ parser.add_argument(
+ "-w",
+ "--width",
+ type=int,
+ dest="width",
+ default=None,
+ help="width of output (default will auto-detect)",
+ )
+ parser.add_argument(
+ "-r",
+ "--wrap",
+ dest="word_wrap",
+ action="store_true",
+ default=False,
+ help="word wrap long lines",
+ )
+ parser.add_argument(
+ "-s",
+ "--soft-wrap",
+ action="store_true",
+ dest="soft_wrap",
+ default=False,
+ help="enable soft wrapping mode",
+ )
+ parser.add_argument(
+ "-t", "--theme", dest="theme", default="monokai", help="pygments theme"
+ )
+ parser.add_argument(
+ "-b",
+ "--background-color",
+ dest="background_color",
+ default=None,
+ help="Overide background color",
+ )
+ parser.add_argument(
+ "-x",
+ "--lexer",
+ default="default",
+ dest="lexer_name",
+ help="Lexer name",
+ )
+ args = parser.parse_args()
+
+ from rich.console import Console
+
+ console = Console(force_terminal=args.force_color, width=args.width)
+
+ if not args.path or args.path == "-":
+ code = sys.stdin.read()
+ syntax = Syntax(
+ code=code,
+ lexer_name=args.lexer_name,
+ line_numbers=args.line_numbers,
+ word_wrap=args.word_wrap,
+ theme=args.theme,
+ background_color=args.background_color,
+ indent_guides=args.indent_guides,
+ )
+ else:
+ syntax = Syntax.from_path(
+ args.path,
+ line_numbers=args.line_numbers,
+ word_wrap=args.word_wrap,
+ theme=args.theme,
+ background_color=args.background_color,
+ indent_guides=args.indent_guides,
+ )
+ console.print(syntax, soft_wrap=args.soft_wrap)
diff --git a/rich/table.py b/rich/table.py
new file mode 100644
index 0000000..41d0116
--- /dev/null
+++ b/rich/table.py
@@ -0,0 +1,881 @@
+from dataclasses import dataclass, field, replace
+from typing import TYPE_CHECKING, Iterable, List, NamedTuple, Optional, Tuple, Union
+
+from . import box, errors
+from ._loop import loop_first_last, loop_last
+from ._pick import pick_bool
+from ._ratio import ratio_distribute, ratio_reduce
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .padding import Padding, PaddingDimensions
+from .protocol import is_renderable
+from .segment import Segment
+from .style import Style, StyleType
+from .text import Text, TextType
+
+if TYPE_CHECKING:
+ from .console import (
+ Console,
+ ConsoleOptions,
+ JustifyMethod,
+ OverflowMethod,
+ RenderableType,
+ RenderResult,
+ )
+
+
+@dataclass
+class Column:
+ """Defines a column in a table."""
+
+ header: "RenderableType" = ""
+ """RenderableType: Renderable for the header (typically a string)"""
+
+ footer: "RenderableType" = ""
+ """RenderableType: Renderable for the footer (typically a string)"""
+
+ header_style: StyleType = ""
+ """StyleType: The style of the header."""
+
+ footer_style: StyleType = ""
+ """StyleType: The style of the footer."""
+
+ style: StyleType = ""
+ """StyleType: The style of the column."""
+
+ justify: "JustifyMethod" = "left"
+ """str: How to justify text within the column ("left", "center", "right", or "full")"""
+
+ overflow: "OverflowMethod" = "ellipsis"
+ """str: Overflow method."""
+
+ width: Optional[int] = None
+ """Optional[int]: Width of the column, or ``None`` (default) to auto calculate width."""
+
+ min_width: Optional[int] = None
+ """Optional[int]: Minimum width of column, or ``None`` for no minimum. Defaults to None."""
+
+ max_width: Optional[int] = None
+ """Optional[int]: Maximum width of column, or ``None`` for no maximum. Defaults to None."""
+
+ ratio: Optional[int] = None
+ """Optional[int]: Ratio to use when calculating column width, or ``None`` (default) to adapt to column contents."""
+
+ no_wrap: bool = False
+ """bool: Prevent wrapping of text within the column. Defaults to ``False``."""
+
+ _index: int = 0
+ """Index of column."""
+
+ _cells: List["RenderableType"] = field(default_factory=list)
+
+ def copy(self) -> "Column":
+ """Return a copy of this Column."""
+ return replace(self, _cells=[])
+
+ @property
+ def cells(self) -> Iterable["RenderableType"]:
+ """Get all cells in the column, not including header."""
+ yield from self._cells
+
+ @property
+ def flexible(self) -> bool:
+ """Check if this column is flexible."""
+ return self.ratio is not None
+
+
+@dataclass
+class Row:
+ """Information regarding a row."""
+
+ style: Optional[StyleType] = None
+ """Style to apply to row."""
+
+ end_section: bool = False
+ """Indicated end of section, which will force a line beneath the row."""
+
+
+class _Cell(NamedTuple):
+ """A single cell in a table."""
+
+ style: StyleType
+ """Style to apply to cell."""
+ renderable: "RenderableType"
+ """Cell renderable."""
+
+
+class Table(JupyterMixin):
+ """A console renderable to draw a table.
+
+ Args:
+ *headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance.
+ title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None.
+ caption (Union[str, Text], optional): The table caption rendered below. Defaults to None.
+ width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None.
+ min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None.
+ box (box.Box, optional): One of the constants in box.py used to draw the edges (see :ref:`appendix_box`). Defaults to box.HEAVY_HEAD.
+ safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
+ padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1).
+ collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False.
+ pad_edge (bool, optional): Enable padding of edge cells. Defaults to True.
+ expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
+ show_header (bool, optional): Show a header row. Defaults to True.
+ show_footer (bool, optional): Show a footer row. Defaults to False.
+ show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
+ show_lines (bool, optional): Draw lines between every row. Defaults to False.
+ leading (bool, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0.
+ style (Union[str, Style], optional): Default style for the table. Defaults to "none".
+ row_styles (List[Union, str], optional): Optional list of row styles, if more that one style is give then the styles will alternate. Defaults to None.
+ header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header".
+ footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer".
+ border_style (Union[str, Style], optional): Style of the border. Defaults to None.
+ title_style (Union[str, Style], optional): Style of the title. Defaults to None.
+ caption_style (Union[str, Style], optional): Style of the caption. Defaults to None.
+ title_justify (str, optional): Justify method for title. Defaults to "center".
+ caption_justify (str, optional): Justify method for caption. Defaults to "center".
+ highlight (bool, optional): Highlight cell contents (if str). Defaults to False.
+ """
+
+ columns: List[Column]
+ rows: List[Row]
+
+ def __init__(
+ self,
+ *headers: Union[Column, str],
+ title: TextType = None,
+ caption: TextType = None,
+ width: int = None,
+ min_width: int = None,
+ box: Optional[box.Box] = box.HEAVY_HEAD,
+ safe_box: Optional[bool] = None,
+ padding: PaddingDimensions = (0, 1),
+ collapse_padding: bool = False,
+ pad_edge: bool = True,
+ expand: bool = False,
+ show_header: bool = True,
+ show_footer: bool = False,
+ show_edge: bool = True,
+ show_lines: bool = False,
+ leading: int = 0,
+ style: StyleType = "none",
+ row_styles: Iterable[StyleType] = None,
+ header_style: Optional[StyleType] = "table.header",
+ footer_style: Optional[StyleType] = "table.footer",
+ border_style: StyleType = None,
+ title_style: StyleType = None,
+ caption_style: StyleType = None,
+ title_justify: "JustifyMethod" = "center",
+ caption_justify: "JustifyMethod" = "center",
+ highlight: bool = False,
+ ) -> None:
+
+ self.columns: List[Column] = []
+ self.rows: List[Row] = []
+ self.title = title
+ self.caption = caption
+ self.width = width
+ self.min_width = min_width
+ self.box = box
+ self.safe_box = safe_box
+ self._padding = Padding.unpack(padding)
+ self.pad_edge = pad_edge
+ self._expand = expand
+ self.show_header = show_header
+ self.show_footer = show_footer
+ self.show_edge = show_edge
+ self.show_lines = show_lines
+ self.leading = leading
+ self.collapse_padding = collapse_padding
+ self.style = style
+ self.header_style = header_style or ""
+ self.footer_style = footer_style or ""
+ self.border_style = border_style
+ self.title_style = title_style
+ self.caption_style = caption_style
+ self.title_justify = title_justify
+ self.caption_justify = caption_justify
+ self.highlight = highlight
+ self.row_styles = list(row_styles or [])
+ append_column = self.columns.append
+ for header in headers:
+ if isinstance(header, str):
+ self.add_column(header=header)
+ else:
+ header._index = len(self.columns)
+ append_column(header)
+
+ @classmethod
+ def grid(
+ cls,
+ *headers: Union[Column, str],
+ padding: PaddingDimensions = 0,
+ collapse_padding: bool = True,
+ pad_edge: bool = False,
+ expand: bool = False,
+ ) -> "Table":
+ """Get a table with no lines, headers, or footer.
+
+ Args:
+ *headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance.
+ padding (PaddingDimensions, optional): Get padding around cells. Defaults to 0.
+ collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to True.
+ pad_edge (bool, optional): Enable padding around edges of table. Defaults to False.
+ expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
+
+ Returns:
+ Table: A table instance.
+ """
+ return cls(
+ *headers,
+ box=None,
+ padding=padding,
+ collapse_padding=collapse_padding,
+ show_header=False,
+ show_footer=False,
+ show_edge=False,
+ pad_edge=pad_edge,
+ expand=expand,
+ )
+
+ @property
+ def expand(self) -> int:
+ """Setting a non-None self.width implies expand."""
+ return self._expand or self.width is not None
+
+ @expand.setter
+ def expand(self, expand: bool) -> None:
+ """Set expand."""
+ self._expand = expand
+
+ @property
+ def _extra_width(self) -> int:
+ """Get extra width to add to cell content."""
+ width = 0
+ if self.box and self.show_edge:
+ width += 2
+ if self.box:
+ width += len(self.columns) - 1
+ return width
+
+ @property
+ def row_count(self) -> int:
+ """Get the current number of rows."""
+ return len(self.rows)
+
+ def get_row_style(self, console: "Console", index: int) -> StyleType:
+ """Get the current row style."""
+ style = Style.null()
+ if self.row_styles:
+ style += console.get_style(self.row_styles[index % len(self.row_styles)])
+ row_style = self.rows[index].style
+ if row_style is not None:
+ style += console.get_style(row_style)
+ return style
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
+ if self.width is not None:
+ max_width = self.width
+ if max_width < 0:
+ return Measurement(0, 0)
+
+ extra_width = self._extra_width
+ max_width = sum(self._calculate_column_widths(console, max_width - extra_width))
+ _measure_column = self._measure_column
+
+ measurements = [
+ _measure_column(console, column, max_width) for column in self.columns
+ ]
+ minimum_width = (
+ sum(measurement.minimum for measurement in measurements) + extra_width
+ )
+ maximum_width = (
+ sum(measurement.maximum for measurement in measurements) + extra_width
+ if (self.width is None)
+ else self.width
+ )
+ measurement = Measurement(minimum_width, maximum_width)
+ measurement = measurement.clamp(self.min_width)
+ return measurement
+
+ @property
+ def padding(self) -> Tuple[int, int, int, int]:
+ """Get cell padding."""
+ return self._padding
+
+ @padding.setter
+ def padding(self, padding: PaddingDimensions) -> "Table":
+ """Set cell padding."""
+ self._padding = Padding.unpack(padding)
+ return self
+
+ def add_column(
+ self,
+ header: "RenderableType" = "",
+ footer: "RenderableType" = "",
+ *,
+ header_style: StyleType = None,
+ footer_style: StyleType = None,
+ style: StyleType = None,
+ justify: "JustifyMethod" = "left",
+ overflow: "OverflowMethod" = "ellipsis",
+ width: int = None,
+ min_width: int = None,
+ max_width: int = None,
+ ratio: int = None,
+ no_wrap: bool = False,
+ ) -> None:
+ """Add a column to the table.
+
+ Args:
+ header (RenderableType, optional): Text or renderable for the header.
+ Defaults to "".
+ footer (RenderableType, optional): Text or renderable for the footer.
+ Defaults to "".
+ header_style (Union[str, Style], optional): Style for the header, or None for default. Defaults to None.
+ footer_style (Union[str, Style], optional): Style for the footer, or None for default. Defaults to None.
+ style (Union[str, Style], optional): Style for the column cells, or None for default. Defaults to None.
+ justify (JustifyMethod, optional): Alignment for cells. Defaults to "left".
+ width (int, optional): Desired width of column in characters, or None to fit to contents. Defaults to None.
+ min_width (Optional[int], optional): Minimum width of column, or ``None`` for no minimum. Defaults to None.
+ max_width (Optional[int], optional): Maximum width of column, or ``None`` for no maximum. Defaults to None.
+ ratio (int, optional): Flexible ratio for the column (requires ``Table.expand`` or ``Table.width``). Defaults to None.
+ no_wrap (bool, optional): Set to ``True`` to disable wrapping of this column.
+ """
+
+ column = Column(
+ _index=len(self.columns),
+ header=header,
+ footer=footer,
+ header_style=header_style or "",
+ footer_style=footer_style or "",
+ style=style or "",
+ justify=justify,
+ overflow=overflow,
+ width=width,
+ min_width=min_width,
+ max_width=max_width,
+ ratio=ratio,
+ no_wrap=no_wrap,
+ )
+ self.columns.append(column)
+
+ def add_row(
+ self,
+ *renderables: Optional["RenderableType"],
+ style: StyleType = None,
+ end_section: bool = False,
+ ) -> None:
+ """Add a row of renderables.
+
+ Args:
+ *renderables (None or renderable): Each cell in a row must be a renderable object (including str),
+ or ``None`` for a blank cell.
+ style (StyleType, optional): An optional style to apply to the entire row. Defaults to None.
+ end_section (bool, optional): End a section and draw a line. Defaults to False.
+
+ Raises:
+ errors.NotRenderableError: If you add something that can't be rendered.
+ """
+
+ def add_cell(column: Column, renderable: "RenderableType") -> None:
+ column._cells.append(renderable)
+
+ cell_renderables: List[Optional["RenderableType"]] = list(renderables)
+
+ columns = self.columns
+ if len(cell_renderables) < len(columns):
+ cell_renderables = [
+ *cell_renderables,
+ *[None] * (len(columns) - len(cell_renderables)),
+ ]
+ for index, renderable in enumerate(cell_renderables):
+ if index == len(columns):
+ column = Column(_index=index)
+ for _ in self.rows:
+ add_cell(column, Text(""))
+ self.columns.append(column)
+ else:
+ column = columns[index]
+ if renderable is None:
+ add_cell(column, "")
+ elif is_renderable(renderable):
+ add_cell(column, renderable)
+ else:
+ raise errors.NotRenderableError(
+ f"unable to render {type(renderable).__name__}; a string or other renderable object is required"
+ )
+ self.rows.append(Row(style=style, end_section=end_section))
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+
+ max_width = options.max_width
+ if self.width is not None:
+ max_width = self.width
+
+ extra_width = self._extra_width
+ widths = self._calculate_column_widths(console, max_width - extra_width)
+ table_width = sum(widths) + extra_width
+
+ render_options = options.update(
+ width=table_width, highlight=self.highlight, height=None
+ )
+
+ def render_annotation(
+ text: TextType, style: StyleType, justify: "JustifyMethod" = "center"
+ ) -> "RenderResult":
+ render_text = (
+ console.render_str(text, style=style, highlight=False)
+ if isinstance(text, str)
+ else text
+ )
+ return console.render(
+ render_text, options=render_options.update(justify=justify)
+ )
+
+ if self.title:
+ yield from render_annotation(
+ self.title,
+ style=Style.pick_first(self.title_style, "table.title"),
+ justify=self.title_justify,
+ )
+ yield from self._render(console, render_options, widths)
+ if self.caption:
+ yield from render_annotation(
+ self.caption,
+ style=Style.pick_first(self.caption_style, "table.caption"),
+ justify=self.caption_justify,
+ )
+
+ def _calculate_column_widths(self, console: "Console", max_width: int) -> List[int]:
+ """Calculate the widths of each column, including padding, not including borders."""
+ columns = self.columns
+ width_ranges = [
+ self._measure_column(console, column, max_width) for column in columns
+ ]
+ widths = [_range.maximum or 1 for _range in width_ranges]
+ get_padding_width = self._get_padding_width
+ extra_width = self._extra_width
+
+ if self.expand:
+ ratios = [col.ratio or 0 for col in columns if col.flexible]
+ if any(ratios):
+ fixed_widths = [
+ 0 if column.flexible else _range.maximum
+ for _range, column in zip(width_ranges, columns)
+ ]
+ flex_minimum = [
+ (column.width or 1) + get_padding_width(column._index)
+ for column in columns
+ if column.flexible
+ ]
+ flexible_width = max_width - sum(fixed_widths)
+ flex_widths = ratio_distribute(flexible_width, ratios, flex_minimum)
+ iter_flex_widths = iter(flex_widths)
+ for index, column in enumerate(columns):
+ if column.flexible:
+ widths[index] = fixed_widths[index] + next(iter_flex_widths)
+ table_width = sum(widths)
+
+ if table_width > max_width:
+ widths = self._collapse_widths(
+ widths,
+ [(column.width is None and not column.no_wrap) for column in columns],
+ max_width,
+ )
+ table_width = sum(widths)
+
+ # last resort, reduce columns evenly
+ if table_width > max_width:
+ excess_width = table_width - max_width
+ widths = ratio_reduce(excess_width, [1] * len(widths), widths, widths)
+ table_width = sum(widths)
+
+ width_ranges = [
+ self._measure_column(console, column, width)
+ for width, column in zip(widths, columns)
+ ]
+ widths = [_range.maximum or 1 for _range in width_ranges]
+
+ if (table_width < max_width and self.expand) or (
+ self.min_width is not None and table_width < (self.min_width - extra_width)
+ ):
+ _max_width = (
+ max_width
+ if self.min_width is None
+ else min(self.min_width - extra_width, max_width)
+ )
+ pad_widths = ratio_distribute(_max_width - table_width, widths)
+ widths = [_width + pad for _width, pad in zip(widths, pad_widths)]
+
+ return widths
+
+ @classmethod
+ def _collapse_widths(
+ cls, widths: List[int], wrapable: List[bool], max_width: int
+ ) -> List[int]:
+ """Reduce widths so that the total is under max_width.
+
+ Args:
+ widths (List[int]): List of widths.
+ wrapable (List[bool]): List of booleans that indicate if a column may shrink.
+ max_width (int): Maximum width to reduce to.
+
+ Returns:
+ List[int]: A new list of widths.
+ """
+ total_width = sum(widths)
+ excess_width = total_width - max_width
+ if any(wrapable):
+ while total_width and excess_width > 0:
+ max_column = max(
+ width for width, allow_wrap in zip(widths, wrapable) if allow_wrap
+ )
+ second_max_column = max(
+ width if allow_wrap and width != max_column else 0
+ for width, allow_wrap in zip(widths, wrapable)
+ )
+ column_difference = max_column - second_max_column
+ ratios = [
+ (1 if (width == max_column and allow_wrap) else 0)
+ for width, allow_wrap in zip(widths, wrapable)
+ ]
+ if not any(ratios) or not column_difference:
+ break
+ max_reduce = [min(excess_width, column_difference)] * len(widths)
+ widths = ratio_reduce(excess_width, ratios, max_reduce, widths)
+
+ total_width = sum(widths)
+ excess_width = total_width - max_width
+ return widths
+
+ def _get_cells(
+ self, console: "Console", column_index: int, column: Column
+ ) -> Iterable[_Cell]:
+ """Get all the cells with padding and optional header."""
+
+ collapse_padding = self.collapse_padding
+ pad_edge = self.pad_edge
+ padding = self.padding
+ any_padding = any(padding)
+
+ first_column = column_index == 0
+ last_column = column_index == len(self.columns) - 1
+
+ def add_padding(
+ renderable: "RenderableType", first_row: bool, last_row: bool
+ ) -> "RenderableType":
+ if not any_padding:
+ return renderable
+ top, right, bottom, left = padding
+
+ if collapse_padding:
+ if not first_column:
+ left = max(0, left - right)
+ if not last_row:
+ bottom = max(0, top - bottom)
+
+ if not pad_edge:
+ if first_column:
+ left = 0
+ if last_column:
+ right = 0
+ if first_row:
+ top = 0
+ if last_row:
+ bottom = 0
+ _padding = Padding(renderable, (top, right, bottom, left))
+ return _padding
+
+ raw_cells: List[Tuple[StyleType, "RenderableType"]] = []
+ _append = raw_cells.append
+ get_style = console.get_style
+ if self.show_header:
+ header_style = get_style(self.header_style or "") + get_style(
+ column.header_style
+ )
+ _append((header_style, column.header))
+ cell_style = get_style(self.style or "") + get_style(column.style)
+ for cell in column.cells:
+ _append((cell_style, cell))
+ if self.show_footer:
+ footer_style = get_style(self.footer_style or "") + get_style(
+ column.footer_style
+ )
+ _append((footer_style, column.footer))
+ for first, last, (style, renderable) in loop_first_last(raw_cells):
+ yield _Cell(style, add_padding(renderable, first, last))
+
+ def _get_padding_width(self, column_index: int) -> int:
+ """Get extra width from padding."""
+ _, pad_right, _, pad_left = self.padding
+ if self.collapse_padding:
+ if column_index > 0:
+ pad_left = max(0, pad_left - pad_right)
+ return pad_left + pad_right
+
+ def _measure_column(
+ self, console: "Console", column: Column, max_width: int
+ ) -> Measurement:
+ """Get the minimum and maximum width of the column."""
+
+ if max_width < 1:
+ return Measurement(0, 0)
+
+ padding_width = self._get_padding_width(column._index)
+
+ if column.width is not None:
+ # Fixed width column
+ return Measurement(
+ column.width + padding_width, column.width + padding_width
+ ).with_maximum(max_width)
+ # Flexible column, we need to measure contents
+ min_widths: List[int] = []
+ max_widths: List[int] = []
+ append_min = min_widths.append
+ append_max = max_widths.append
+ get_render_width = Measurement.get
+ for cell in self._get_cells(console, column._index, column):
+ _min, _max = get_render_width(console, cell.renderable, max_width)
+ append_min(_min)
+ append_max(_max)
+
+ measurement = Measurement(
+ max(min_widths) if min_widths else 1,
+ max(max_widths) if max_widths else max_width,
+ ).with_maximum(max_width)
+ measurement = measurement.clamp(
+ None if column.min_width is None else column.min_width + padding_width,
+ None if column.max_width is None else column.max_width + padding_width,
+ )
+ return measurement
+
+ def _render(
+ self, console: "Console", options: "ConsoleOptions", widths: List[int]
+ ) -> "RenderResult":
+ table_style = console.get_style(self.style or "")
+
+ border_style = table_style + console.get_style(self.border_style or "")
+ _column_cells = (
+ self._get_cells(console, column_index, column)
+ for column_index, column in enumerate(self.columns)
+ )
+ row_cells: List[Tuple[_Cell, ...]] = list(zip(*_column_cells))
+ _box = (
+ self.box.substitute(
+ options, safe=pick_bool(self.safe_box, console.safe_box)
+ )
+ if self.box
+ else None
+ )
+
+ # _box = self.box
+ new_line = Segment.line()
+
+ columns = self.columns
+ show_header = self.show_header
+ show_footer = self.show_footer
+ show_edge = self.show_edge
+ show_lines = self.show_lines
+ leading = self.leading
+
+ _Segment = Segment
+ if _box:
+ box_segments = [
+ (
+ _Segment(_box.head_left, border_style),
+ _Segment(_box.head_right, border_style),
+ _Segment(_box.head_vertical, border_style),
+ ),
+ (
+ _Segment(_box.foot_left, border_style),
+ _Segment(_box.foot_right, border_style),
+ _Segment(_box.foot_vertical, border_style),
+ ),
+ (
+ _Segment(_box.mid_left, border_style),
+ _Segment(_box.mid_right, border_style),
+ _Segment(_box.mid_vertical, border_style),
+ ),
+ ]
+ if show_edge:
+ yield _Segment(_box.get_top(widths), border_style)
+ yield new_line
+ else:
+ box_segments = []
+
+ get_row_style = self.get_row_style
+ get_style = console.get_style
+
+ for index, (first, last, row_cell) in enumerate(loop_first_last(row_cells)):
+ header_row = first and show_header
+ footer_row = last and show_footer
+ row = (
+ self.rows[index - show_header]
+ if (not header_row and not footer_row)
+ else None
+ )
+ max_height = 1
+ cells: List[List[List[Segment]]] = []
+ if header_row or footer_row:
+ row_style = Style.null()
+ else:
+ row_style = get_style(
+ get_row_style(console, index - 1 if show_header else index)
+ )
+ for width, cell, column in zip(widths, row_cell, columns):
+ render_options = options.update(
+ width=width,
+ justify=column.justify,
+ no_wrap=column.no_wrap,
+ overflow=column.overflow,
+ height=None,
+ )
+ cell_style = table_style + row_style + get_style(cell.style)
+ lines = console.render_lines(
+ cell.renderable, render_options, style=cell_style
+ )
+ max_height = max(max_height, len(lines))
+ cells.append(lines)
+
+ cells[:] = [
+ _Segment.set_shape(
+ _cell, width, max_height, style=table_style + row_style
+ )
+ for width, _cell in zip(widths, cells)
+ ]
+
+ if _box:
+ if last and show_footer:
+ yield _Segment(
+ _box.get_row(widths, "foot", edge=show_edge), border_style
+ )
+ yield new_line
+ left, right, _divider = box_segments[0 if first else (2 if last else 1)]
+
+ # If the column divider is whitespace also style it with the row background
+ divider = (
+ _divider
+ if _divider.text.strip()
+ else _Segment(
+ _divider.text, row_style.background_style + _divider.style
+ )
+ )
+ for line_no in range(max_height):
+ if show_edge:
+ yield left
+ for last_cell, rendered_cell in loop_last(cells):
+ yield from rendered_cell[line_no]
+ if not last_cell:
+ yield divider
+ if show_edge:
+ yield right
+ yield new_line
+ else:
+ for line_no in range(max_height):
+ for rendered_cell in cells:
+ yield from rendered_cell[line_no]
+ yield new_line
+ if _box and first and show_header:
+ yield _Segment(
+ _box.get_row(widths, "head", edge=show_edge), border_style
+ )
+ yield new_line
+ end_section = row and row.end_section
+ if _box and (show_lines or leading or end_section):
+ if (
+ not last
+ and not (show_footer and index >= len(row_cells) - 2)
+ and not (show_header and header_row)
+ ):
+ if leading:
+ yield _Segment(
+ _box.get_row(widths, "mid", edge=show_edge) * leading,
+ border_style,
+ )
+ else:
+ yield _Segment(
+ _box.get_row(widths, "row", edge=show_edge), border_style
+ )
+ yield new_line
+
+ if _box and show_edge:
+ yield _Segment(_box.get_bottom(widths), border_style)
+ yield new_line
+
+
+if __name__ == "__main__": # pragma: no cover
+ from rich.console import Console
+ from rich.highlighter import ReprHighlighter
+ from rich.table import Table
+
+ table = Table(
+ title="Star Wars Movies",
+ caption="Rich example table",
+ caption_justify="right",
+ )
+
+ table.add_column("Released", header_style="bright_cyan", style="cyan", no_wrap=True)
+ table.add_column("Title", style="magenta")
+ table.add_column("Box Office", justify="right", style="green")
+
+ table.add_row(
+ "Dec 20, 2019",
+ "Star Wars: The Rise of Skywalker",
+ "$952,110,690",
+ )
+ table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
+ table.add_row(
+ "Dec 15, 2017",
+ "Star Wars Ep. V111: The Last Jedi",
+ "$1,332,539,889",
+ style="on black",
+ end_section=True,
+ )
+ table.add_row(
+ "Dec 16, 2016",
+ "Rogue One: A Star Wars Story",
+ "$1,332,439,889",
+ )
+
+ def header(text: str) -> None:
+ console.print()
+ console.rule(highlight(text))
+ console.print()
+
+ console = Console()
+ highlight = ReprHighlighter()
+ header("Example Table")
+ console.print(table, justify="center")
+
+ table.expand = True
+ header("expand=True")
+ console.print(table, justify="center")
+
+ table.width = 50
+ header("width=50")
+
+ console.print(table, justify="center")
+
+ table.width = None
+ table.expand = False
+ table.row_styles = ["dim", "none"]
+ header("row_styles=['dim', 'none']")
+
+ console.print(table, justify="center")
+
+ table.width = None
+ table.expand = False
+ table.row_styles = ["dim", "none"]
+ table.leading = 1
+ header("leading=1, row_styles=['dim', 'none']")
+ console.print(table, justify="center")
+
+ table.width = None
+ table.expand = False
+ table.row_styles = ["dim", "none"]
+ table.show_lines = True
+ table.leading = 0
+ header("show_lines=True, row_styles=['dim', 'none']")
+ console.print(table, justify="center")
diff --git a/rich/tabulate.py b/rich/tabulate.py
new file mode 100644
index 0000000..2966948
--- /dev/null
+++ b/rich/tabulate.py
@@ -0,0 +1,75 @@
+from collections.abc import Mapping
+from typing import Optional
+
+from rich.console import JustifyMethod
+
+from . import box
+from .highlighter import ReprHighlighter
+from .pretty import Pretty
+from .table import Table
+
+
+def tabulate_mapping(
+ mapping: Mapping,
+ title: str = None,
+ caption: str = None,
+ title_justify: Optional[JustifyMethod] = None,
+ caption_justify: Optional[JustifyMethod] = None,
+) -> Table:
+ """Generate a simple table from a mapping.
+
+ Args:
+ mapping (Mapping): A mapping object (e.g. a dict);
+ title (str, optional): Optional title to be displayed over the table.
+ caption (str, optional): Optional caption to be displayed below the table.
+ title_justify (str, optional): Justify method for title. Defaults to None.
+ caption_justify (str, optional): Justify method for caption. Defaults to None.
+
+ Returns:
+ Table: A table instance which may be rendered by the Console.
+ """
+ table = Table(
+ show_header=False,
+ title=title,
+ caption=caption,
+ box=box.ROUNDED,
+ border_style="blue",
+ )
+ table.title = title
+ table.caption = caption
+ if title_justify is not None:
+ table.title_justify = title_justify
+ if caption_justify is not None:
+ table.caption_justify = caption_justify
+ highlighter = ReprHighlighter()
+ for key, value in mapping.items():
+ table.add_row(
+ Pretty(key, highlighter=highlighter), Pretty(value, highlighter=highlighter)
+ )
+ return table
+
+
+if __name__ == "__main__": # pragma: no cover
+ from rich import print
+
+ def test(foo, bar, tjustify=None, cjustify=None):
+ list_of_things = [1, 2, 3, None, 4, True, False, "Hello World"]
+ dict_of_things = {
+ "version": "1.1",
+ "method": "confirmFruitPurchase",
+ "params": [["apple", "orange", "mangoes", "pomelo"], 1.123],
+ "id": "194521489",
+ }
+ print(
+ tabulate_mapping(
+ locals(),
+ title="locals()",
+ title_justify=tjustify,
+ caption="__main__.test",
+ caption_justify=cjustify,
+ )
+ )
+
+ print()
+ test(20.3423, 3.1427, cjustify="right")
+ print()
diff --git a/rich/terminal_theme.py b/rich/terminal_theme.py
new file mode 100644
index 0000000..a5ca1c0
--- /dev/null
+++ b/rich/terminal_theme.py
@@ -0,0 +1,55 @@
+from typing import List, Tuple
+
+from .color_triplet import ColorTriplet
+from .palette import Palette
+
+_ColorTuple = Tuple[int, int, int]
+
+
+class TerminalTheme:
+ """A color theme used when exporting console content.
+
+ Args:
+ background (Tuple[int, int, int]): The background color.
+ foreground (Tuple[int, int, int]): The foreground (text) color.
+ normal (List[Tuple[int, int, int]]): A list of 8 normal intensity colors.
+ bright (List[Tuple[int, int, int]], optional): A list of 8 bright colors, or None
+ to repeat normal intensity. Defaults to None.
+ """
+
+ def __init__(
+ self,
+ background: _ColorTuple,
+ foreground: _ColorTuple,
+ normal: List[_ColorTuple],
+ bright: List[_ColorTuple] = None,
+ ) -> None:
+ self.background_color = ColorTriplet(*background)
+ self.foreground_color = ColorTriplet(*foreground)
+ self.ansi_colors = Palette(normal + (bright or normal))
+
+
+DEFAULT_TERMINAL_THEME = TerminalTheme(
+ (255, 255, 255),
+ (0, 0, 0),
+ [
+ (0, 0, 0),
+ (128, 0, 0),
+ (0, 128, 0),
+ (128, 128, 0),
+ (0, 0, 128),
+ (128, 0, 128),
+ (0, 128, 128),
+ (192, 192, 192),
+ ],
+ [
+ (128, 128, 128),
+ (255, 0, 0),
+ (0, 255, 0),
+ (255, 255, 0),
+ (0, 0, 255),
+ (255, 0, 255),
+ (0, 255, 255),
+ (255, 255, 255),
+ ],
+)
diff --git a/rich/text.py b/rich/text.py
new file mode 100644
index 0000000..6a4a332
--- /dev/null
+++ b/rich/text.py
@@ -0,0 +1,1133 @@
+import re
+from functools import partial, reduce
+from math import gcd
+from operator import attrgetter, itemgetter
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ Optional,
+ Tuple,
+ Union,
+ cast,
+)
+
+from ._loop import loop_last
+from ._pick import pick_bool
+from ._wrap import divide_line
+from .align import AlignMethod
+from .cells import cell_len, set_cell_size
+from .containers import Lines
+from .control import strip_control_codes
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .segment import Segment
+from .style import Style, StyleType
+
+if TYPE_CHECKING: # pragma: no cover
+ from .console import Console, ConsoleOptions, JustifyMethod, OverflowMethod
+
+DEFAULT_JUSTIFY: "JustifyMethod" = "default"
+DEFAULT_OVERFLOW: "OverflowMethod" = "fold"
+
+
+_re_whitespace = re.compile(r"\s+$")
+
+TextType = Union[str, "Text"]
+
+GetStyleCallable = Callable[[str], Optional[StyleType]]
+
+
+class Span(NamedTuple):
+ """A marked up region in some text."""
+
+ start: int
+ """Span start index."""
+ end: int
+ """Span end index."""
+ style: Union[str, Style]
+ """Style associated with the span."""
+
+ def __repr__(self) -> str:
+ return f"Span({self.start}, {self.end}, {str(self.style)!r})"
+
+ def __bool__(self) -> bool:
+ return self.end > self.start
+
+ def split(self, offset: int) -> Tuple["Span", Optional["Span"]]:
+ """Split a span in to 2 from a given offset."""
+
+ if offset < self.start:
+ return self, None
+ if offset >= self.end:
+ return self, None
+
+ start, end, style = self
+ span1 = Span(start, min(end, offset), style)
+ span2 = Span(span1.end, end, style)
+ return span1, span2
+
+ def move(self, offset: int) -> "Span":
+ """Move start and end by a given offset.
+
+ Args:
+ offset (int): Number of characters to add to start and end.
+
+ Returns:
+ TextSpan: A new TextSpan with adjusted position.
+ """
+ start, end, style = self
+ return Span(start + offset, end + offset, style)
+
+ def right_crop(self, offset: int) -> "Span":
+ """Crop the span at the given offset.
+
+ Args:
+ offset (int): A value between start and end.
+
+ Returns:
+ Span: A new (possibly smaller) span.
+ """
+ start, end, style = self
+ if offset >= end:
+ return self
+ return Span(start, min(offset, end), style)
+
+
+class Text(JupyterMixin):
+ """Text with color / style.
+
+ Args:
+ text (str, optional): Default unstyled text. Defaults to "".
+ style (Union[str, Style], optional): Base style for text. Defaults to "".
+ justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
+ overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
+ no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.
+ end (str, optional): Character to end text with. Defaults to "\\\\n".
+ tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
+ spans (List[Span], optional). A list of predefined style spans. Defaults to None.
+ """
+
+ __slots__ = [
+ "_text",
+ "style",
+ "justify",
+ "overflow",
+ "no_wrap",
+ "end",
+ "tab_size",
+ "_spans",
+ "_length",
+ ]
+
+ def __init__(
+ self,
+ text: str = "",
+ style: Union[str, Style] = "",
+ *,
+ justify: "JustifyMethod" = None,
+ overflow: "OverflowMethod" = None,
+ no_wrap: bool = None,
+ end: str = "\n",
+ tab_size: Optional[int] = 8,
+ spans: List[Span] = None,
+ ) -> None:
+ self._text = [strip_control_codes(text)]
+ self.style = style
+ self.justify = justify
+ self.overflow = overflow
+ self.no_wrap = no_wrap
+ self.end = end
+ self.tab_size = tab_size
+ self._spans: List[Span] = spans or []
+ self._length: int = len(text)
+
+ def __len__(self) -> int:
+ return self._length
+
+ def __bool__(self) -> bool:
+ return bool(self._length)
+
+ def __str__(self) -> str:
+ return self.plain
+
+ def __repr__(self) -> str:
+ return f"<text {self.plain!r} {self._spans!r}>"
+
+ def __add__(self, other: Any) -> "Text":
+ if isinstance(other, (str, Text)):
+ result = self.copy()
+ result.append(other)
+ return result
+ return NotImplemented
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Text):
+ return NotImplemented
+ return self.plain == other.plain and self._spans == other._spans
+
+ def __contains__(self, other: object) -> bool:
+ if isinstance(other, str):
+ return other in self.plain
+ elif isinstance(other, Text):
+ return other.plain in self.plain
+ return False
+
+ def __getitem__(self, slice: Union[int, slice]) -> "Text":
+ def get_text_at(offset) -> "Text":
+ _Span = Span
+ text = Text(
+ self.plain[offset],
+ spans=[
+ _Span(0, 1, style)
+ for start, end, style in self._spans
+ if end > offset >= start
+ ],
+ end="",
+ )
+ return text
+
+ if isinstance(slice, int):
+ return get_text_at(slice)
+ else:
+ start, stop, step = slice.indices(len(self.plain))
+ if step == 1:
+ lines = self.divide([start, stop])
+ return lines[1]
+ else:
+ # This would be a bit of work to implement efficiently
+ # For now, its not required
+ raise TypeError("slices with step!=1 are not supported")
+
+ @property
+ def cell_len(self) -> int:
+ """Get the number of cells required to render this text."""
+ return cell_len(self.plain)
+
+ @classmethod
+ def from_markup(
+ cls,
+ text: str,
+ *,
+ style: Union[str, Style] = "",
+ emoji: bool = True,
+ justify: "JustifyMethod" = None,
+ overflow: "OverflowMethod" = None,
+ ) -> "Text":
+ """Create Text instance from markup.
+
+ Args:
+ text (str): A string containing console markup.
+ emoji (bool, optional): Also render emoji code. Defaults to True.
+ justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
+ overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
+
+ Returns:
+ Text: A Text instance with markup rendered.
+ """
+ from .markup import render
+
+ rendered_text = render(text, style, emoji=emoji)
+ rendered_text.justify = justify
+ rendered_text.overflow = overflow
+ return rendered_text
+
+ @classmethod
+ def styled(
+ cls,
+ text: str,
+ style: StyleType = "",
+ *,
+ justify: "JustifyMethod" = None,
+ overflow: "OverflowMethod" = None,
+ ) -> "Text":
+ """Construct a Text instance with a pre-applied styled. A style applied in this way won't be used
+ to pad the text when it is justified.
+
+ Args:
+ text (str): A string containing console markup.
+ style (Union[str, Style]): Style to apply to the text. Defaults to "".
+ justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
+ overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
+
+ Returns:
+ Text: A text instance with a style applied to the entire string.
+ """
+ styled_text = cls(text, justify=justify, overflow=overflow)
+ styled_text.stylize(style)
+ return styled_text
+
+ @classmethod
+ def assemble(
+ cls,
+ *parts: Union[str, "Text", Tuple[str, StyleType]],
+ style: Union[str, Style] = "",
+ justify: "JustifyMethod" = None,
+ overflow: "OverflowMethod" = None,
+ no_wrap: bool = None,
+ end: str = "\n",
+ tab_size: int = 8,
+ ) -> "Text":
+ """Construct a text instance by combining a sequence of strings with optional styles.
+ The positional arguments should be either strings, or a tuple of string + style.
+
+ Args:
+ style (Union[str, Style], optional): Base style for text. Defaults to "".
+ justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
+ overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
+ end (str, optional): Character to end text with. Defaults to "\\\\n".
+ tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
+
+ Returns:
+ Text: A new text instance.
+ """
+ text = cls(
+ style=style,
+ justify=justify,
+ overflow=overflow,
+ no_wrap=no_wrap,
+ end=end,
+ tab_size=tab_size,
+ )
+ append = text.append
+ _Text = Text
+ for part in parts:
+ if isinstance(part, (_Text, str)):
+ append(part)
+ else:
+ append(*part)
+ return text
+
+ @property
+ def plain(self) -> str:
+ """Get the text as a single string."""
+ if len(self._text) != 1:
+ self._text[:] = ["".join(self._text)]
+ return self._text[0]
+
+ @plain.setter
+ def plain(self, new_text: str) -> None:
+ """Set the text to a new value."""
+ if new_text != self.plain:
+ self._text[:] = [new_text]
+ old_length = self._length
+ self._length = len(new_text)
+ if old_length > self._length:
+ self._trim_spans()
+
+ @property
+ def spans(self) -> List[Span]:
+ """Get a reference to the internal list of spans."""
+ return self._spans
+
+ @spans.setter
+ def spans(self, spans: List[Span]) -> None:
+ """Set spans."""
+ self._spans = spans[:]
+
+ def blank_copy(self) -> "Text":
+ """Return a new Text instance with copied meta data (but not the string or spans)."""
+ copy_self = Text(
+ style=self.style,
+ justify=self.justify,
+ overflow=self.overflow,
+ no_wrap=self.no_wrap,
+ end=self.end,
+ tab_size=self.tab_size,
+ )
+ return copy_self
+
+ def copy(self) -> "Text":
+ """Return a copy of this instance."""
+ copy_self = Text(
+ self.plain,
+ style=self.style,
+ justify=self.justify,
+ overflow=self.overflow,
+ no_wrap=self.no_wrap,
+ end=self.end,
+ tab_size=self.tab_size,
+ )
+ copy_self._spans[:] = self._spans
+ return copy_self
+
+ def stylize(
+ self, style: Union[str, Style], start: int = 0, end: Optional[int] = None
+ ) -> None:
+ """Apply a style to the text, or a portion of the text.
+
+ Args:
+ style (Union[str, Style]): Style instance or style definition to apply.
+ start (int): Start offset (negative indexing is supported). Defaults to 0.
+ end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.
+
+ """
+ length = len(self)
+ if start < 0:
+ start = length + start
+ if end is None:
+ end = length
+ if end < 0:
+ end = length + end
+ if start >= length or end <= start:
+ # Span not in text or not valid
+ return
+ self._spans.append(Span(start, min(length, end), style))
+
+ def remove_suffix(self, suffix: str) -> None:
+ """Remove a suffix if it exists.
+
+ Args:
+ suffix (str): Suffix to remove.
+ """
+ if self.plain.endswith(suffix):
+ self.right_crop(len(suffix))
+
+ def get_style_at_offset(self, console: "Console", offset: int) -> Style:
+ """Get the style of a character at give offset.
+
+ Args:
+ console (~Console): Console where text will be rendered.
+ offset (int): Offset in to text (negative indexing supported)
+
+ Returns:
+ Style: A Style instance.
+ """
+ # TODO: This is a little inefficient, it is only used by full justify
+ if offset < 0:
+ offset = len(self) + offset
+ get_style = console.get_style
+ style = get_style(self.style).copy()
+ for start, end, span_style in self._spans:
+ if end > offset >= start:
+ style += get_style(span_style, default="")
+ return style
+
+ def highlight_regex(
+ self,
+ re_highlight: str,
+ style: Union[GetStyleCallable, StyleType] = None,
+ *,
+ style_prefix: str = "",
+ ) -> int:
+ """Highlight text with a regular expression, where group names are
+ translated to styles.
+
+ Args:
+ re_highlight (str): A regular expression.
+ style (Union[GetStyleCallable, StyleType]): Optional style to apply to whole match, or a callable
+ which accepts the matched text and returns a style. Defaults to None.
+ style_prefix (str, optional): Optional prefix to add to style group names.
+
+ Returns:
+ int: Number of regex matches
+ """
+ count = 0
+ append_span = self._spans.append
+ _Span = Span
+ plain = self.plain
+ for match in re.finditer(re_highlight, plain):
+ get_span = match.span
+ if style:
+ start, end = get_span()
+ match_style = style(plain[start:end]) if callable(style) else style
+ if match_style is not None and end > start:
+ append_span(_Span(start, end, match_style))
+
+ count += 1
+ for name in match.groupdict().keys():
+ start, end = get_span(name)
+ if start != -1 and end > start:
+ append_span(_Span(start, end, f"{style_prefix}{name}"))
+ return count
+
+ def highlight_words(
+ self,
+ words: Iterable[str],
+ style: Union[str, Style],
+ *,
+ case_sensitive: bool = True,
+ ) -> int:
+ """Highlight words with a style.
+
+ Args:
+ words (Iterable[str]): Worlds to highlight.
+ style (Union[str, Style]): Style to apply.
+ case_sensitive (bool, optional): Enable case sensitive matchings. Defaults to True.
+
+ Returns:
+ int: Number of words highlighted.
+ """
+ re_words = "|".join(re.escape(word) for word in words)
+ add_span = self._spans.append
+ count = 0
+ _Span = Span
+ for match in re.finditer(
+ re_words, self.plain, flags=0 if case_sensitive else re.IGNORECASE
+ ):
+ start, end = match.span(0)
+ add_span(_Span(start, end, style))
+ count += 1
+ return count
+
+ def rstrip(self) -> None:
+ """Strip whitespace from end of text."""
+ self.plain = self.plain.rstrip()
+
+ def rstrip_end(self, size: int) -> None:
+ """Remove whitespace beyond a certain width at the end of the text.
+
+ Args:
+ size (int): The desired size of the text.
+ """
+ text_length = len(self)
+ if text_length > size:
+ excess = text_length - size
+ whitespace_match = _re_whitespace.search(self.plain)
+ if whitespace_match is not None:
+ whitespace_count = len(whitespace_match.group(0))
+ self.right_crop(min(whitespace_count, excess))
+
+ def set_length(self, new_length: int) -> None:
+ """Set new length of the text, clipping or padding is required."""
+ length = len(self)
+ if length != new_length:
+ if length < new_length:
+ self.pad_right(new_length - length)
+ else:
+ self.right_crop(length - new_length)
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> Iterable[Segment]:
+ tab_size: int = console.tab_size or self.tab_size or 8 # type: ignore
+ justify = cast(
+ "JustifyMethod", self.justify or options.justify or DEFAULT_OVERFLOW
+ )
+ overflow = cast(
+ "OverflowMethod", self.overflow or options.overflow or DEFAULT_OVERFLOW
+ )
+
+ lines = self.wrap(
+ console,
+ options.max_width,
+ justify=justify,
+ overflow=overflow,
+ tab_size=tab_size or 8,
+ no_wrap=pick_bool(self.no_wrap, options.no_wrap, False),
+ )
+ all_lines = Text("\n").join(lines)
+ yield from all_lines.render(console, end=self.end)
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
+ text = self.plain
+ if not text.strip():
+ 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(self, console: "Console", end: str = "") -> Iterable["Segment"]:
+ """Render the text as Segments.
+
+ Args:
+ console (Console): Console instance.
+ end (Optional[str], optional): Optional end character.
+
+ Returns:
+ Iterable[Segment]: Result of render that may be written to the console.
+ """
+
+ _Segment = Segment
+ text = self.plain
+ enumerated_spans = list(enumerate(self._spans, 1))
+ get_style = partial(console.get_style, default=Style.null())
+ style_map = {index: get_style(span.style) for index, span in enumerated_spans}
+ style_map[0] = get_style(self.style)
+
+ spans = [
+ (0, False, 0),
+ *((span.start, False, index) for index, span in enumerated_spans),
+ *((span.end, True, index) for index, span in enumerated_spans),
+ (len(text), True, 0),
+ ]
+ spans.sort(key=itemgetter(0, 1))
+
+ stack: List[int] = []
+ stack_append = stack.append
+ stack_pop = stack.remove
+
+ style_cache: Dict[Tuple[Style, ...], Style] = {}
+ style_cache_get = style_cache.get
+ combine = Style.combine
+
+ def get_current_style() -> Style:
+ """Construct current style from stack."""
+ styles = tuple(style_map[_style_id] for _style_id in sorted(stack))
+ cached_style = style_cache_get(styles)
+ if cached_style is not None:
+ return cached_style
+ current_style = combine(styles)
+ style_cache[styles] = current_style
+ return current_style
+
+ for (offset, leaving, style_id), (next_offset, _, _) in zip(spans, spans[1:]):
+ if leaving:
+ stack_pop(style_id)
+ else:
+ stack_append(style_id)
+ if next_offset > offset:
+ yield _Segment(text[offset:next_offset], get_current_style())
+ if end:
+ yield _Segment(end)
+
+ def join(self, lines: Iterable["Text"]) -> "Text":
+ """Join text together with this instance as the separator.
+
+ Args:
+ lines (Iterable[Text]): An iterable of Text instances to join.
+
+ Returns:
+ Text: A new text instance containing join text.
+ """
+
+ new_text = self.blank_copy()
+
+ def iter_text() -> Iterable["Text"]:
+ if self.plain:
+ for last, line in loop_last(lines):
+ yield line
+ if not last:
+ yield self
+ else:
+ yield from lines
+
+ extend_text = new_text._text.extend
+ append_span = new_text._spans.append
+ extend_spans = new_text._spans.extend
+ offset = 0
+ _Span = Span
+
+ for text in iter_text():
+ extend_text(text._text)
+ if text.style is not None:
+ append_span(_Span(offset, offset + len(text), text.style))
+ extend_spans(
+ _Span(offset + start, offset + end, style)
+ for start, end, style in text._spans
+ )
+ offset += len(text)
+ new_text._length = offset
+ return new_text
+
+ def expand_tabs(self, tab_size: int = None) -> None:
+ """Converts tabs to spaces.
+
+ Args:
+ tab_size (int, optional): Size of tabs. Defaults to 8.
+
+ """
+ if "\t" not in self.plain:
+ return
+ pos = 0
+ if tab_size is None:
+ tab_size = self.tab_size
+ assert tab_size is not None
+ result = self.blank_copy()
+ append = result.append
+
+ _style = self.style
+ for line in self.split("\n", include_separator=True):
+ parts = line.split("\t", include_separator=True)
+ for part in parts:
+ if part.plain.endswith("\t"):
+ part._text = [part.plain[:-1] + " "]
+ append(part)
+ pos += len(part)
+ spaces = tab_size - ((pos - 1) % tab_size) - 1
+ if spaces:
+ append(" " * spaces, _style)
+ pos += spaces
+ else:
+ append(part)
+ self._text = [result.plain]
+ self._length = len(self.plain)
+ self._spans[:] = result._spans
+
+ def truncate(
+ self,
+ max_width: int,
+ *,
+ overflow: Optional["OverflowMethod"] = None,
+ pad: bool = False,
+ ) -> None:
+ """Truncate text if it is longer that a given width.
+
+ Args:
+ max_width (int): Maximum number of characters in text.
+ overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None, to use self.overflow.
+ pad (bool, optional): Pad with spaces if the length is less than max_width. Defaults to False.
+ """
+ _overflow = overflow or self.overflow or DEFAULT_OVERFLOW
+ if _overflow != "ignore":
+ length = cell_len(self.plain)
+ if length > max_width:
+ if _overflow == "ellipsis":
+ self.plain = set_cell_size(self.plain, max_width - 1) + "…"
+ else:
+ self.plain = set_cell_size(self.plain, max_width)
+ if pad and length < max_width:
+ spaces = max_width - length
+ self._text = [f"{self.plain}{' ' * spaces}"]
+ self._length = len(self.plain)
+
+ def _trim_spans(self) -> None:
+ """Remove or modify any spans that are over the end of the text."""
+ max_offset = len(self.plain)
+ _Span = Span
+ self._spans[:] = [
+ (
+ span
+ if span.end < max_offset
+ else _Span(span.start, min(max_offset, span.end), span.style)
+ )
+ for span in self._spans
+ if span.start < max_offset
+ ]
+
+ def pad(self, count: int, character: str = " ") -> None:
+ """Pad left and right with a given number of characters.
+
+ Args:
+ count (int): Width of padding.
+ """
+ assert len(character) == 1, "Character must be a string of length 1"
+ if count:
+ pad_characters = character * count
+ self.plain = f"{pad_characters}{self.plain}{pad_characters}"
+ _Span = Span
+ self._spans[:] = [
+ _Span(start + count, end + count, style)
+ for start, end, style in self._spans
+ ]
+
+ def pad_left(self, count: int, character: str = " ") -> None:
+ """Pad the left with a given character.
+
+ Args:
+ count (int): Number of characters to pad.
+ character (str, optional): Character to pad with. Defaults to " ".
+ """
+ assert len(character) == 1, "Character must be a string of length 1"
+ if count:
+ self.plain = f"{character * count}{self.plain}"
+ _Span = Span
+ self._spans[:] = [
+ _Span(start + count, end + count, style)
+ for start, end, style in self._spans
+ ]
+
+ def pad_right(self, count: int, character: str = " ") -> None:
+ """Pad the right with a given character.
+
+ Args:
+ count (int): Number of characters to pad.
+ character (str, optional): Character to pad with. Defaults to " ".
+ """
+ assert len(character) == 1, "Character must be a string of length 1"
+ if count:
+ self.plain = f"{self.plain}{character * count}"
+
+ def align(self, align: AlignMethod, width: int, character: str = " ") -> None:
+ """Align text to a given width.
+
+ Args:
+ align (AlignMethod): One of "left", "center", or "right".
+ width (int): Desired width.
+ character (str, optional): Character to pad with. Defaults to " ".
+ """
+ self.truncate(width)
+ excess_space = width - cell_len(self.plain)
+ if excess_space:
+ if align == "left":
+ self.pad_right(excess_space, character)
+ elif align == "center":
+ left = excess_space // 2
+ self.pad_left(left, character)
+ self.pad_right(excess_space - left, character)
+ else:
+ self.pad_left(excess_space, character)
+
+ def append(
+ self, text: Union["Text", str], style: Union[str, "Style"] = None
+ ) -> "Text":
+ """Add text with an optional style.
+
+ Args:
+ text (Union[Text, str]): A str or Text to append.
+ style (str, optional): A style name. Defaults to None.
+
+ Returns:
+ Text: Returns self for chaining.
+ """
+
+ if not isinstance(text, (str, Text)):
+ raise TypeError("Only str or Text can be appended to Text")
+
+ if len(text):
+ if isinstance(text, str):
+ text = strip_control_codes(text)
+ self._text.append(text)
+ offset = len(self)
+ text_length = len(text)
+ if style is not None:
+ self._spans.append(Span(offset, offset + text_length, style))
+ self._length += text_length
+ elif isinstance(text, Text):
+ _Span = Span
+ if style is not None:
+ raise ValueError(
+ "style must not be set when appending Text instance"
+ )
+ text_length = self._length
+ if text.style is not None:
+ self._spans.append(
+ _Span(text_length, text_length + len(text), text.style)
+ )
+ self._text.append(text.plain)
+ self._spans.extend(
+ _Span(start + text_length, end + text_length, style)
+ for start, end, style in text._spans
+ )
+ self._length += len(text)
+ return self
+
+ def append_text(self, text: "Text") -> "Text":
+ """Append another Text instance. This method is more performant that Text.append, but
+ only works for Text.
+
+ Returns:
+ Text: Returns self for chaining.
+ """
+ _Span = Span
+ text_length = self._length
+ if text.style is not None:
+ self._spans.append(_Span(text_length, text_length + len(text), text.style))
+ self._text.append(text.plain)
+ self._spans.extend(
+ _Span(start + text_length, end + text_length, style)
+ for start, end, style in text._spans
+ )
+ self._length += len(text)
+ return self
+
+ def append_tokens(self, tokens: Iterable[Tuple[str, Optional[StyleType]]]):
+ """Append iterable of str and style. Style may be a Style instance or a str style definition.
+
+ Args:
+ pairs (Iterable[Tuple[str, Optional[StyleType]]]): An iterable of tuples containing str content and style.
+
+ Returns:
+ Text: Returns self for chaining.
+ """
+ append_text = self._text.append
+ append_span = self._spans.append
+ _Span = Span
+ offset = len(self)
+ for content, style in tokens:
+ append_text(content)
+ if style is not None:
+ append_span(_Span(offset, offset + len(content), style))
+ offset += len(content)
+ self._length = offset
+ return self
+
+ def copy_styles(self, text: "Text") -> None:
+ """Copy styles from another Text instance.
+
+ Args:
+ text (Text): A Text instance to copy styles from, must be the same length.
+ """
+ self._spans.extend(text._spans)
+
+ def split(
+ self,
+ separator="\n",
+ *,
+ include_separator: bool = False,
+ allow_blank: bool = False,
+ ) -> Lines:
+ """Split rich text in to lines, preserving styles.
+
+ Args:
+ separator (str, optional): String to split on. Defaults to "\\\\n".
+ include_separator (bool, optional): Include the separator in the lines. Defaults to False.
+ allow_blank (bool, optional): Return a blank line if the text ends with a separator. Defaults to False.
+
+ Returns:
+ List[RichText]: A list of rich text, one per line of the original.
+ """
+ assert separator, "separator must not be empty"
+
+ text = self.plain
+ if separator not in text:
+ return Lines([self.copy()])
+
+ if include_separator:
+ lines = self.divide(
+ match.end() for match in re.finditer(re.escape(separator), text)
+ )
+ else:
+
+ def flatten_spans() -> Iterable[int]:
+ for match in re.finditer(re.escape(separator), text):
+ start, end = match.span()
+ yield start
+ yield end
+
+ lines = Lines(
+ line for line in self.divide(flatten_spans()) if line.plain != separator
+ )
+
+ if not allow_blank and text.endswith(separator):
+ lines.pop()
+
+ return lines
+
+ def divide(self, offsets: Iterable[int]) -> Lines:
+ """Divide text in to a number of lines at given offsets.
+
+ Args:
+ offsets (Iterable[int]): Offsets used to divide text.
+
+ Returns:
+ Lines: New RichText instances between offsets.
+ """
+ _offsets = list(offsets)
+ if not _offsets:
+ return Lines([self.copy()])
+
+ text = self.plain
+ text_length = len(text)
+ divide_offsets = [0, *_offsets, text_length]
+ line_ranges = list(zip(divide_offsets, divide_offsets[1:]))
+
+ style = self.style
+ justify = self.justify
+ overflow = self.overflow
+ _Text = Text
+ new_lines = Lines(
+ _Text(
+ text[start:end],
+ style=style,
+ justify=justify,
+ overflow=overflow,
+ )
+ for start, end in line_ranges
+ )
+ if not self._spans:
+ return new_lines
+ order = {span: span_index for span_index, span in enumerate(self._spans)}
+ span_stack = sorted(self._spans, key=attrgetter("start"), reverse=True)
+
+ pop = span_stack.pop
+ push = span_stack.append
+ _Span = Span
+ get_order = order.__getitem__
+
+ for line, (start, end) in zip(new_lines, line_ranges):
+ if not span_stack:
+ break
+ append_span = line._spans.append
+ position = len(span_stack) - 1
+ while span_stack[position].start < end:
+ span = pop(position)
+ add_span, remaining_span = span.split(end)
+ if remaining_span:
+ push(remaining_span)
+ order[remaining_span] = order[span]
+ span_start, span_end, span_style = add_span
+ line_span = _Span(span_start - start, span_end - start, span_style)
+ order[line_span] = order[span]
+ append_span(line_span)
+ position -= 1
+ if position < 0 or not span_stack:
+ break # pragma: no cover
+ line._spans.sort(key=get_order)
+
+ return new_lines
+
+ def right_crop(self, amount: int = 1) -> None:
+ """Remove a number of characters from the end of the text."""
+ max_offset = len(self.plain) - amount
+ _Span = Span
+ self._spans[:] = [
+ (
+ span
+ if span.end < max_offset
+ else _Span(span.start, min(max_offset, span.end), span.style)
+ )
+ for span in self._spans
+ if span.start < max_offset
+ ]
+ self._text = [self.plain[:-amount]]
+ self._length -= amount
+
+ def wrap(
+ self,
+ console: "Console",
+ width: int,
+ *,
+ justify: "JustifyMethod" = None,
+ overflow: "OverflowMethod" = None,
+ tab_size: int = 8,
+ no_wrap: bool = None,
+ ) -> Lines:
+ """Word wrap the text.
+
+ Args:
+ console (Console): Console instance.
+ width (int): Number of characters per line.
+ emoji (bool, optional): Also render emoji code. Defaults to True.
+ justify (str, optional): Justify method: "default", "left", "center", "full", "right". Defaults to "default".
+ overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None.
+ tab_size (int, optional): Default tab size. Defaults to 8.
+ no_wrap (bool, optional): Disable wrapping, Defaults to False.
+
+ Returns:
+ Lines: Number of lines.
+ """
+ wrap_justify = cast("JustifyMethod", justify or self.justify or DEFAULT_JUSTIFY)
+ wrap_overflow = cast(
+ "OverflowMethod", overflow or self.overflow or DEFAULT_OVERFLOW
+ )
+ no_wrap = pick_bool(no_wrap, self.no_wrap, False) or overflow == "ignore"
+
+ lines = Lines()
+ for line in self.split(allow_blank=True):
+ if "\t" in line:
+ line.expand_tabs(tab_size)
+ if no_wrap:
+ new_lines = Lines([line])
+ else:
+ offsets = divide_line(str(line), width, fold=wrap_overflow == "fold")
+ new_lines = line.divide(offsets)
+ for line in new_lines:
+ line.rstrip_end(width)
+ if wrap_justify:
+ new_lines.justify(
+ console, width, justify=wrap_justify, overflow=wrap_overflow
+ )
+ for line in new_lines:
+ line.truncate(width, overflow=wrap_overflow)
+ lines.extend(new_lines)
+ return lines
+
+ def fit(self, width: int) -> Lines:
+ """Fit the text in to given width by chopping in to lines.
+
+ Args:
+ width (int): Maximum characters in a line.
+
+ Returns:
+ Lines: List of lines.
+ """
+ lines: Lines = Lines()
+ append = lines.append
+ for line in self.split():
+ line.set_length(width)
+ append(line)
+ return lines
+
+ def detect_indentation(self) -> int:
+ """Auto-detect indentation of code.
+
+ Returns:
+ int: Number of spaces used to indent code.
+ """
+
+ _indentations = {
+ len(match.group(1))
+ for match in re.finditer(r"^( *)(.*)$", self.plain, flags=re.MULTILINE)
+ }
+
+ try:
+ indentation = (
+ reduce(gcd, [indent for indent in _indentations if not indent % 2]) or 1
+ )
+ except TypeError:
+ indentation = 1
+
+ return indentation
+
+ def with_indent_guides(
+ self,
+ indent_size: int = None,
+ *,
+ character: str = "│",
+ style: StyleType = "dim green",
+ ) -> "Text":
+ """Adds indent guide lines to text.
+
+ Args:
+ indent_size (Optional[int]): Size of indentation, or None to auto detect. Defaults to None.
+ character (str, optional): Character to use for indentation. Defaults to "│".
+ style (Union[Style, str], optional): Style of indent guides.
+
+ Returns:
+ Text: New text with indentation guides.
+ """
+
+ _indent_size = self.detect_indentation() if indent_size is None else indent_size
+
+ text = self.copy()
+ text.expand_tabs()
+ indent_line = f"{character}{' ' * (_indent_size - 1)}"
+
+ re_indent = re.compile(r"^( *)(.*)$")
+ new_lines: List[Text] = []
+ add_line = new_lines.append
+ blank_lines = 0
+ for line in text.split():
+ match = re_indent.match(line.plain)
+ if not match or not match.group(2):
+ blank_lines += 1
+ continue
+ indent = match.group(1)
+ full_indents, remaining_space = divmod(len(indent), _indent_size)
+ new_indent = f"{indent_line * full_indents}{' ' * remaining_space}"
+ line.plain = new_indent + line.plain[len(new_indent) :]
+ line.stylize(style, 0, len(new_indent))
+ if blank_lines:
+ new_lines.extend([Text(new_indent, style=style)] * blank_lines)
+ blank_lines = 0
+ add_line(line)
+ if blank_lines:
+ new_lines.extend([Text("", style=style)] * blank_lines)
+
+ new_text = Text("\n").join(new_lines)
+ return new_text
+
+
+if __name__ == "__main__": # pragma: no cover
+ from rich import print
+
+ text = Text("<span>\n\tHello\n</span>")
+ text.expand_tabs(4)
+ print(text)
+
+ code = """
+def __add__(self, other: Any) -> "Text":
+ if isinstance(other, (str, Text)):
+ result = self.copy()
+ result.append(other)
+ return result
+ return NotImplemented
+"""
+ text = Text(code)
+ text = text.with_indent_guides()
+ print(text)
diff --git a/rich/theme.py b/rich/theme.py
new file mode 100644
index 0000000..7f161d9
--- /dev/null
+++ b/rich/theme.py
@@ -0,0 +1,110 @@
+import configparser
+from typing import Dict, List, IO, Mapping, Optional
+
+from .default_styles import DEFAULT_STYLES
+from .style import Style, StyleType
+
+
+class Theme:
+ """A container for style information, used by :class:`~rich.console.Console`.
+
+ Args:
+ styles (Dict[str, Style], optional): A mapping of style names on to styles. Defaults to None for a theme with no styles.
+ inherit (bool, optional): Inherit default styles. Defaults to True.
+ """
+
+ styles: Dict[str, Style]
+
+ def __init__(self, styles: Mapping[str, StyleType] = None, inherit: bool = True):
+ self.styles = DEFAULT_STYLES.copy() if inherit else {}
+ if styles is not None:
+ self.styles.update(
+ {
+ name: style if isinstance(style, Style) else Style.parse(style)
+ for name, style in styles.items()
+ }
+ )
+
+ @property
+ def config(self) -> str:
+ """Get contents of a config file for this theme."""
+ config = "[styles]\n" + "\n".join(
+ f"{name} = {style}" for name, style in sorted(self.styles.items())
+ )
+ return config
+
+ @classmethod
+ def from_file(
+ cls, config_file: IO[str], source: str = None, inherit: bool = True
+ ) -> "Theme":
+ """Load a theme from a text mode file.
+
+ Args:
+ config_file (IO[str]): An open conf file.
+ source (str, optional): The filename of the open file. Defaults to None.
+ inherit (bool, optional): Inherit default styles. Defaults to True.
+
+ Returns:
+ Theme: A New theme instance.
+ """
+ config = configparser.ConfigParser()
+ config.read_file(config_file, source=source)
+ styles = {name: Style.parse(value) for name, value in config.items("styles")}
+ theme = Theme(styles, inherit=inherit)
+ return theme
+
+ @classmethod
+ def read(cls, path: str, inherit: bool = True) -> "Theme":
+ """Read a theme from a path.
+
+ Args:
+ path (str): Path to a config file readable by Python configparser module.
+ inherit (bool, optional): Inherit default styles. Defaults to True.
+
+ Returns:
+ Theme: A new theme instance.
+ """
+ with open(path, "rt") as config_file:
+ return cls.from_file(config_file, source=path, inherit=inherit)
+
+
+class ThemeStackError(Exception):
+ """Base exception for errors related to the theme stack."""
+
+
+class ThemeStack:
+ """A stack of themes.
+
+ Args:
+ theme (Theme): A theme instance
+ """
+
+ def __init__(self, theme: Theme) -> None:
+ self._entries: List[Dict[str, Style]] = [theme.styles]
+ self.get = self._entries[-1].get
+
+ def push_theme(self, theme: Theme, inherit: bool = True) -> None:
+ """Push a theme on the top of the stack.
+
+ Args:
+ theme (Theme): A Theme instance.
+ inherit (boolean, optional): Inherit styles from current top of stack.
+ """
+ styles: Dict[str, Style]
+ styles = (
+ {**self._entries[-1], **theme.styles} if inherit else theme.styles.copy()
+ )
+ self._entries.append(styles)
+ self.get = self._entries[-1].get
+
+ def pop_theme(self) -> None:
+ """Pop (and discard) the top-most theme."""
+ if len(self._entries) == 1:
+ raise ThemeStackError("Unable to pop base theme")
+ self._entries.pop()
+ self.get = self._entries[-1].get
+
+
+if __name__ == "__main__": # pragma: no cover
+ theme = Theme()
+ print(theme.config)
diff --git a/rich/themes.py b/rich/themes.py
new file mode 100644
index 0000000..bf6db10
--- /dev/null
+++ b/rich/themes.py
@@ -0,0 +1,5 @@
+from .default_styles import DEFAULT_STYLES
+from .theme import Theme
+
+
+DEFAULT = Theme(DEFAULT_STYLES)
diff --git a/rich/traceback.py b/rich/traceback.py
new file mode 100644
index 0000000..e97cc52
--- /dev/null
+++ b/rich/traceback.py
@@ -0,0 +1,621 @@
+from __future__ import absolute_import
+
+import os
+import platform
+import sys
+from dataclasses import dataclass, field
+from traceback import walk_tb
+from types import TracebackType
+from typing import Any, Callable, Dict, Iterable, List, Optional, Type
+
+from pygments.lexers import guess_lexer_for_filename
+from pygments.token import Comment, Keyword, Name, Number, Operator, String
+from pygments.token import Text as TextToken
+from pygments.token import Token
+
+from . import pretty
+from ._loop import loop_first, loop_last
+from .columns import Columns
+from .console import (
+ Console,
+ ConsoleOptions,
+ ConsoleRenderable,
+ RenderResult,
+ render_group,
+)
+from .constrain import Constrain
+from .highlighter import RegexHighlighter, ReprHighlighter
+from .panel import Panel
+from .scope import render_scope
+from .style import Style
+from .syntax import Syntax
+from .text import Text
+from .theme import Theme
+
+WINDOWS = platform.system() == "Windows"
+
+LOCALS_MAX_LENGTH = 10
+LOCALS_MAX_STRING = 80
+
+
+def install(
+ *,
+ console: Console = None,
+ width: Optional[int] = 100,
+ extra_lines: int = 3,
+ theme: Optional[str] = None,
+ word_wrap: bool = False,
+ show_locals: bool = False,
+ indent_guides: bool = True,
+) -> Callable:
+ """Install a rich traceback handler.
+
+ Once installed, any tracebacks will be printed with syntax highlighting and rich formatting.
+
+
+ Args:
+ console (Optional[Console], optional): Console to write exception to. Default uses internal Console instance.
+ width (Optional[int], optional): Width (in characters) of traceback. Defaults to 100.
+ extra_lines (int, optional): Extra lines of code. Defaults to 3.
+ theme (Optional[str], optional): Pygments theme to use in traceback. Defaults to ``None`` which will pick
+ a theme appropriate for the platform.
+ word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
+ show_locals (bool, optional): Enable display of local variables. Defaults to False.
+ indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
+
+ Returns:
+ Callable: The previous exception handler that was replaced.
+
+ """
+ traceback_console = Console(file=sys.stderr) if console is None else console
+
+ def excepthook(
+ type_: Type[BaseException],
+ value: BaseException,
+ traceback: Optional[TracebackType],
+ ) -> None:
+ traceback_console.print(
+ Traceback.from_exception(
+ type_,
+ value,
+ traceback,
+ width=width,
+ extra_lines=extra_lines,
+ theme=theme,
+ word_wrap=word_wrap,
+ show_locals=show_locals,
+ indent_guides=indent_guides,
+ )
+ )
+
+ def ipy_excepthook_closure(ip) -> None: # pragma: no cover
+ tb_data = {} # store information about showtraceback call
+ default_showtraceback = ip.showtraceback # keep reference of default traceback
+
+ def ipy_show_traceback(*args, **kwargs) -> None:
+ """wrap the default ip.showtraceback to store info for ip._showtraceback"""
+ nonlocal tb_data
+ tb_data = kwargs
+ default_showtraceback(*args, **kwargs)
+
+ def ipy_display_traceback(*args, is_syntax: bool = False, **kwargs) -> None:
+ """Internally called traceback from ip._showtraceback"""
+ nonlocal tb_data
+ exc_tuple = ip._get_exc_info()
+
+ # do not display trace on syntax error
+ tb: Optional[TracebackType] = None if is_syntax else exc_tuple[2]
+
+ # determine correct tb_offset
+ compiled = tb_data.get("running_compiled_code", False)
+ tb_offset = tb_data.get("tb_offset", 1 if compiled else 0)
+ # remove ipython internal frames from trace with tb_offset
+ for _ in range(tb_offset):
+ if tb is None:
+ break
+ tb = tb.tb_next
+
+ excepthook(exc_tuple[0], exc_tuple[1], tb)
+ tb_data = {} # clear data upon usage
+
+ # replace _showtraceback instead of showtraceback to allow ipython features such as debugging to work
+ # this is also what the ipython docs recommends to modify when subclassing InteractiveShell
+ ip._showtraceback = ipy_display_traceback
+ # add wrapper to capture tb_data
+ ip.showtraceback = ipy_show_traceback
+ ip.showsyntaxerror = lambda *args, **kwargs: ipy_display_traceback(
+ *args, is_syntax=True, **kwargs
+ )
+
+ try: # pragma: no cover
+ # if wihin ipython, use customized traceback
+ ip = get_ipython() # type: ignore
+ ipy_excepthook_closure(ip)
+ return sys.excepthook
+ except Exception:
+ # otherwise use default system hook
+ old_excepthook = sys.excepthook
+ sys.excepthook = excepthook
+ return old_excepthook
+
+
+@dataclass
+class Frame:
+ filename: str
+ lineno: int
+ name: str
+ line: str = ""
+ locals: Optional[Dict[str, pretty.Node]] = None
+
+
+@dataclass
+class _SyntaxError:
+ offset: int
+ filename: str
+ line: str
+ lineno: int
+ msg: str
+
+
+@dataclass
+class Stack:
+ exc_type: str
+ exc_value: str
+ syntax_error: Optional[_SyntaxError] = None
+ is_cause: bool = False
+ frames: List[Frame] = field(default_factory=list)
+
+
+@dataclass
+class Trace:
+ stacks: List[Stack]
+
+
+class PathHighlighter(RegexHighlighter):
+ highlights = [r"(?P<dim>.*/)(?P<bold>.+)"]
+
+
+class Traceback:
+ """A Console renderable that renders a traceback.
+
+ Args:
+ trace (Trace, optional): A `Trace` object produced from `extract`. Defaults to None, which uses
+ the last exception.
+ width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.
+ extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
+ theme (str, optional): Override pygments theme used in traceback.
+ word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
+ show_locals (bool, optional): Enable display of local variables. Defaults to False.
+ indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
+ locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to 10.
+ locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
+ """
+
+ LEXERS = {
+ "": "text",
+ ".py": "python",
+ ".pxd": "cython",
+ ".pyx": "cython",
+ ".pxi": "pyrex",
+ }
+
+ def __init__(
+ self,
+ trace: Trace = None,
+ width: Optional[int] = 100,
+ extra_lines: int = 3,
+ theme: Optional[str] = None,
+ word_wrap: bool = False,
+ show_locals: bool = False,
+ indent_guides: bool = True,
+ locals_max_length: int = LOCALS_MAX_LENGTH,
+ locals_max_string: int = LOCALS_MAX_STRING,
+ ):
+ if trace is None:
+ exc_type, exc_value, traceback = sys.exc_info()
+ if exc_type is None or exc_value is None or traceback is None:
+ raise ValueError(
+ "Value for 'trace' required if not called in except: block"
+ )
+ trace = self.extract(
+ exc_type, exc_value, traceback, show_locals=show_locals
+ )
+ self.trace = trace
+ self.width = width
+ self.extra_lines = extra_lines
+ self.theme = Syntax.get_theme(theme or "ansi_dark")
+ self.word_wrap = word_wrap
+ self.show_locals = show_locals
+ self.indent_guides = indent_guides
+ self.locals_max_length = locals_max_length
+ self.locals_max_string = locals_max_string
+
+ @classmethod
+ def from_exception(
+ cls,
+ exc_type: Type,
+ exc_value: BaseException,
+ traceback: Optional[TracebackType],
+ width: Optional[int] = 100,
+ extra_lines: int = 3,
+ theme: Optional[str] = None,
+ word_wrap: bool = False,
+ show_locals: bool = False,
+ indent_guides: bool = True,
+ locals_max_length: int = LOCALS_MAX_LENGTH,
+ locals_max_string: int = LOCALS_MAX_STRING,
+ ) -> "Traceback":
+ """Create a traceback from exception info
+
+ Args:
+ exc_type (Type[BaseException]): Exception type.
+ exc_value (BaseException): Exception value.
+ traceback (TracebackType): Python Traceback object.
+ width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.
+ extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
+ theme (str, optional): Override pygments theme used in traceback.
+ word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
+ show_locals (bool, optional): Enable display of local variables. Defaults to False.
+ indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
+ locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to 10.
+ locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
+
+ Returns:
+ Traceback: A Traceback instance that may be printed.
+ """
+ rich_traceback = cls.extract(
+ exc_type, exc_value, traceback, show_locals=show_locals
+ )
+ return cls(
+ rich_traceback,
+ width=width,
+ extra_lines=extra_lines,
+ theme=theme,
+ word_wrap=word_wrap,
+ show_locals=show_locals,
+ indent_guides=indent_guides,
+ locals_max_length=locals_max_length,
+ locals_max_string=locals_max_string,
+ )
+
+ @classmethod
+ def extract(
+ cls,
+ exc_type: Type[BaseException],
+ exc_value: BaseException,
+ traceback: Optional[TracebackType],
+ show_locals: bool = False,
+ locals_max_length: int = LOCALS_MAX_LENGTH,
+ locals_max_string: int = LOCALS_MAX_STRING,
+ ) -> Trace:
+ """Extract traceback information.
+
+ Args:
+ exc_type (Type[BaseException]): Exception type.
+ exc_value (BaseException): Exception value.
+ traceback (TracebackType): Python Traceback object.
+ show_locals (bool, optional): Enable display of local variables. Defaults to False.
+ locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to 10.
+ locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
+
+ Returns:
+ Trace: A Trace instance which you can use to construct a `Traceback`.
+ """
+
+ stacks: List[Stack] = []
+ is_cause = False
+
+ from rich import _IMPORT_CWD
+
+ def safe_str(_object: Any) -> str:
+ """Don't allow exceptions from __str__ to propegate."""
+ try:
+ return str(_object)
+ except Exception:
+ return "<exception str() failed>"
+
+ while True:
+ stack = Stack(
+ exc_type=safe_str(exc_type.__name__),
+ exc_value=safe_str(exc_value),
+ is_cause=is_cause,
+ )
+
+ if isinstance(exc_value, SyntaxError):
+ stack.syntax_error = _SyntaxError(
+ offset=exc_value.offset or 0,
+ filename=exc_value.filename or "?",
+ lineno=exc_value.lineno or 0,
+ line=exc_value.text or "",
+ msg=exc_value.msg,
+ )
+
+ stacks.append(stack)
+ append = stack.frames.append
+
+ for frame_summary, line_no in walk_tb(traceback):
+ filename = frame_summary.f_code.co_filename
+ if filename and not filename.startswith("<"):
+ if not os.path.isabs(filename):
+ filename = os.path.join(_IMPORT_CWD, filename)
+ frame = Frame(
+ filename=filename or "?",
+ lineno=line_no,
+ name=frame_summary.f_code.co_name,
+ locals={
+ key: pretty.traverse(
+ value,
+ max_length=locals_max_length,
+ max_string=locals_max_string,
+ )
+ for key, value in frame_summary.f_locals.items()
+ }
+ if show_locals
+ else None,
+ )
+ append(frame)
+
+ cause = getattr(exc_value, "__cause__", None)
+ if cause and cause.__traceback__:
+ exc_type = cause.__class__
+ exc_value = cause
+ traceback = cause.__traceback__
+ if traceback:
+ is_cause = True
+ continue
+
+ cause = exc_value.__context__
+ if (
+ cause
+ and cause.__traceback__
+ and not getattr(exc_value, "__suppress_context__", False)
+ ):
+ exc_type = cause.__class__
+ exc_value = cause
+ traceback = cause.__traceback__
+ if traceback:
+ is_cause = False
+ continue
+ # No cover, code is reached but coverage doesn't recognize it.
+ break # pragma: no cover
+
+ trace = Trace(stacks=stacks)
+ return trace
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ theme = self.theme
+ background_style = theme.get_background_style()
+ token_style = theme.get_style_for_token
+
+ traceback_theme = Theme(
+ {
+ "pretty": token_style(TextToken),
+ "pygments.text": token_style(Token),
+ "pygments.string": token_style(String),
+ "pygments.function": token_style(Name.Function),
+ "pygments.number": token_style(Number),
+ "repr.indent": token_style(Comment) + Style(dim=True),
+ "repr.str": token_style(String),
+ "repr.brace": token_style(TextToken) + Style(bold=True),
+ "repr.number": token_style(Number),
+ "repr.bool_true": token_style(Keyword.Constant),
+ "repr.bool_false": token_style(Keyword.Constant),
+ "repr.none": token_style(Keyword.Constant),
+ "scope.border": token_style(String.Delimiter),
+ "scope.equals": token_style(Operator),
+ "scope.key": token_style(Name),
+ "scope.key.special": token_style(Name.Constant) + Style(dim=True),
+ }
+ )
+
+ highlighter = ReprHighlighter()
+ for last, stack in loop_last(reversed(self.trace.stacks)):
+ if stack.frames:
+ stack_renderable: ConsoleRenderable = Panel(
+ self._render_stack(stack),
+ title="[traceback.title]Traceback [dim](most recent call last)",
+ style=background_style,
+ border_style="traceback.border.syntax_error",
+ expand=True,
+ padding=(0, 1),
+ )
+ stack_renderable = Constrain(stack_renderable, self.width)
+ with console.use_theme(traceback_theme):
+ yield stack_renderable
+ if stack.syntax_error is not None:
+ with console.use_theme(traceback_theme):
+ yield Constrain(
+ Panel(
+ self._render_syntax_error(stack.syntax_error),
+ style=background_style,
+ border_style="traceback.border",
+ expand=True,
+ padding=(0, 1),
+ width=self.width,
+ ),
+ self.width,
+ )
+ yield Text.assemble(
+ (f"{stack.exc_type}: ", "traceback.exc_type"),
+ highlighter(stack.syntax_error.msg),
+ )
+ else:
+ yield Text.assemble(
+ (f"{stack.exc_type}: ", "traceback.exc_type"),
+ highlighter(stack.exc_value),
+ )
+
+ if not last:
+ if stack.is_cause:
+ yield Text.from_markup(
+ "\n[i]The above exception was the direct cause of the following exception:\n",
+ )
+ else:
+ yield Text.from_markup(
+ "\n[i]During handling of the above exception, another exception occurred:\n",
+ )
+
+ @render_group()
+ def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult:
+ highlighter = ReprHighlighter()
+ path_highlighter = PathHighlighter()
+ if syntax_error.filename != "<stdin>":
+ text = Text.assemble(
+ (f" {syntax_error.filename}", "pygments.string"),
+ (":", "pygments.text"),
+ (str(syntax_error.lineno), "pygments.number"),
+ style="pygments.text",
+ )
+ yield path_highlighter(text)
+ syntax_error_text = highlighter(syntax_error.line.rstrip())
+ syntax_error_text.no_wrap = True
+ offset = min(syntax_error.offset - 1, len(syntax_error_text))
+ syntax_error_text.stylize("bold underline", offset, offset + 1)
+ syntax_error_text += Text.from_markup(
+ "\n" + " " * offset + "[traceback.offset]▲[/]",
+ style="pygments.text",
+ )
+ yield syntax_error_text
+
+ @classmethod
+ def _guess_lexer(cls, filename: str, code: str) -> str:
+ ext = os.path.splitext(filename)[-1]
+ if not ext:
+ # No extension, look at first line to see if it is a hashbang
+ # Note, this is an educated guess and not a guarantee
+ # If it fails, the only downside is that the code is highlighted strangely
+ new_line_index = code.index("\n")
+ first_line = code[:new_line_index] if new_line_index != -1 else code
+ if first_line.startswith("#!") and "python" in first_line.lower():
+ return "python"
+ lexer_name = (
+ cls.LEXERS.get(ext) or guess_lexer_for_filename(filename, code).name
+ )
+ return lexer_name
+
+ @render_group()
+ def _render_stack(self, stack: Stack) -> RenderResult:
+ path_highlighter = PathHighlighter()
+ theme = self.theme
+ code_cache: Dict[str, str] = {}
+
+ def read_code(filename: str) -> str:
+ """Read files, and cache results on filename.
+
+ Args:
+ filename (str): Filename to read
+
+ Returns:
+ str: Contents of file
+ """
+ code = code_cache.get(filename)
+ if code is None:
+ with open(
+ filename, "rt", encoding="utf-8", errors="replace"
+ ) as code_file:
+ code = code_file.read()
+ code_cache[filename] = code
+ return code
+
+ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
+ if frame.locals:
+ yield render_scope(
+ frame.locals,
+ title="locals",
+ indent_guides=self.indent_guides,
+ max_length=self.locals_max_length,
+ max_string=self.locals_max_string,
+ )
+
+ for first, frame in loop_first(stack.frames):
+ text = Text.assemble(
+ path_highlighter(Text(frame.filename, style="pygments.string")),
+ (":", "pygments.text"),
+ (str(frame.lineno), "pygments.number"),
+ " in ",
+ (frame.name, "pygments.function"),
+ style="pygments.text",
+ )
+ if not frame.filename.startswith("<") and not first:
+ yield ""
+ yield text
+ if frame.filename.startswith("<"):
+ yield from render_locals(frame)
+ continue
+ try:
+ code = read_code(frame.filename)
+ lexer_name = self._guess_lexer(frame.filename, code)
+ syntax = Syntax(
+ code,
+ lexer_name,
+ theme=theme,
+ line_numbers=True,
+ line_range=(
+ frame.lineno - self.extra_lines,
+ frame.lineno + self.extra_lines,
+ ),
+ highlight_lines={frame.lineno},
+ word_wrap=self.word_wrap,
+ code_width=88,
+ indent_guides=self.indent_guides,
+ dedent=False,
+ )
+ yield ""
+ except Exception as error:
+ yield Text.assemble(
+ (f"\n{error}", "traceback.error"),
+ )
+ else:
+ yield (
+ Columns(
+ [
+ syntax,
+ *render_locals(frame),
+ ],
+ padding=1,
+ )
+ if frame.locals
+ else syntax
+ )
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from .console import Console
+
+ console = Console()
+ import sys
+
+ def bar(a): # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑
+ one = 1
+ print(one / a)
+
+ def foo(a):
+
+ zed = {
+ "characters": {
+ "Paul Atriedies",
+ "Vladimir Harkonnen",
+ "Thufir Haway",
+ "Duncan Idaho",
+ },
+ "atomic_types": (None, False, True),
+ }
+ bar(a)
+
+ def error():
+
+ try:
+ try:
+ foo(0)
+ except:
+ slfkjsldkfj # type: ignore
+ except:
+ console.print_exception(show_locals=True)
+
+ error()
diff --git a/rich/tree.py b/rich/tree.py
new file mode 100644
index 0000000..15a3a93
--- /dev/null
+++ b/rich/tree.py
@@ -0,0 +1,240 @@
+from typing import Iterator, List, Tuple
+
+from ._loop import loop_first, loop_last
+from .console import Console, ConsoleOptions, RenderableType, RenderResult
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .segment import Segment
+from .style import Style, StyleStack, StyleType
+from .styled import Styled
+
+
+class Tree(JupyterMixin):
+ """A renderable for a tree structure.
+
+ Args:
+ label (RenderableType): The renderable or str for the tree label.
+ style (StyleType, optional): Style of this tree. Defaults to "tree".
+ guide_style (StyleType, optional): Style of the guide lines. Defaults to "tree.line".
+ expanded (bool, optional): Also display children. Defaults to True.
+ highlight (bool, optional): Highlight renderable (if str). Defaults to False.
+ """
+
+ def __init__(
+ self,
+ label: RenderableType,
+ *,
+ style: StyleType = "tree",
+ guide_style: StyleType = "tree.line",
+ expanded=True,
+ highlight=False,
+ ) -> None:
+ self.label = label
+ self.style = style
+ self.guide_style = guide_style
+ self.children: List[Tree] = []
+ self.expanded = expanded
+ self.highlight = highlight
+
+ def add(
+ self,
+ label: RenderableType,
+ *,
+ style: StyleType = None,
+ guide_style: StyleType = None,
+ expanded=True,
+ highlight=False,
+ ) -> "Tree":
+ """Add a child tree.
+
+ Args:
+ label (RenderableType): The renderable or str for the tree label.
+ style (StyleType, optional): Style of this tree. Defaults to "tree".
+ guide_style (StyleType, optional): Style of the guide lines. Defaults to "tree.line".
+ expanded (bool, optional): Also display children. Defaults to True.
+ highlight (Optional[bool], optional): Highlight renderable (if str). Defaults to False.
+
+ Returns:
+ Tree: A new child Tree, which may be further modified.
+ """
+ node = Tree(
+ label,
+ style=self.style if style is None else style,
+ guide_style=self.guide_style if guide_style is None else guide_style,
+ expanded=expanded,
+ highlight=self.highlight if highlight is None else highlight,
+ )
+ self.children.append(node)
+ return node
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+
+ stack: List[Iterator[Tuple[bool, Tree]]] = []
+ pop = stack.pop
+ push = stack.append
+ new_line = Segment.line()
+
+ get_style = console.get_style
+ null_style = Style.null()
+ guide_style = get_style(self.guide_style) or null_style
+ SPACE, CONTINUE, FORK, END = range(4)
+
+ ASCII_GUIDES = (" ", "| ", "+-- ", "`-- ")
+ TREE_GUIDES = [
+ (" ", "│ ", "├── ", "└── "),
+ (" ", "┃ ", "┣━━ ", "┗━━ "),
+ (" ", "║ ", "╠══ ", "╚══ "),
+ ]
+ _Segment = Segment
+
+ def make_guide(index: int, style: Style) -> Segment:
+ """Make a Segment for a level of the guide lines."""
+ if options.ascii_only:
+ line = ASCII_GUIDES[index]
+ else:
+ guide = 1 if style.bold else (2 if style.underline2 else 0)
+ line = TREE_GUIDES[0 if options.legacy_windows else guide][index]
+ return _Segment(line, style)
+
+ levels: List[Segment] = [make_guide(CONTINUE, guide_style)]
+ push(iter(loop_last([self])))
+
+ guide_style_stack = StyleStack(get_style(self.guide_style))
+ style_stack = StyleStack(get_style(self.style))
+ remove_guide_styles = Style(bold=False, underline2=False)
+
+ while stack:
+ stack_node = pop()
+ try:
+ last, node = next(stack_node)
+ except StopIteration:
+ levels.pop()
+ if levels:
+ guide_style = levels[-1].style or null_style
+ levels[-1] = make_guide(FORK, guide_style)
+ guide_style_stack.pop()
+ style_stack.pop()
+ continue
+ push(stack_node)
+ if last:
+ levels[-1] = make_guide(END, levels[-1].style or null_style)
+
+ guide_style = guide_style_stack.current + get_style(node.guide_style)
+ style = style_stack.current + get_style(node.style)
+ prefix = levels[1:]
+ renderable_lines = console.render_lines(
+ Styled(node.label, style),
+ options.update(
+ width=options.max_width
+ - sum(level.cell_length for level in prefix),
+ highlight=self.highlight,
+ height=None,
+ ),
+ )
+ for first, line in loop_first(renderable_lines):
+ if prefix:
+ yield from _Segment.apply_style(
+ prefix,
+ style.background_style,
+ post_style=remove_guide_styles,
+ )
+ yield from line
+ yield new_line
+ if first and prefix:
+ prefix[-1] = make_guide(
+ SPACE if last else CONTINUE, prefix[-1].style or null_style
+ )
+
+ if node.expanded and node.children:
+ levels[-1] = make_guide(
+ SPACE if last else CONTINUE, levels[-1].style or null_style
+ )
+ levels.append(
+ make_guide(END if len(node.children) == 1 else FORK, guide_style)
+ )
+ style_stack.push(get_style(node.style))
+ guide_style_stack.push(get_style(node.guide_style))
+ push(iter(loop_last(node.children)))
+
+ def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
+ stack: List[Iterator[Tree]] = [iter([self])]
+ pop = stack.pop
+ push = stack.append
+ minimum = 0
+ maximum = 0
+ measure = Measurement.get
+ level = 0
+ while stack:
+ iter_tree = pop()
+ try:
+ tree = next(iter_tree)
+ except StopIteration:
+ level -= 1
+ continue
+ push(iter_tree)
+ min_measure, max_measure = measure(console, tree.label, max_width)
+ indent = level * 4
+ minimum = max(min_measure + indent, minimum)
+ maximum = max(max_measure + indent, maximum)
+ if tree.expanded and tree.children:
+ push(iter(tree.children))
+ level += 1
+ return Measurement(minimum, maximum)
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from rich.console import RenderGroup
+ from rich.markdown import Markdown
+ from rich.panel import Panel
+ from rich.syntax import Syntax
+ from rich.table import Table
+
+ table = Table(row_styles=["", "dim"])
+
+ table.add_column("Released", style="cyan", no_wrap=True)
+ table.add_column("Title", style="magenta")
+ table.add_column("Box Office", justify="right", style="green")
+
+ table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690")
+ table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
+ table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889")
+ table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889")
+
+ code = """\
+class Segment(NamedTuple):
+ text: str = ""
+ style: Optional[Style] = None
+ is_control: bool = False
+"""
+ syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
+
+ markdown = Markdown(
+ """\
+### example.md
+> Hello, World!
+>
+> Markdown _all_ the things
+"""
+ )
+
+ root = Tree("🌲 [b green]Rich Tree", highlight=True)
+
+ node = root.add(":file_folder: Renderables", guide_style="red")
+ simple_node = node.add(":file_folder: [bold yellow]Atomic", guide_style="uu green")
+ simple_node.add(RenderGroup("📄 Syntax", syntax))
+ simple_node.add(RenderGroup("📄 Markdown", Panel(markdown, border_style="green")))
+
+ containers_node = node.add(
+ ":file_folder: [bold magenta]Containers", guide_style="bold magenta"
+ )
+ containers_node.expanded = True
+ panel = Panel.fit("Just a panel", border_style="red")
+ containers_node.add(RenderGroup("📄 Panels", panel))
+
+ containers_node.add(RenderGroup("📄 [b magenta]Table", table))
+
+ console = Console()
+ console.print(root)