From 2364e97deb0f2b4da133f92ed904196348301413 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 21 Mar 2022 10:03:08 +0000 Subject: [PATCH 01/29] Add skeleton export_svg and save_svg methods to Console --- rich/console.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rich/console.py b/rich/console.py index 6a0b3e902..a6dbe514c 100644 --- a/rich/console.py +++ b/rich/console.py @@ -282,11 +282,9 @@ def __rich_console__( # A type that may be rendered by Console. RenderableType = Union[ConsoleRenderable, RichCast, str] - # The result of calling a __rich_console__ method. RenderResult = Iterable[Union[RenderableType, Segment]] - _null_highlighter = NullHighlighter() @@ -542,7 +540,6 @@ def _is_jupyter() -> bool: # pragma: no cover "windows": ColorSystem.WINDOWS, } - _COLOR_SYSTEMS_NAMES = {system: name for name, system in COLOR_SYSTEMS.items()} @@ -2190,6 +2187,12 @@ def save_html( with open(path, "wt", encoding="utf-8") as write_file: write_file.write(html) + def export_svg(self) -> str: + pass + + def save_svg(self) -> None: + pass + if __name__ == "__main__": # pragma: no cover console = Console() From fb2f98b96231a733751495195aa8cca3f1c7dff8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Mar 2022 11:19:34 +0000 Subject: [PATCH 02/29] Exporting SVG --- rich/console.py | 131 +++++++++++++++++++++++++++++++++++++++-- rich/terminal_theme.py | 25 ++++++++ 2 files changed, 150 insertions(+), 6 deletions(-) diff --git a/rich/console.py b/rich/console.py index a6dbe514c..45ad41c6a 100644 --- a/rich/console.py +++ b/rich/console.py @@ -60,7 +60,7 @@ from .segment import Segment from .style import Style, StyleType from .styled import Styled -from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme +from .terminal_theme import DEFAULT_TERMINAL_THEME, SVG_EXPORT_THEME, TerminalTheme from .text import Text, TextType from .theme import Theme, ThemeStack @@ -115,6 +115,26 @@ class NoChange: """ +CONSOLE_SVG_FORMAT = """ + + + + + + + + + {title} + + + + + + {code} + + +""" + _TERM_COLORS = {"256color": ColorSystem.EIGHT_BIT, "16color": ColorSystem.STANDARD} @@ -2187,15 +2207,106 @@ def save_html( with open(path, "wt", encoding="utf-8") as write_file: write_file.write(html) - def export_svg(self) -> str: - pass + def export_svg( + self, + title: str = "Rich", + theme: Optional[TerminalTheme] = None, + clear: bool = True, + ) -> str: + assert ( + self.record + ), "To export console contents set record=True in the constructor or instance" - def save_svg(self) -> None: - pass + fragments: List[str] = [] + append = fragments.append + _theme = theme or SVG_EXPORT_THEME + code_format = CONSOLE_SVG_FORMAT # TODO: Support user defined formats + + with self._record_buffer_lock: + segments = Segment.simplify(self._record_buffer) + segments = Segment.filter_control(segments) + text = Text.assemble(*((text, style) for text, style, _ in segments)) + lines = text.wrap(self, width=self.width, overflow="fold") + segments = self.render(lines, options=self.options) + segment_lines = list( + Segment.split_and_crop_lines( + segments, length=self.width, include_new_lines=False + ) + ) + left_margin = 12 + font_size = 12 # TODO: currently hardcoded here, need to use in template + line_spacing = 2 + code_start_y = 60 + y = code_start_y + required_code_height = (font_size + line_spacing) * len(lines) + + for line in segment_lines: + line_spans = [] + for segment in line: + text, style, _ = segment + text = escape(text) + if style: + font_weight = "bold" if style.bold else "normal" + font_style = "italic" if style.italic else "normal" + fill_color = "#f0f0f0" # TODO: what to default to? - terminal theme foreground color + # TODO: inject terminal theme background into template + color = style.color + if color: + triplet = style.color.get_truecolor(_theme) + fill_color = triplet.hex + + text_spaces_escaped = text.replace(" ", " ") + text = f'{text_spaces_escaped}' + line_spans.append(text) + + line_text = "".join(line_spans) + append(f'{line_text}') + y += font_size + line_spacing + + margin = 50 + terminal_height = required_code_height + code_start_y + monospace_font_width_scale = 0.55 + terminal_width = ( + self.width * monospace_font_width_scale * font_size + + 2 * left_margin + + self.width + ) + total_height = terminal_height + 2 * margin + total_width = terminal_width + 2 * margin + title_mid_anchor = terminal_width / 2 + + rendered_code = code_format.format( + code="\n\t".join(fragments), + total_height=total_height, + total_width=total_width, + terminal_width=terminal_width, + terminal_height=terminal_height, + title_mid_anchor=title_mid_anchor, + margin=margin, + font_size=font_size, + title=title, + ) + + if clear: + self._record_buffer.clear() + + return rendered_code + + def save_svg( + self, + path: str, + *, + title: str = "Rich", + theme: Optional[TerminalTheme] = None, + clear: bool = True, + ) -> None: + svg = self.export_svg(title=title, theme=theme, clear=clear) + with open(path, "wt", encoding="utf-8") as write_file: + write_file.write(svg) if __name__ == "__main__": # pragma: no cover - console = Console() + console = Console(record=True) console.log( "JSONRPC [i]request[/i]", @@ -2250,4 +2361,12 @@ def save_svg(self) -> None: ) console.log("foo") + from rich.panel import Panel + console.print_json(data={"name": "apple", "count": 1}, indent=None) + console.print_json(data={"name": "apple", "count": 1}, indent=None) + console.print(Panel("Hello, world!"), width=20) + svg = console.export_svg( + title="Rich Output Exported to SVG", theme=SVG_EXPORT_THEME + ) + print(svg) diff --git a/rich/terminal_theme.py b/rich/terminal_theme.py index 801ac0b7b..d33bc38da 100644 --- a/rich/terminal_theme.py +++ b/rich/terminal_theme.py @@ -53,3 +53,28 @@ def __init__( (255, 255, 255), ], ) + +SVG_EXPORT_THEME = TerminalTheme( + (12, 12, 12), + (242, 242, 242), + [ + (12, 12, 12), + (205, 49, 49), + (13, 188, 121), + (229, 229, 16), + (36, 114, 200), + (188, 63, 188), + (17, 168, 205), + (229, 229, 229), + ], + [ + (102, 102, 102), + (241, 76, 76), + (35, 209, 139), + (245, 245, 67), + (59, 142, 234), + (214, 112, 214), + (41, 184, 219), + (229, 229, 229), + ], +) From 18d3638f257247746fe62b36e5c9a36738aea8d2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 22 Mar 2022 16:01:05 +0000 Subject: [PATCH 03/29] SVG export - Writing HTML foreign object into naively calculated SVG rect background --- rich/console.py | 85 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/rich/console.py b/rich/console.py index 45ad41c6a..b12b59666 100644 --- a/rich/console.py +++ b/rich/console.py @@ -107,7 +107,7 @@ class NoChange: - +
{code}
@@ -117,6 +117,11 @@ class NoChange: CONSOLE_SVG_FORMAT = """ + @@ -125,12 +130,18 @@ class NoChange: {title} - - - + + + - - {code} + + + +
+ {code} +
+ +
""" @@ -2233,35 +2244,44 @@ def export_svg( segments, length=self.width, include_new_lines=False ) ) - left_margin = 12 - font_size = 12 # TODO: currently hardcoded here, need to use in template - line_spacing = 2 - code_start_y = 60 - y = code_start_y - required_code_height = (font_size + line_spacing) * len(lines) + fragments = [] + foreground_color = _theme.foreground_color.hex + theme_default_foreground = ( + f"color: {foreground_color}; text-decoration-color: {foreground_color};" + ) for line in segment_lines: line_spans = [] for segment in line: text, style, _ = segment text = escape(text) if style: - font_weight = "bold" if style.bold else "normal" - font_style = "italic" if style.italic else "normal" - fill_color = "#f0f0f0" # TODO: what to default to? - terminal theme foreground color - # TODO: inject terminal theme background into template - color = style.color - if color: - triplet = style.color.get_truecolor(_theme) - fill_color = triplet.hex - - text_spaces_escaped = text.replace(" ", " ") - text = f'{text_spaces_escaped}' + rule = style.get_html_style(_theme) + if style.link: + text = f'{text}' + + # If the style doesn't contain a color, we still + # need to make sure we output the default colors + # from the TerminalTheme. + additional_styles = "" + if not style.color: + additional_styles += theme_default_foreground + + text = ( + f'{text}' + ) + else: + text = f'{text}' line_spans.append(text) - line_text = "".join(line_spans) - append(f'{line_text}') - y += font_size + line_spacing + fragments.append(f"
{''.join(line_spans)}
") + + left_margin = 12 + font_size = 12 + line_spacing = 2 + code_start_y = 60 + y = code_start_y + required_code_height = (font_size + line_spacing) * len(lines) margin = 50 terminal_height = required_code_height + code_start_y @@ -2276,7 +2296,7 @@ def export_svg( title_mid_anchor = terminal_width / 2 rendered_code = code_format.format( - code="\n\t".join(fragments), + code="\n".join(fragments), total_height=total_height, total_width=total_width, terminal_width=terminal_width, @@ -2362,10 +2382,21 @@ def save_svg( console.log("foo") from rich.panel import Panel + from rich.syntax import Syntax console.print_json(data={"name": "apple", "count": 1}, indent=None) console.print_json(data={"name": "apple", "count": 1}, indent=None) console.print(Panel("Hello, world!"), width=20) + console.print( + Syntax( + """ + def hello_world(): + print("Hello, world!") + """, + lexer="python", + ) + ) + console.print("[white on blue]white on blue!") svg = console.export_svg( title="Rich Output Exported to SVG", theme=SVG_EXPORT_THEME ) From 31dadb872b15a9c46ddb1e5c729d65ec10ea3b39 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Mar 2022 13:42:24 +0000 Subject: [PATCH 04/29] Exporting as foreign object SVG --- rich/console.py | 110 ++++++++++++++++++++++++++++++++++-------------- rich/syntax.py | 4 +- 2 files changed, 81 insertions(+), 33 deletions(-) diff --git a/rich/console.py b/rich/console.py index b12b59666..cf01e000d 100644 --- a/rich/console.py +++ b/rich/console.py @@ -115,34 +115,76 @@ class NoChange: """ -CONSOLE_SVG_FORMAT = """ - +CONSOLE_SVG_FORMAT = """\ + - - - - - - - - {title} - - - - - - + -
- {code} +
+
+ + + + + +
+ Rich Output Exported to SVG +
+
+
+ {code} +
- """ @@ -2247,6 +2289,7 @@ def export_svg( fragments = [] foreground_color = _theme.foreground_color.hex + background_color = _theme.background_color.hex theme_default_foreground = ( f"color: {foreground_color}; text-decoration-color: {foreground_color};" ) @@ -2274,18 +2317,19 @@ def export_svg( text = f'{text}' line_spans.append(text) - fragments.append(f"
{''.join(line_spans)}
") + fragments.append(f"
{''.join(line_spans)}
") left_margin = 12 - font_size = 12 - line_spacing = 2 + font_size = 18 + line_height = font_size + 4 code_start_y = 60 - y = code_start_y - required_code_height = (font_size + line_spacing) * len(lines) + required_code_height = line_height * len(lines) - margin = 50 + margin = 140 terminal_height = required_code_height + code_start_y - monospace_font_width_scale = 0.55 + monospace_font_width_scale = ( + 0.6 # generally around 0.5-0.55 width/height ratio, added extra to be safe + ) terminal_width = ( self.width * monospace_font_width_scale * font_size + 2 * left_margin @@ -2302,8 +2346,11 @@ def export_svg( terminal_width=terminal_width, terminal_height=terminal_height, title_mid_anchor=title_mid_anchor, + theme_foreground_color=foreground_color, + theme_background_color=background_color, margin=margin, font_size=font_size, + line_height=line_height, title=title, ) @@ -2397,7 +2444,6 @@ def hello_world(): ) ) console.print("[white on blue]white on blue!") - svg = console.export_svg( - title="Rich Output Exported to SVG", theme=SVG_EXPORT_THEME - ) + + svg = console.export_svg(title="Rich Output Exported to SVG") print(svg) diff --git a/rich/syntax.py b/rich/syntax.py index 6a337e407..d5bd5a3ed 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -743,7 +743,7 @@ def __rich_console__( from rich.console import Console - console = Console(force_terminal=args.force_color, width=args.width) + console = Console(force_terminal=args.force_color, width=args.width, record=True) if args.path == "-": code = sys.stdin.read() @@ -767,3 +767,5 @@ def __rich_console__( indent_guides=args.indent_guides, ) console.print(syntax, soft_wrap=args.soft_wrap) + svg = console.export_svg(title="Rich Output Exported to SVG") + print(svg) From 00a4bd130a7f51b7aa13c5fe28fa5be50e73a162 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 23 Mar 2022 15:27:17 +0000 Subject: [PATCH 05/29] Working with drop-shadow --- rich/console.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/rich/console.py b/rich/console.py index cf01e000d..90212409f 100644 --- a/rich/console.py +++ b/rich/console.py @@ -2266,19 +2266,20 @@ def export_svg( theme: Optional[TerminalTheme] = None, clear: bool = True, ) -> str: + + # TODO: Dim, reverse, flashing style support + assert ( self.record ), "To export console contents set record=True in the constructor or instance" - fragments: List[str] = [] - append = fragments.append _theme = theme or SVG_EXPORT_THEME - code_format = CONSOLE_SVG_FORMAT # TODO: Support user defined formats with self._record_buffer_lock: segments = Segment.simplify(self._record_buffer) segments = Segment.filter_control(segments) - text = Text.assemble(*((text, style) for text, style, _ in segments)) + parts = [(text, style or Style.null()) for text, style, _ in segments] + text = Text.assemble(*parts) lines = text.wrap(self, width=self.width, overflow="fold") segments = self.render(lines, options=self.options) segment_lines = list( @@ -2304,7 +2305,7 @@ def export_svg( text = f'{text}' # If the style doesn't contain a color, we still - # need to make sure we output the default colors + # need to make sure we output the default foreground color # from the TerminalTheme. additional_styles = "" if not style.color: @@ -2319,11 +2320,14 @@ def export_svg( fragments.append(f"
{''.join(line_spans)}
") - left_margin = 12 - font_size = 18 - line_height = font_size + 4 - code_start_y = 60 - required_code_height = line_height * len(lines) + if clear: + self._record_buffer.clear() + + left_margin = 12 + font_size = 18 + line_height = font_size + 4 + code_start_y = 60 + required_code_height = line_height * len(lines) margin = 140 terminal_height = required_code_height + code_start_y @@ -2339,7 +2343,7 @@ def export_svg( total_width = terminal_width + 2 * margin title_mid_anchor = terminal_width / 2 - rendered_code = code_format.format( + rendered_code = CONSOLE_SVG_FORMAT.format( code="\n".join(fragments), total_height=total_height, total_width=total_width, @@ -2354,9 +2358,6 @@ def export_svg( title=title, ) - if clear: - self._record_buffer.clear() - return rendered_code def save_svg( From 0bc4530d6b324aa60cd6ba7fbb3cd2b247593129 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Mar 2022 13:04:53 +0000 Subject: [PATCH 06/29] Update terminal output style to include tab and background/border --- rich/__main__.py | 5 +++- rich/console.py | 62 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/rich/__main__.py b/rich/__main__.py index 7b3ffb4f2..ae1228db1 100644 --- a/rich/__main__.py +++ b/rich/__main__.py @@ -233,7 +233,7 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: from rich.panel import Panel - console = Console() + console = Console(record=True) sponsor_message = Table.grid(padding=1) sponsor_message.add_column(style="green", justify="right") @@ -276,3 +276,6 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: ), justify="center", ) + + svg = console.export_svg() + print(svg) diff --git a/rich/console.py b/rich/console.py index 90212409f..c56e3d88c 100644 --- a/rich/console.py +++ b/rich/console.py @@ -123,7 +123,6 @@ class NoChange: display: inline-block; white-space: pre; vertical-align: top; - margin: 0; font-size: {font_size}px; font-family:Fira Code,Monaco,Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace; }} @@ -131,43 +130,67 @@ class NoChange: text-decoration: none; color: inherit; }} + .wrapper {{ + padding: {margin}px; + }} .terminal {{ + position: relative; display: flex; flex-direction: column; align-items: center; - margin: {margin}px; - margin-top: 50px; background-color: {theme_background_color}; - padding: 14px; - padding-bottom: 20px; - border-radius: 12px; - box-shadow: inset 0 0 0 1px rgb(255,255,255,.4), - 0px 0.9px 0.7px rgba(0, 0, 0, 0.163), - 0px 2.5px 4.8px rgba(0, 0, 0, 0.24), - 0px 6px 16.5px rgba(0, 0, 0, 0.258), - 0px 20px 60px rgba(0, 0, 0, 0.51) + border-radius: 14px; + outline: 1px solid #484848; + }} + .terminal:after {{ + position: absolute; + width: 100%; + height: 100%; + content: ''; + border-radius: 14px; + background: rgb(71,77,102); + background: linear-gradient(90deg, #365D73 0%, #4F7775 100%); + transform: rotate(3.5deg); + z-index: -1; }} .terminal-header {{ position: relative; width: 100%; - text-align: center; - font-family: sans-serif; - font-size: 18px; - margin-bottom: 24px; + background-color: #2e2e2e; + margin-bottom: 12px; font-weight: bold; + border-radius: 14px 14px 0 0; color: {theme_foreground_color}; + font-size: 18px; + box-shadow: inset 0px -1px 0px 0px #4e4e4e, + inset 0px -4px 8px 0px #1a1a1a; + }} + .terminal-title-tab {{ + display: inline-block; + margin-top: 14px; + margin-left: 124px; + font-family: sans-serif; + padding: 14px 28px; + border-radius: 6px 6px 0 0; + background-color: #0c0c0c; + box-shadow: inset 0px 1px 0px 0px #4e4e4e, + 0px -4px 4px 0px #1e1e1e, + inset 1px 0px 0px 0px #4e4e4e, + inset -1px 0px 0px 0px #4e4e4e; }} .terminal-traffic-lights {{ position: absolute; - top: 2px; - left: 6px; + top: 24px; + left: 20px; }} .terminal-body {{ line-height: {line_height}px; + padding: 14px; }} +
@@ -175,14 +198,13 @@ class NoChange: -
- Rich Output Exported to SVG -
+
{title}
{code}
+
From 6c3e385d2da5829420559b1e4382169c917b0a50 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Mar 2022 16:06:36 +0000 Subject: [PATCH 07/29] Add more terminal themes, support dim, reverse in SVG output --- rich/__main__.py | 3 -- rich/console.py | 64 +++++++++++++++++------------------- rich/terminal_theme.py | 73 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 37 deletions(-) diff --git a/rich/__main__.py b/rich/__main__.py index ae1228db1..b7eeb9a71 100644 --- a/rich/__main__.py +++ b/rich/__main__.py @@ -276,6 +276,3 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: ), justify="center", ) - - svg = console.export_svg() - print(svg) diff --git a/rich/console.py b/rich/console.py index c56e3d88c..fedeedd2c 100644 --- a/rich/console.py +++ b/rich/console.py @@ -130,10 +130,10 @@ class NoChange: text-decoration: none; color: inherit; }} - .wrapper {{ + #wrapper {{ padding: {margin}px; }} - .terminal {{ + #terminal {{ position: relative; display: flex; flex-direction: column; @@ -142,18 +142,18 @@ class NoChange: border-radius: 14px; outline: 1px solid #484848; }} - .terminal:after {{ + #terminal:after {{ position: absolute; width: 100%; height: 100%; content: ''; border-radius: 14px; background: rgb(71,77,102); - background: linear-gradient(90deg, #365D73 0%, #4F7775 100%); - transform: rotate(3.5deg); + background: linear-gradient(90deg, #804D69 0%, #4E4B89 100%); + transform: rotate(-4.5deg); z-index: -1; }} - .terminal-header {{ + #terminal-header {{ position: relative; width: 100%; background-color: #2e2e2e; @@ -165,46 +165,46 @@ class NoChange: box-shadow: inset 0px -1px 0px 0px #4e4e4e, inset 0px -4px 8px 0px #1a1a1a; }} - .terminal-title-tab {{ + #terminal-title-tab {{ display: inline-block; margin-top: 14px; margin-left: 124px; font-family: sans-serif; padding: 14px 28px; border-radius: 6px 6px 0 0; - background-color: #0c0c0c; + background-color: {theme_background_color}; box-shadow: inset 0px 1px 0px 0px #4e4e4e, 0px -4px 4px 0px #1e1e1e, inset 1px 0px 0px 0px #4e4e4e, inset -1px 0px 0px 0px #4e4e4e; }} - .terminal-traffic-lights {{ + #terminal-traffic-lights {{ position: absolute; top: 24px; left: 20px; }} - .terminal-body {{ + #terminal-body {{ line-height: {line_height}px; padding: 14px; }} -
-
-
- - - - - -
{title}
-
-
- {code} +
+
+
+ + + + + +
{title}
+
+
+ {code} +
-
@@ -2311,11 +2311,8 @@ def export_svg( ) fragments = [] - foreground_color = _theme.foreground_color.hex - background_color = _theme.background_color.hex - theme_default_foreground = ( - f"color: {foreground_color}; text-decoration-color: {foreground_color};" - ) + theme_foreground_color = _theme.foreground_color.hex + theme_background_color = _theme.background_color.hex for line in segment_lines: line_spans = [] for segment in line: @@ -2329,15 +2326,14 @@ def export_svg( # If the style doesn't contain a color, we still # need to make sure we output the default foreground color # from the TerminalTheme. + theme_default_foreground = f"color: {theme_foreground_color}; text-decoration-color: {theme_foreground_color};" additional_styles = "" if not style.color: additional_styles += theme_default_foreground - text = ( - f'{text}' - ) + text = f'{text}' else: - text = f'{text}' + text = f'{text}' line_spans.append(text) fragments.append(f"
{''.join(line_spans)}
") @@ -2372,8 +2368,8 @@ def export_svg( terminal_width=terminal_width, terminal_height=terminal_height, title_mid_anchor=title_mid_anchor, - theme_foreground_color=foreground_color, - theme_background_color=background_color, + theme_foreground_color=theme_foreground_color, + theme_background_color=theme_background_color, margin=margin, font_size=font_size, line_height=line_height, diff --git a/rich/terminal_theme.py b/rich/terminal_theme.py index d33bc38da..ace8e93de 100644 --- a/rich/terminal_theme.py +++ b/rich/terminal_theme.py @@ -78,3 +78,76 @@ def __init__( (229, 229, 229), ], ) + +MONOKAI = TerminalTheme( + (12, 12, 12), + (217, 217, 217), + [ + (26, 26, 26), + (244, 0, 95), + (152, 224, 36), + (253, 151, 31), + (157, 101, 255), + (244, 0, 95), + (88, 209, 235), + (196, 197, 181), + (98, 94, 76), + ], + [ + (244, 0, 95), + (152, 224, 36), + (224, 213, 97), + (157, 101, 255), + (244, 0, 95), + (88, 209, 235), + (246, 246, 239), + ], +) +DIMMED_MONOKAI = TerminalTheme( + (25, 25, 25), + (185, 188, 186), + [ + (58, 61, 67), + (190, 63, 72), + (135, 154, 59), + (197, 166, 53), + (79, 118, 161), + (133, 92, 141), + (87, 143, 164), + (185, 188, 186), + (136, 137, 135), + ], + [ + (251, 0, 31), + (15, 114, 47), + (196, 112, 51), + (24, 109, 227), + (251, 0, 103), + (46, 112, 109), + (253, 255, 185), + ], +) +NIGHT_OWLISH = TerminalTheme( + (255, 255, 255), + (64, 63, 83), + [ + (1, 22, 39), + (211, 66, 62), + (42, 162, 152), + (218, 170, 1), + (72, 118, 214), + (64, 63, 83), + (8, 145, 106), + (122, 129, 129), + (122, 129, 129), + ], + [ + (247, 110, 110), + (73, 208, 197), + (218, 194, 107), + (92, 167, 228), + (105, 112, 152), + (0, 201, 144), + (152, 159, 177), + ], +) From 9fe64875249421dfc4e303c122b80dd477c7bf5c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Mar 2022 16:08:16 +0000 Subject: [PATCH 08/29] Fix some HTML export tests --- rich/console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rich/console.py b/rich/console.py index fedeedd2c..803b1dc87 100644 --- a/rich/console.py +++ b/rich/console.py @@ -107,7 +107,7 @@ class NoChange: - +
{code}
From 8a362c393cf913dcdcb769ece7d631d6b30bea2f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Mar 2022 16:49:33 +0000 Subject: [PATCH 09/29] Allow for templating of SVG output --- rich/console.py | 51 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/rich/console.py b/rich/console.py index 803b1dc87..e7e3b68e7 100644 --- a/rich/console.py +++ b/rich/console.py @@ -124,7 +124,7 @@ class NoChange: white-space: pre; vertical-align: top; font-size: {font_size}px; - font-family:Fira Code,Monaco,Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace; + font-family:'Fira Code','Cascadia Code',Monaco,Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace; }} a {{ text-decoration: none; @@ -2287,10 +2287,21 @@ def export_svg( title: str = "Rich", theme: Optional[TerminalTheme] = None, clear: bool = True, + code_format: str = CONSOLE_SVG_FORMAT, ) -> str: + """Generate an SVG string from the console contents (requires record=True in Console constructor) - # TODO: Dim, reverse, flashing style support + Args: + title (str): The title of the tab in the output image + theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal + clear (bool, optional): Clear record buffer after exporting. Defaults to ``True`` + code_format (str): Format string used to generate the SVG. Rich will inject a number of variables + into the string in order to form the final SVG output. The default template used and the variables + injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable. + Returns: + str: The string representation of the SVG. That is, the ``code_format`` template with content injected. + """ assert ( self.record ), "To export console contents set record=True in the constructor or instance" @@ -2341,33 +2352,34 @@ def export_svg( if clear: self._record_buffer.clear() - left_margin = 12 + # These values are the ones that I found to work well after experimentation. + # Many of them can be tweaked, but too much variation from these values could + # result in visually broken output/clipping issues. + terminal_padding = 12 font_size = 18 line_height = font_size + 4 code_start_y = 60 required_code_height = line_height * len(lines) + # Monospace fonts are generally around 0.5-0.55 width/height ratio, but I've + # added extra width to ensure that the output SVG is big enough. + monospace_font_width_scale = 0.6 margin = 140 terminal_height = required_code_height + code_start_y - monospace_font_width_scale = ( - 0.6 # generally around 0.5-0.55 width/height ratio, added extra to be safe - ) + + # This works out as a good heuristic for the final width of the drawn terminal. terminal_width = ( self.width * monospace_font_width_scale * font_size - + 2 * left_margin + + 2 * terminal_padding + self.width ) total_height = terminal_height + 2 * margin total_width = terminal_width + 2 * margin - title_mid_anchor = terminal_width / 2 - rendered_code = CONSOLE_SVG_FORMAT.format( + rendered_code = code_format.format( code="\n".join(fragments), total_height=total_height, total_width=total_width, - terminal_width=terminal_width, - terminal_height=terminal_height, - title_mid_anchor=title_mid_anchor, theme_foreground_color=theme_foreground_color, theme_background_color=theme_background_color, margin=margin, @@ -2385,8 +2397,21 @@ def save_svg( title: str = "Rich", theme: Optional[TerminalTheme] = None, clear: bool = True, + code_format: str = CONSOLE_SVG_FORMAT, ) -> None: - svg = self.export_svg(title=title, theme=theme, clear=clear) + """Generate an SVG file from the console contents (requires record=True in Console constructor) + + Args: + title (str): The title of the tab in the output image + theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal + clear (bool, optional): Clear record buffer after exporting. Defaults to ``True`` + code_format (str): Format string used to generate the SVG. Rich will inject a number of variables + into the string in order to form the final SVG output. The default template used and the variables + injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable. + """ + svg = self.export_svg( + title=title, theme=theme, clear=clear, code_format=code_format + ) with open(path, "wt", encoding="utf-8") as write_file: write_file.write(svg) From 66dfef85fa1b1534234d4d6cff4ea865a2545e7f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Mar 2022 16:51:32 +0000 Subject: [PATCH 10/29] Fix mypy issue involving shadowed variable in SVG export --- rich/console.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rich/console.py b/rich/console.py index e7e3b68e7..6ff42cf78 100644 --- a/rich/console.py +++ b/rich/console.py @@ -2312,8 +2312,8 @@ def export_svg( segments = Segment.simplify(self._record_buffer) segments = Segment.filter_control(segments) parts = [(text, style or Style.null()) for text, style, _ in segments] - text = Text.assemble(*parts) - lines = text.wrap(self, width=self.width, overflow="fold") + terminal_text = Text.assemble(*parts) + lines = terminal_text.wrap(self, width=self.width, overflow="fold") segments = self.render(lines, options=self.options) segment_lines = list( Segment.split_and_crop_lines( From a6b630c06729e01b9175df9cd1db9c8ed17d981e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Mar 2022 16:54:35 +0000 Subject: [PATCH 11/29] Remove unused code from main block in console.py --- rich/console.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/rich/console.py b/rich/console.py index 6ff42cf78..2cfd04003 100644 --- a/rich/console.py +++ b/rich/console.py @@ -2470,24 +2470,3 @@ def save_svg( }, } ) - console.log("foo") - - from rich.panel import Panel - from rich.syntax import Syntax - - console.print_json(data={"name": "apple", "count": 1}, indent=None) - console.print_json(data={"name": "apple", "count": 1}, indent=None) - console.print(Panel("Hello, world!"), width=20) - console.print( - Syntax( - """ - def hello_world(): - print("Hello, world!") - """, - lexer="python", - ) - ) - console.print("[white on blue]white on blue!") - - svg = console.export_svg(title="Rich Output Exported to SVG") - print(svg) From c3ed655e338c10b9c5ec0cb1ba0d3d75056538e9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Mar 2022 16:55:35 +0000 Subject: [PATCH 12/29] Remove unused record=True in __main__.py Console init --- rich/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rich/__main__.py b/rich/__main__.py index b7eeb9a71..7b3ffb4f2 100644 --- a/rich/__main__.py +++ b/rich/__main__.py @@ -233,7 +233,7 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: from rich.panel import Panel - console = Console(record=True) + console = Console() sponsor_message = Table.grid(padding=1) sponsor_message.add_column(style="green", justify="right") From b6f93e43f0a10deccab0a471d3df16afbc88ce17 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Mar 2022 16:58:55 +0000 Subject: [PATCH 13/29] Small tidy ups in console.py SVG export --- rich/console.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rich/console.py b/rich/console.py index 2cfd04003..51936901f 100644 --- a/rich/console.py +++ b/rich/console.py @@ -2360,14 +2360,14 @@ def export_svg( line_height = font_size + 4 code_start_y = 60 required_code_height = line_height * len(lines) + margin = 140 # Monospace fonts are generally around 0.5-0.55 width/height ratio, but I've # added extra width to ensure that the output SVG is big enough. monospace_font_width_scale = 0.6 - margin = 140 - terminal_height = required_code_height + code_start_y - # This works out as a good heuristic for the final width of the drawn terminal. + # This works out as a good heuristic for the final size of the drawn terminal. + terminal_height = required_code_height + code_start_y terminal_width = ( self.width * monospace_font_width_scale * font_size + 2 * terminal_padding @@ -2399,7 +2399,7 @@ def save_svg( clear: bool = True, code_format: str = CONSOLE_SVG_FORMAT, ) -> None: - """Generate an SVG file from the console contents (requires record=True in Console constructor) + """Generate an SVG file from the console contents (requires record=True in Console constructor). Args: title (str): The title of the tab in the output image From 7225b35bb50900359b5cb4009052e056ceee5786 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Mar 2022 17:02:20 +0000 Subject: [PATCH 14/29] Add test for exporting to SVG --- rich/console.py | 1 + tests/test_console.py | 102 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/rich/console.py b/rich/console.py index 51936901f..fbfe542f3 100644 --- a/rich/console.py +++ b/rich/console.py @@ -2402,6 +2402,7 @@ def save_svg( """Generate an SVG file from the console contents (requires record=True in Console constructor). Args: + path (str): The path to write the SVG to. title (str): The title of the tab in the output image theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal clear (bool, optional): Clear record buffer after exporting. Defaults to ``True`` diff --git a/tests/test_console.py b/tests/test_console.py index 701ccf740..daa622aa9 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -450,6 +450,108 @@ def test_export_html_inline(): assert html == expected +def test_export_svg(): + console = Console(record=True, width=100) + console.print("[b]foo [link=https://example.org]Click[/link]") + svg = console.export_svg() + expected = """\ + + + + +
+
+
+ + + + + +
Rich
+
+
+
foo Click
+
+
+
+
+ +
+
+""" + assert svg == expected + + def test_save_text(): console = Console(record=True, width=100) console.print("foo") From d3113e768143572bb01cc48963fa2b9195b50946 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 24 Mar 2022 17:05:29 +0000 Subject: [PATCH 15/29] Add tests for export SVG and save SVG --- tests/test_console.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/test_console.py b/tests/test_console.py index daa622aa9..a5bb00d95 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -450,11 +450,7 @@ def test_export_html_inline(): assert html == expected -def test_export_svg(): - console = Console(record=True, width=100) - console.print("[b]foo [link=https://example.org]Click[/link]") - svg = console.export_svg() - expected = """\ +EXPECTED_SVG = """\ """ - assert svg == expected + + +def test_export_svg(): + console = Console(record=True, width=100) + console.print("[b]foo [link=https://example.org]Click[/link]") + svg = console.export_svg() + + assert svg == EXPECTED_SVG + + +def test_save_svg(): + console = Console(record=True, width=100) + console.print("[b]foo [link=https://example.org]Click[/link]") + with tempfile.TemporaryDirectory() as path: + export_path = os.path.join(path, "example.svg") + console.save_svg(export_path) + with open(export_path, "rt") as svg_file: + assert svg_file.read() == EXPECTED_SVG def test_save_text(): From bd55ee339c858c349f02fffe3c277da02a96cd62 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Mar 2022 10:11:22 +0100 Subject: [PATCH 16/29] Update docs with info on SVG exports --- docs/source/console.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/console.rst b/docs/source/console.rst index ed4b6c46f..e890f2001 100644 --- a/docs/source/console.rst +++ b/docs/source/console.rst @@ -249,12 +249,12 @@ If Python's builtin :mod:`readline` module is previously loaded, elaborate line Exporting --------- -The Console class can export anything written to it as either text or html. To enable exporting, first set ``record=True`` on the constructor. This tells Rich to save a copy of any data you ``print()`` or ``log()``. Here's an example:: +The Console class can export anything written to it as either text, svg, or html. To enable exporting, first set ``record=True`` on the constructor. This tells Rich to save a copy of any data you ``print()`` or ``log()``. Here's an example:: from rich.console import Console console = Console(record=True) -After you have written content, you can call :meth:`~rich.console.Console.export_text` or :meth:`~rich.console.Console.export_html` to get the console output as a string. You can also call :meth:`~rich.console.Console.save_text` or :meth:`~rich.console.Console.save_html` to write the contents directly to disk. +After you have written content, you can call :meth:`~rich.console.Console.export_text`, :meth:`~rich.console.Console.export_svg` or :meth:`~rich.console.Console.export_html` to get the console output as a string. You can also call :meth:`~rich.console.Console.save_text`, :meth:`~rich.console.Console.save_svg`, or :meth:`~rich.console.Console.save_html` to write the contents directly to disk. For examples of the html output generated by Rich Console, see :ref:`appendix-colors`. From 645b58823d146aff8f33fecab0be97a324943adb Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Mar 2022 12:39:56 +0100 Subject: [PATCH 17/29] Add support for blink and blink2 to SVG export, use Fira Code webfont fallback --- rich/console.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/rich/console.py b/rich/console.py index fbfe542f3..c3474b5a4 100644 --- a/rich/console.py +++ b/rich/console.py @@ -119,6 +119,22 @@ class NoChange: {text}' + # If the style doesn't contain a color, we still # need to make sure we output the default foreground color # from the TerminalTheme. @@ -2364,7 +2391,7 @@ def export_svg( # Monospace fonts are generally around 0.5-0.55 width/height ratio, but I've # added extra width to ensure that the output SVG is big enough. - monospace_font_width_scale = 0.6 + monospace_font_width_scale = 0.57 # This works out as a good heuristic for the final size of the drawn terminal. terminal_height = required_code_height + code_start_y From bb69a6e87503fdadad6def07806ee49b36e9125c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Mar 2022 13:06:39 +0100 Subject: [PATCH 18/29] Update tests for SVG exporting --- tests/test_console.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/test_console.py b/tests/test_console.py index a5bb00d95..0e3b9b8da 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -451,9 +451,25 @@ def test_export_html_inline(): EXPECTED_SVG = """\ -
Rich
-
foo Click
+
foo Click
@@ -549,7 +573,7 @@ def test_export_html_inline(): def test_export_svg(): console = Console(record=True, width=100) - console.print("[b]foo [link=https://example.org]Click[/link]") + console.print("[b]foo [blink][link=https://example.org]Click[/link][/]") svg = console.export_svg() assert svg == EXPECTED_SVG @@ -557,7 +581,7 @@ def test_export_svg(): def test_save_svg(): console = Console(record=True, width=100) - console.print("[b]foo [link=https://example.org]Click[/link]") + console.print("[b]foo [blink][link=https://example.org]Click[/link][/]") with tempfile.TemporaryDirectory() as path: export_path = os.path.join(path, "example.svg") console.save_svg(export_path) From 4834f038f7acf168300c543557b5e7396da700a5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Mar 2022 14:43:07 +0100 Subject: [PATCH 19/29] Add more information to docs about SVG exporting --- docs/images/svg_export.svg | 176 +++++++++++++++++++++++++++++++++++++ docs/source/console.rst | 13 ++- rich/__main__.py | 4 +- rich/console.py | 7 +- 4 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 docs/images/svg_export.svg diff --git a/docs/images/svg_export.svg b/docs/images/svg_export.svg new file mode 100644 index 000000000..4d6d8479e --- /dev/null +++ b/docs/images/svg_export.svg @@ -0,0 +1,176 @@ + + + + +
+
+
+ + + + + +
Rich can export to SVG!
+
+
+
Rich features
+
+
Colors 4-bit color
+
8-bit color
+
Truecolor (16.7 million)
+
Dumb terminals
+
Automatic color conversion
+
+
Styles All ansi styles: bold, dim, italic, underline, strikethrough, reverse, and even blink.
+
+
Text Word wrap text. Justify left, center, right or full.
+
+
Lorem ipsum dolor sit Lorem ipsum dolor sit Lorem ipsum dolor sit Lorem ipsum dolor sit
+
amet, consectetur amet, consectetur amet, consectetur amet, consectetur
+
adipiscing elit. adipiscing elit. adipiscing elit. adipiscing elit. Quisque
+
Quisque in metus sed Quisque in metus sed Quisque in metus sed in metus sed sapien
+
sapien ultricies sapien ultricies sapien ultricies ultricies pretium a at
+
pretium a at justo. pretium a at justo. pretium a at justo. justo. Maecenas luctus
+
Maecenas luctus velit Maecenas luctus velit Maecenas luctus velit velit et auctor maximus.
+
et auctor maximus. et auctor maximus. et auctor maximus.
+
+
Asian 🇨🇳 该库支持中文,日文和韩文文本!
+
language 🇯🇵 ライブラリは中国語、日本語、韓国語のテキストをサポートしています
+
support 🇰🇷 이 라이브러리는 중국어, 일본어 및 한국어 텍스트를 지원합니다
+
+
Markup Rich supports a simple bbcode-like markup for color, style, and emoji! 👍 🍎 🐜 🐻 🥖 🚌
+
+
Tables Date Title Production Budget Box Office
+
─────────────────────────────────────────────────────────────────────────────────────────
+
Dec 20, 2019 Star Wars: The Rise of Skywalker $275,000,000 $375,126,118
+
May 25, 2018 Solo: A Star Wars Story $275,000,000 $393,151,347
+
Dec 15, 2017 Star Wars Ep. VIII: The Last Jedi $262,000,000 $1,332,539,889
+
May 19, 1999 Star Wars Ep. I: The phantom Menace $115,000,000 $1,027,044,677
+
+
Syntax 1 def iter_last(values: Iterable[T]) -> Ite {
+
highlighting 2 """Iterate and generate a tuple with 'foo': [
+
& 3 iter_values = iter(values) │ │ 3.1427,
+
pretty 4 try: │ │ (
+
printing 5 │ │ previous_value = next(iter_values │ │ │ 'Paul Atreides',
+
6 except StopIteration: │ │ │ 'Vladimir Harkonnen',
+
7 │ │ return │ │ │ 'Thufir Hawat'
+
8 for value in iter_values: │ │ )
+
9 │ │ yield False, previous_value ],
+
10 │ │ previous_value = value 'atomic': (False, True, None)
+
11 yield True, previous_value }
+
+
Markdown # Markdown ╔═══════════════════════════════════════════╗
+
Markdown
+
Supports much of the *markdown* __syntax__! ╚═══════════════════════════════════════════╝
+
+
- Headers Supports much of the markdown syntax!
+
- Basic formatting: **bold**, *italic*, `code`
+
- Block quotes Headers
+
- Lists, and more... Basic formatting: bold, italic, code
+
Block quotes
+
Lists, and more...
+
+
+more! Progress bars, columns, styled logging handler, tracebacks, etc...
+
+
+
+
+
+ +
+
diff --git a/docs/source/console.rst b/docs/source/console.rst index e890f2001..7d742b45e 100644 --- a/docs/source/console.rst +++ b/docs/source/console.rst @@ -258,6 +258,17 @@ After you have written content, you can call :meth:`~rich.console.Console.export For examples of the html output generated by Rich Console, see :ref:`appendix-colors`. +Exporting SVGs +^^^^^^^^^^^^^^ + +When using :meth:`~rich.console.Console.export_svg` or :meth:`~rich.console.Console.save_svg`, the width of the generated SVG will +match the width (in terms of character cells) of your terminal window. The height of the exported SVG will scale automatically to accommodate the console output. + +The generated SVG can be viewed inside any web browser, and can be included on a webpage either by directly including the SVG markup +or by referencing the file itself using an :mod:`` tag. For finer control over the dimensions, you'll have to use an :mod:`` tag. + +.. image:: ../images/svg_export.svg + Error console ------------- @@ -381,7 +392,7 @@ If Rich detects that it is not writing to a terminal it will strip control codes Letting Rich auto-detect terminals is useful as it will write plain text when you pipe output to a file or other application. Interactive mode -~~~~~~~~~~~~~~~~ +---------------- Rich will remove animations such as progress bars and status indicators when not writing to a terminal as you probably don't want to write these out to a text file (for example). You can override this behavior by setting the ``force_interactive`` argument on the constructor. Set it to True to enable animations or False to disable them. diff --git a/rich/__main__.py b/rich/__main__.py index 7b3ffb4f2..5c91ac82e 100644 --- a/rich/__main__.py +++ b/rich/__main__.py @@ -226,7 +226,9 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: console.print(test_card) taken = round((process_time() - start) * 1000.0, 1) - Console().print(test_card) + c = Console(record=True) + c.print(test_card) + # c.save_svg(path="/Users/darrenburns/Library/Application Support/JetBrains/PyCharm2021.3/scratches/scratch_7.svg", title="Rich can export to SVG!") print(f"rendered in {pre_cache_taken}ms (cold cache)") print(f"rendered in {taken}ms (warm cache)") diff --git a/rich/console.py b/rich/console.py index c3474b5a4..f5e2556c9 100644 --- a/rich/console.py +++ b/rich/console.py @@ -156,6 +156,7 @@ class NoChange: }} #wrapper {{ padding: {margin}px; + padding-top: 100px; }} #terminal {{ position: relative; @@ -1454,7 +1455,7 @@ def render_str( highlight: Optional[bool] = None, highlighter: Optional[HighlighterType] = None, ) -> "Text": - """Convert a string to a Text instance. This is is called automatically if + """Convert a string to a Text instance. This is called automatically if you print or log a string. Args: @@ -1504,7 +1505,7 @@ def render_str( def get_style( self, name: Union[str, Style], *, default: Optional[Union[Style, str]] = None ) -> Style: - """Get a Style instance by it's theme name or parse a definition. + """Get a Style instance by its theme name or parse a definition. Args: name (str): The name of a style or a style definition. @@ -2391,7 +2392,7 @@ def export_svg( # Monospace fonts are generally around 0.5-0.55 width/height ratio, but I've # added extra width to ensure that the output SVG is big enough. - monospace_font_width_scale = 0.57 + monospace_font_width_scale = 0.60 # This works out as a good heuristic for the final size of the drawn terminal. terminal_height = required_code_height + code_start_y From 6afbd68178405db4256e599b70f4fc528bd0ee6a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Mar 2022 15:17:34 +0100 Subject: [PATCH 20/29] Update SVG exporting tests --- rich/__main__.py | 5 ++++- tests/test_console.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/rich/__main__.py b/rich/__main__.py index 5c91ac82e..144c40b02 100644 --- a/rich/__main__.py +++ b/rich/__main__.py @@ -228,7 +228,10 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: c = Console(record=True) c.print(test_card) - # c.save_svg(path="/Users/darrenburns/Library/Application Support/JetBrains/PyCharm2021.3/scratches/scratch_7.svg", title="Rich can export to SVG!") + c.save_svg( + path="/Users/darrenburns/Library/Application Support/JetBrains/PyCharm2021.3/scratches/scratch_7.svg", + title="Rich can export to SVG!", + ) print(f"rendered in {pre_cache_taken}ms (cold cache)") print(f"rendered in {taken}ms (warm cache)") diff --git a/tests/test_console.py b/tests/test_console.py index 0e3b9b8da..7ea32990f 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -451,7 +451,7 @@ def test_export_html_inline(): EXPECTED_SVG = """\ - ` tag. For finer control over the dimensions, you'll have to use an :mod:`` tag. +or by referencing the file itself using an ```` tag. For finer control over the dimensions, you'll have to use an ```` tag. + +The image below shows an example of an SVG exported by Rich. .. image:: ../images/svg_export.svg +You can customise the theme used during SVG export by importing the desired theme from the :mod:`rich.terminal_themes` module and passing it to +:meth:`~rich.console.Console.export_svg` or :meth:`~rich.console.Console.save_svg` via the ``theme`` parameter:: + + + from rich.console import Console + from rich.terminal_themes import MONOKAI + + console = Console(record=True) + console.save_svg("example.svg", theme=MONOKAI) + Error console ------------- diff --git a/rich/console.py b/rich/console.py index f5e2556c9..398c661af 100644 --- a/rich/console.py +++ b/rich/console.py @@ -729,7 +729,7 @@ class Console: 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. + required to call :meth:`export_html`, :meth:`export_svg`, 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. emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None. From 218adba42184f6e34ffef62692edc3f335adc1aa Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Mar 2022 15:48:46 +0100 Subject: [PATCH 22/29] Remove some development testing code --- rich/__main__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/rich/__main__.py b/rich/__main__.py index 144c40b02..7b3ffb4f2 100644 --- a/rich/__main__.py +++ b/rich/__main__.py @@ -226,12 +226,7 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: console.print(test_card) taken = round((process_time() - start) * 1000.0, 1) - c = Console(record=True) - c.print(test_card) - c.save_svg( - path="/Users/darrenburns/Library/Application Support/JetBrains/PyCharm2021.3/scratches/scratch_7.svg", - title="Rich can export to SVG!", - ) + Console().print(test_card) print(f"rendered in {pre_cache_taken}ms (cold cache)") print(f"rendered in {taken}ms (warm cache)") From 122c7be51bfab1288e5e36bc96b679e8e82deade Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Mar 2022 15:51:34 +0100 Subject: [PATCH 23/29] Remove some more testing code --- rich/syntax.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rich/syntax.py b/rich/syntax.py index d5bd5a3ed..6a337e407 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -743,7 +743,7 @@ def __rich_console__( from rich.console import Console - console = Console(force_terminal=args.force_color, width=args.width, record=True) + console = Console(force_terminal=args.force_color, width=args.width) if args.path == "-": code = sys.stdin.read() @@ -767,5 +767,3 @@ def __rich_console__( indent_guides=args.indent_guides, ) console.print(syntax, soft_wrap=args.soft_wrap) - svg = console.export_svg(title="Rich Output Exported to SVG") - print(svg) From 5f821177f9c1e1bf9bbb2a3d7ce6967b5dc44692 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Mar 2022 15:54:53 +0100 Subject: [PATCH 24/29] Improve docs, fix typo --- docs/source/console.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/source/console.rst b/docs/source/console.rst index 82f5706d7..c850e1183 100644 --- a/docs/source/console.rst +++ b/docs/source/console.rst @@ -271,16 +271,19 @@ The image below shows an example of an SVG exported by Rich. .. image:: ../images/svg_export.svg -You can customise the theme used during SVG export by importing the desired theme from the :mod:`rich.terminal_themes` module and passing it to +You can customise the theme used during SVG export by importing the desired theme from the :mod:`rich.terminal_theme` module and passing it to :meth:`~rich.console.Console.export_svg` or :meth:`~rich.console.Console.save_svg` via the ``theme`` parameter:: from rich.console import Console - from rich.terminal_themes import MONOKAI + from rich.terminal_theme import MONOKAI console = Console(record=True) console.save_svg("example.svg", theme=MONOKAI) +Alternatively, you can create your own themes by constructing a :class:`~rich.terminal_theme.TerminalTheme` instance +yourself and passing that in. + Error console ------------- From 77b167dd13e81ac8089cd8bc5c7bcd2f51afdac5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Mar 2022 15:59:36 +0100 Subject: [PATCH 25/29] Fixing a typo --- docs/source/console.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/console.rst b/docs/source/console.rst index c850e1183..0b0a36b83 100644 --- a/docs/source/console.rst +++ b/docs/source/console.rst @@ -281,7 +281,7 @@ You can customise the theme used during SVG export by importing the desired them console = Console(record=True) console.save_svg("example.svg", theme=MONOKAI) -Alternatively, you can create your own themes by constructing a :class:`~rich.terminal_theme.TerminalTheme` instance +Alternatively, you can create your own theme by constructing a :class:`~rich.terminal_theme.TerminalTheme` instance yourself and passing that in. Error console From 657fa36d4817b6cd34b2c9a8e13a4868fa73fe3a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Mon, 28 Mar 2022 16:05:59 +0100 Subject: [PATCH 26/29] Add note to changelog about SVG export functionality --- CHANGELOG.md | 4 ++++ docs/source/console.rst | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 603a939c9..f0151a5ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- SVG export functionality https://github.com/Textualize/rich/pull/2101 + ### Changed - Improve performance of cell_length https://github.com/Textualize/rich/pull/2061 diff --git a/docs/source/console.rst b/docs/source/console.rst index 0b0a36b83..585c2be38 100644 --- a/docs/source/console.rst +++ b/docs/source/console.rst @@ -281,7 +281,7 @@ You can customise the theme used during SVG export by importing the desired them console = Console(record=True) console.save_svg("example.svg", theme=MONOKAI) -Alternatively, you can create your own theme by constructing a :class:`~rich.terminal_theme.TerminalTheme` instance +Alternatively, you can create your own theme by constructing a :class:`rich.terminal_theme.TerminalTheme` instance yourself and passing that in. Error console From 33f9f9fccd219c7c634419b6b3b4273bd2a0432b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Mar 2022 16:19:31 +0100 Subject: [PATCH 27/29] Use CSS styling instead of inline styles on SVG export --- rich/__main__.py | 8 ++++++-- rich/console.py | 21 ++++++++++++++++----- tests/test_console.py | 7 ++++--- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/rich/__main__.py b/rich/__main__.py index 7b3ffb4f2..802bbdb01 100644 --- a/rich/__main__.py +++ b/rich/__main__.py @@ -51,7 +51,6 @@ def make_test_card() -> Table: 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" @@ -226,7 +225,12 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: console.print(test_card) taken = round((process_time() - start) * 1000.0, 1) - Console().print(test_card) + c = Console(record=True) + c.print(test_card) + c.save_svg( + path="/Users/darrenburns/Library/Application Support/JetBrains/PyCharm2021.3/scratches/scratch_7.svg", + title="Rich can export to SVG", + ) print(f"rendered in {pre_cache_taken}ms (cold cache)") print(f"rendered in {taken}ms (warm cache)") diff --git a/rich/console.py b/rich/console.py index 398c661af..8dea456cc 100644 --- a/rich/console.py +++ b/rich/console.py @@ -212,6 +212,7 @@ class NoChange: line-height: {line_height}px; padding: 14px; }} + {stylesheet} @@ -2346,9 +2347,13 @@ def export_svg( ) ) + styles: Dict[str, int] = {} fragments = [] theme_foreground_color = _theme.foreground_color.hex theme_background_color = _theme.background_color.hex + theme_default_foreground_css = f"color: {theme_foreground_color}; text-decoration-color: {theme_foreground_color};" + styles[theme_default_foreground_css] = 1 + for line in segment_lines: line_spans = [] for segment in line: @@ -2365,18 +2370,23 @@ def export_svg( # If the style doesn't contain a color, we still # need to make sure we output the default foreground color # from the TerminalTheme. - theme_default_foreground = f"color: {theme_foreground_color}; text-decoration-color: {theme_foreground_color};" - additional_styles = "" if not style.color: - additional_styles += theme_default_foreground + rule += "; " + theme_default_foreground_css - text = f'{text}' + style_number = styles.setdefault(rule, len(styles) + 1) + text = f'{text}' else: - text = f'{text}' + text = f'{text}' line_spans.append(text) fragments.append(f"
{''.join(line_spans)}
") + stylesheet_rules = [] + for style_rule, style_number in styles.items(): + if style_rule: + stylesheet_rules.append(f".r{style_number} {{{ style_rule }}}") + stylesheet = "\n".join(stylesheet_rules) + if clear: self._record_buffer.clear() @@ -2414,6 +2424,7 @@ def export_svg( font_size=font_size, line_height=line_height, title=title, + stylesheet=stylesheet, ) return rendered_code diff --git a/tests/test_console.py b/tests/test_console.py index 7ea32990f..962126213 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -547,6 +547,8 @@ def test_export_html_inline(): line-height: 22px; padding: 14px; } + .r1 {color: #f2f2f2; text-decoration-color: #f2f2f2;} +.r2 {font-weight: bold; color: #f2f2f2; text-decoration-color: #f2f2f2;} @@ -561,8 +563,8 @@ def test_export_html_inline():
Rich
-
foo Click
-
+
foo Click
+
@@ -576,7 +578,6 @@ def test_export_svg(): console = Console(record=True, width=100) console.print("[b]foo [blink][link=https://example.org]Click[/link][/]") svg = console.export_svg() - assert svg == EXPECTED_SVG From 0d2767463b5c247459f7a85e8906c0cac6f44403 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Mar 2022 16:47:18 +0100 Subject: [PATCH 28/29] Fix issues noted in code review, fix reverse style --- rich/console.py | 31 ++++++++++++++++++++++++------- tests/test_console.py | 15 ++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/rich/console.py b/rich/console.py index 8dea456cc..0c7d5862c 100644 --- a/rich/console.py +++ b/rich/console.py @@ -2310,6 +2310,7 @@ def save_html( def export_svg( self, + *, title: str = "Rich", theme: Optional[TerminalTheme] = None, clear: bool = True, @@ -2347,12 +2348,17 @@ def export_svg( ) ) - styles: Dict[str, int] = {} fragments = [] theme_foreground_color = _theme.foreground_color.hex theme_background_color = _theme.background_color.hex - theme_default_foreground_css = f"color: {theme_foreground_color}; text-decoration-color: {theme_foreground_color};" - styles[theme_default_foreground_css] = 1 + + theme_foreground_css = f"color: {theme_foreground_color}; text-decoration-color: {theme_foreground_color};" + theme_background_css = f"background-color: {theme_background_color};" + + theme_css = theme_foreground_css + theme_background_css + + styles: Dict[str, int] = {} + styles[theme_css] = 1 for line in segment_lines: line_spans = [] @@ -2360,7 +2366,7 @@ def export_svg( text, style, _ = segment text = escape(text) if style: - rule = style.get_html_style(_theme) + rules = style.get_html_style(_theme) if style.link: text = f'{text}' @@ -2370,10 +2376,21 @@ def export_svg( # If the style doesn't contain a color, we still # need to make sure we output the default foreground color # from the TerminalTheme. - if not style.color: - rule += "; " + theme_default_foreground_css + if not style.reverse: + foreground_css = theme_foreground_css + background_css = theme_background_css + else: + foreground_css = f"color: {theme_background_color}; text-decoration-color: {theme_background_color};" + background_css = ( + f"background-color: {theme_foreground_color};" + ) - style_number = styles.setdefault(rule, len(styles) + 1) + if style.color is None: + rules += f";{foreground_css}" + if style.bgcolor is None: + rules += f";{background_css}" + + style_number = styles.setdefault(rules, len(styles) + 1) text = f'{text}' else: text = f'{text}' diff --git a/tests/test_console.py b/tests/test_console.py index 962126213..82339a3b6 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -547,8 +547,9 @@ def test_export_html_inline(): line-height: 22px; padding: 14px; } - .r1 {color: #f2f2f2; text-decoration-color: #f2f2f2;} -.r2 {font-weight: bold; color: #f2f2f2; text-decoration-color: #f2f2f2;} + .r1 {color: #f2f2f2; text-decoration-color: #f2f2f2;background-color: #0c0c0c;} +.r2 {color: #2472c8; text-decoration-color: #2472c8; background-color: #cd3131; font-weight: bold} +.r3 {;color: #f2f2f2; text-decoration-color: #f2f2f2;;background-color: #0c0c0c;} @@ -563,7 +564,7 @@ def test_export_html_inline():
Rich
-
foo Click
+
foo Click
@@ -576,14 +577,18 @@ def test_export_html_inline(): def test_export_svg(): console = Console(record=True, width=100) - console.print("[b]foo [blink][link=https://example.org]Click[/link][/]") + console.print( + "[b red on blue reverse]foo[/] [blink][link=https://example.org]Click[/link]" + ) svg = console.export_svg() assert svg == EXPECTED_SVG def test_save_svg(): console = Console(record=True, width=100) - console.print("[b]foo [blink][link=https://example.org]Click[/link][/]") + console.print( + "[b red on blue reverse]foo[/] [blink][link=https://example.org]Click[/link]" + ) with tempfile.TemporaryDirectory() as path: export_path = os.path.join(path, "example.svg") console.save_svg(export_path) From 85200415757c9ba05e4891c1082274dd311a4f22 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 29 Mar 2022 17:14:08 +0100 Subject: [PATCH 29/29] Update SVG used in Rich docs --- docs/images/svg_export.svg | 765 ++++++++++++++++++++++++++++++++++--- rich/__main__.py | 8 +- 2 files changed, 707 insertions(+), 66 deletions(-) diff --git a/docs/images/svg_export.svg b/docs/images/svg_export.svg index 4d6d8479e..91c5efa3e 100644 --- a/docs/images/svg_export.svg +++ b/docs/images/svg_export.svg @@ -1,4 +1,4 @@ - @@ -105,69 +749,66 @@ -
Rich can export to SVG!
+
Rich can export to SVG
-
Rich features
-
-
Colors 4-bit color
-
8-bit color
-
Truecolor (16.7 million)
-
Dumb terminals
-
Automatic color conversion
-
-
Styles All ansi styles: bold, dim, italic, underline, strikethrough, reverse, and even blink.
-
-
Text Word wrap text. Justify left, center, right or full.
-
-
Lorem ipsum dolor sit Lorem ipsum dolor sit Lorem ipsum dolor sit Lorem ipsum dolor sit
-
amet, consectetur amet, consectetur amet, consectetur amet, consectetur
-
adipiscing elit. adipiscing elit. adipiscing elit. adipiscing elit. Quisque
-
Quisque in metus sed Quisque in metus sed Quisque in metus sed in metus sed sapien
-
sapien ultricies sapien ultricies sapien ultricies ultricies pretium a at
-
pretium a at justo. pretium a at justo. pretium a at justo. justo. Maecenas luctus
-
Maecenas luctus velit Maecenas luctus velit Maecenas luctus velit velit et auctor maximus.
-
et auctor maximus. et auctor maximus. et auctor maximus.
-
-
Asian 🇨🇳 该库支持中文,日文和韩文文本!
-
language 🇯🇵 ライブラリは中国語、日本語、韓国語のテキストをサポートしています
-
support 🇰🇷 이 라이브러리는 중국어, 일본어 및 한국어 텍스트를 지원합니다
-
-
Markup Rich supports a simple bbcode-like markup for color, style, and emoji! 👍 🍎 🐜 🐻 🥖 🚌
-
-
Tables Date Title Production Budget Box Office
-
─────────────────────────────────────────────────────────────────────────────────────────
-
Dec 20, 2019 Star Wars: The Rise of Skywalker $275,000,000 $375,126,118
-
May 25, 2018 Solo: A Star Wars Story $275,000,000 $393,151,347
-
Dec 15, 2017 Star Wars Ep. VIII: The Last Jedi $262,000,000 $1,332,539,889
-
May 19, 1999 Star Wars Ep. I: The phantom Menace $115,000,000 $1,027,044,677
-
-
Syntax 1 def iter_last(values: Iterable[T]) -> Ite {
-
highlighting 2 """Iterate and generate a tuple with 'foo': [
-
& 3 iter_values = iter(values) │ │ 3.1427,
-
pretty 4 try: │ │ (
-
printing 5 │ │ previous_value = next(iter_values │ │ │ 'Paul Atreides',
-
6 except StopIteration: │ │ │ 'Vladimir Harkonnen',
-
7 │ │ return │ │ │ 'Thufir Hawat'
-
8 for value in iter_values: │ │ )
-
9 │ │ yield False, previous_value ],
-
10 │ │ previous_value = value 'atomic': (False, True, None)
-
11 yield True, previous_value }
-
-
Markdown # Markdown ╔═══════════════════════════════════════════╗
-
Markdown
-
Supports much of the *markdown* __syntax__! ╚═══════════════════════════════════════════╝
-
-
- Headers Supports much of the markdown syntax!
-
- Basic formatting: **bold**, *italic*, `code`
-
- Block quotes Headers
-
- Lists, and more... Basic formatting: bold, italic, code
-
Block quotes
-
Lists, and more...
-
-
+more! Progress bars, columns, styled logging handler, tracebacks, etc...
-
-
+
Rich features
+
+
Colors 4-bit color
+
8-bit color
+
Truecolor (16.7 million)
+
Dumb terminals
+
Automatic color conversion
+
+
Styles All ansi styles: bold, dim, italic, underline, strikethrough, reverse, and even blink.
+
+
Text Word wrap text. Justify left, center, right or full.
+
+
Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet,
+
consectetur adipiscing elit. Quisque consectetur adipiscing elit. Quisque consectetur adipiscing elit. Quisque consectetur adipiscing elit. Quisque
+
in metus sed sapien ultricies pretium in metus sed sapien ultricies pretium in metus sed sapien ultricies in metus sed sapien ultricies pretium
+
a at justo. Maecenas luctus velit et a at justo. Maecenas luctus velit et pretium a at justo. Maecenas luctus a at justo. Maecenas luctus velit et
+
auctor maximus. auctor maximus. velit et auctor maximus. auctor maximus.
+
+
Asian 🇨🇳 该库支持中文,日文和韩文文本!
+
language 🇯🇵 ライブラリは中国語、日本語、韓国語のテキストをサポートしています
+
support 🇰🇷 이 라이브러리는 중국어, 일본어 및 한국어 텍스트를 지원합니다
+
+
Markup Rich supports a simple bbcode-like markup for color, style, and emoji! 👍 🍎 🐜 🐻 🥖 🚌
+
+
Tables Date Title Production Budget Box Office
+
─────────────────────────────────────────────────────────────────────────────────────────
+
Dec 20, 2019 Star Wars: The Rise of Skywalker $275,000,000 $375,126,118
+
May 25, 2018 Solo: A Star Wars Story $275,000,000 $393,151,347
+
Dec 15, 2017 Star Wars Ep. VIII: The Last Jedi $262,000,000 $1,332,539,889
+
May 19, 1999 Star Wars Ep. I: The phantom Menace $115,000,000 $1,027,044,677
+
+
Syntax 1 def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: {
+
highlighting 2 """Iterate and generate a tuple with a flag for last value.""" 'foo': [
+
& 3 iter_values = iter(values) │ │ 3.1427,
+
pretty 4 try: │ │ ('Paul Atreides', 'Vladimir Harkonnen', 'Thufir Hawat')
+
printing 5 │ │ previous_value = next(iter_values) ],
+
6 except StopIteration: 'atomic': (False, True, None)
+
7 │ │ return }
+
8 for value in iter_values:
+
9 │ │ yield False, previous_value
+
10 │ │ previous_value = value
+
11 yield True, previous_value
+
+
Markdown # Markdown ╔════════════════════════════════════════════════════════════════════════╗
+
Markdown
+
Supports much of the *markdown* __syntax__! ╚════════════════════════════════════════════════════════════════════════╝
+
+
- Headers Supports much of the markdown syntax!
+
- Basic formatting: **bold**, *italic*, `code`
+
- Block quotes Headers
+
- Lists, and more... Basic formatting: bold, italic, code
+
Block quotes
+
Lists, and more...
+
+
+more! Progress bars, columns, styled logging handler, tracebacks, etc...
+
+
diff --git a/rich/__main__.py b/rich/__main__.py index 802bbdb01..eb293a0ba 100644 --- a/rich/__main__.py +++ b/rich/__main__.py @@ -227,10 +227,10 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: c = Console(record=True) c.print(test_card) - c.save_svg( - path="/Users/darrenburns/Library/Application Support/JetBrains/PyCharm2021.3/scratches/scratch_7.svg", - title="Rich can export to SVG", - ) + # c.save_svg( + # path="/Users/darrenburns/Library/Application Support/JetBrains/PyCharm2021.3/scratches/svg_export.svg", + # title="Rich can export to SVG", + # ) print(f"rendered in {pre_cache_taken}ms (cold cache)") print(f"rendered in {taken}ms (warm cache)")