feat: patch console to improve drop-shadow

This commit is contained in:
Daylin Morgan 2022-06-16 13:28:45 -05:00
parent 21600f0b17
commit 13f4e50281
7 changed files with 331 additions and 35 deletions

View file

@ -1,5 +1,5 @@
[flake8] [flake8]
ignore = E203, E266, E501, W503, F403 ignore = E203, E266, E501, W503, F403, C901
exclude = exclude =
.git, .git,
__pycache__, __pycache__,

View file

@ -64,7 +64,7 @@ theme-docs:
./scripts/theme-showcase-gen ./scripts/theme-showcase-gen
diff-docs: diff-docs:
./scripts/code_svg_format_diff.py >docs/rich-diff.md ./scripts/rich-diff > docs/rich-diff.md
svg-docs: svg-docs:
lolcat -F .5 -S 9 -f assets/logo.txt | yartsu -o assets/logo.svg lolcat -F .5 -S 9 -f assets/logo.txt | yartsu -o assets/logo.svg

View file

@ -4,14 +4,14 @@
## Versions ## Versions
- Rich: 12.4.4 - Rich: 12.4.4
- Yartsu: 22.6b2.dev2+gabdbc1d - Yartsu: 22.6b4.dev3+gddae53c.d20220617
## Diff ## CONSOLE_SVG_FORMAT Diff
```diff ```diff
--- ---
+++ +++
@@ -1,47 +1,43 @@ @@ -1,20 +1,17 @@
-<svg class="rich-terminal" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg"> -<svg class="rich-terminal" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal shadow" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg"> +<svg class="rich-terminal shadow" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io --> <!-- Generated with Rich https://www.textualize.io -->
@ -35,37 +35,58 @@
font-style: bold; font-style: bold;
font-weight: 700; font-weight: 700;
}} }}
- @@ -32,6 +29,10 @@
.{unique_id}-matrix {{
font-family: Fira Code, monospace;
font-size: {char_height}px;
line-height: {line_height}px;
font-variant-east-asian: full-width;
}}
-
.{unique_id}-title {{
font-size: 18px;
font-weight: bold;
font-family: arial; font-family: arial;
}} }}
-
+ .shadow {{ + .shadow {{
+ -webkit-filter: drop-shadow( 5px 10px 15px rgba(0, 0, 0, .7)); + -webkit-filter: drop-shadow( 2px 5px 2px rgba(0, 0, 0, .7));
+ filter: drop-shadow( 5px 10px 15px rgba(0, 0, 0, .7)); + filter: drop-shadow( 2px 5px 2px rgba(0, 0, 0, .7));
+ }} + }}
{styles} {styles}
</style> </style>
-
<defs> @@ -43,7 +44,7 @@
<clipPath id="{unique_id}-clip-terminal">
<rect x="0" y="0" width="{terminal_width}" height="{terminal_height}" />
</clipPath>
{lines}
</defs> </defs>
-
{chrome} {chrome}
<g transform="translate({terminal_x}, {terminal_y})" clip-path="url(#{unique_id}-clip-terminal)"> - <g transform="translate({terminal_x}, {terminal_y})" clip-path="url(#{unique_id}-clip-terminal)">
+ <g transform="translate({terminal_x}, {terminal_y}) scale(.95)" clip-path="url(#{unique_id}-clip-terminal)">
{backgrounds} {backgrounds}
<g class="{unique_id}-matrix">
{matrix}
```
## Console.export_svg Diff
```diff
---
+++
@@ -64,9 +64,9 @@
line_height = char_height * 1.22
margin_top = 1
- margin_right = 1
- margin_bottom = 1
- margin_left = 1
+ margin_right = char_width * 5 / 6
+ margin_bottom = 20 * 5 / 3
+ margin_left = char_width * 5 / 6
padding_top = 40
padding_right = 8
@@ -214,8 +214,8 @@
x=terminal_width // 2,
y=margin_top + char_height + 6,
)
- chrome += f"""
- <g transform="translate(26,22)">
+ chrome += """
+ <g transform="translate(32,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
``` ```

View file

@ -1,10 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import inspect
from importlib.metadata import version from importlib.metadata import version
from rich._export_format import CONSOLE_SVG_FORMAT as DEFAULT from rich._export_format import CONSOLE_SVG_FORMAT as DEFAULT
from rich.console import Console as RichConsole
from yartsu._export_format import CONSOLE_SVG_FORMAT as MODIFIED from yartsu._export_format import CONSOLE_SVG_FORMAT as MODIFIED
from yartsu.console import Console as YartsuConsole
MARKDOWN_DOC = """ MARKDOWN_DOC = """
# Deviation From Rich # Deviation From Rich
@ -14,10 +17,16 @@ MARKDOWN_DOC = """
- Rich: {rich_version} - Rich: {rich_version}
- Yartsu: {yartsu_version} - Yartsu: {yartsu_version}
## Diff ## CONSOLE_SVG_FORMAT Diff
```diff ```diff
{diff} {export_format_diff}
```
## Console.export_svg Diff
```diff
{export_svg_diff}
``` ```
AUTO-GENERATED by code_svg_format_diff.py""" AUTO-GENERATED by code_svg_format_diff.py"""
@ -45,8 +54,12 @@ def main():
MARKDOWN_DOC.format( MARKDOWN_DOC.format(
rich_version=version("rich"), rich_version=version("rich"),
yartsu_version=version("yartsu"), yartsu_version=version("yartsu"),
diff=unidiff_output(DEFAULT, MODIFIED), export_format_diff=unidiff_output(DEFAULT, MODIFIED),
) export_svg_diff=unidiff_output(
inspect.getsource(RichConsole.export_svg),
inspect.getsource(YartsuConsole.export_svg),
),
),
) )

View file

@ -16,31 +16,36 @@ CONSOLE_SVG_FORMAT = """\
font-style: bold; font-style: bold;
font-weight: 700; font-weight: 700;
}} }}
.{unique_id}-matrix {{ .{unique_id}-matrix {{
font-family: Fira Code, monospace; font-family: Fira Code, monospace;
font-size: {char_height}px; font-size: {char_height}px;
line-height: {line_height}px; line-height: {line_height}px;
font-variant-east-asian: full-width; font-variant-east-asian: full-width;
}} }}
.{unique_id}-title {{ .{unique_id}-title {{
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
font-family: arial; font-family: arial;
}} }}
.shadow {{ .shadow {{
-webkit-filter: drop-shadow( 5px 10px 15px rgba(0, 0, 0, .7)); -webkit-filter: drop-shadow( 2px 5px 2px rgba(0, 0, 0, .7));
filter: drop-shadow( 5px 10px 15px rgba(0, 0, 0, .7)); filter: drop-shadow( 2px 5px 2px rgba(0, 0, 0, .7));
}} }}
{styles} {styles}
</style> </style>
<defs> <defs>
<clipPath id="{unique_id}-clip-terminal"> <clipPath id="{unique_id}-clip-terminal">
<rect x="0" y="0" width="{terminal_width}" height="{terminal_height}" /> <rect x="0" y="0" width="{terminal_width}" height="{terminal_height}" />
</clipPath> </clipPath>
{lines} {lines}
</defs> </defs>
{chrome} {chrome}
<g transform="translate({terminal_x}, {terminal_y})" clip-path="url(#{unique_id}-clip-terminal)"> <g transform="translate({terminal_x}, {terminal_y}) scale(.95)" clip-path="url(#{unique_id}-clip-terminal)">
{backgrounds} {backgrounds}
<g class="{unique_id}-matrix"> <g class="{unique_id}-matrix">
{matrix} {matrix}

View file

@ -5,13 +5,13 @@ from argparse import SUPPRESS, FileType, Namespace
from pathlib import Path from pathlib import Path
from rich.__main__ import make_test_card from rich.__main__ import make_test_card
from rich.console import Console
from rich.text import Text from rich.text import Text
from ._export_format import CONSOLE_SVG_FORMAT from ._export_format import CONSOLE_SVG_FORMAT
from ._run_cmd import run_cmd from ._run_cmd import run_cmd
from ._version import __version__ from ._version import __version__
from .argparse import ArgumentParser from .argparse import ArgumentParser
from .console import Console
from .term import term from .term import term
from .themes import THEMES from .themes import THEMES

257
yartsu/console.py Normal file
View file

@ -0,0 +1,257 @@
import zlib
from html import escape
from math import ceil
from typing import Dict, List, Optional
from rich.color import blend_rgb
from rich.console import Console as RichConsole
from rich.segment import Segment
from rich.style import Style
from rich.terminal_theme import SVG_EXPORT_THEME, TerminalTheme
from ._export_format import CONSOLE_SVG_FORMAT
class Console(RichConsole):
def export_svg(
self,
*,
title: str = "Rich",
theme: Optional[TerminalTheme] = None,
clear: bool = True,
code_format: str = CONSOLE_SVG_FORMAT,
) -> str:
"""
Generate an SVG 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``
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.
"""
from rich.cells import cell_len
style_cache: Dict[Style, str] = {}
def get_svg_style(style: Style) -> str:
"""Convert a Style to CSS rules for SVG."""
if style in style_cache:
return style_cache[style]
css_rules = []
color = (
_theme.foreground_color
if (style.color is None or style.color.is_default)
else style.color.get_truecolor(_theme)
)
bgcolor = (
_theme.background_color
if (style.bgcolor is None or style.bgcolor.is_default)
else style.bgcolor.get_truecolor(_theme)
)
if style.reverse:
color, bgcolor = bgcolor, color
if style.dim:
color = blend_rgb(color, bgcolor, 0.4)
css_rules.append(f"fill: {color.hex}")
if style.bold:
css_rules.append("font-weight: bold")
if style.italic:
css_rules.append("font-style: italic;")
if style.underline:
css_rules.append("text-decoration: underline;")
if style.strike:
css_rules.append("text-decoration: line-through;")
css = ";".join(css_rules)
style_cache[style] = css
return css
_theme = theme or SVG_EXPORT_THEME
width = self.width
char_height = 20
char_width = char_height * 0.61
line_height = char_height * 1.22
margin_top = 1
margin_right = char_width * 5 / 6
margin_bottom = 20 * 5 / 3
margin_left = char_width * 5 / 6
padding_top = 40
padding_right = 8
padding_bottom = 8
padding_left = 8
padding_width = padding_left + padding_right
padding_height = padding_top + padding_bottom
margin_width = margin_left + margin_right
margin_height = margin_top + margin_bottom
text_backgrounds: List[str] = []
text_group: List[str] = []
classes: Dict[str, int] = {}
style_no = 1
def escape_text(text: str) -> str:
"""HTML escape text and replace spaces with nbsp."""
return escape(text).replace(" ", "&#160;")
def make_tag(
name: str, content: Optional[str] = None, **attribs: object
) -> str:
"""Make a tag from name, content, and attributes."""
def stringify(value: object) -> str:
if isinstance(value, (float)):
return format(value, "g")
return str(value)
tag_attribs = " ".join(
f'{k.lstrip("_").replace("_", "-")}="{stringify(v)}"'
for k, v in attribs.items()
)
return (
f"<{name} {tag_attribs}>{content}</{name}>"
if content
else f"<{name} {tag_attribs}/>"
)
with self._record_buffer_lock:
segments = list(Segment.filter_control(self._record_buffer))
if clear:
self._record_buffer.clear()
unique_id = "terminal-" + str(
zlib.adler32(
("".join(segment.text for segment in segments)).encode(
"utf-8", "ignore"
)
+ title.encode("utf-8", "ignore")
)
)
y = 0
for y, line in enumerate(Segment.split_and_crop_lines(segments, length=width)):
x = 0
for text, style, _control in line:
style = style or Style()
rules = get_svg_style(style)
if rules not in classes:
classes[rules] = style_no
style_no += 1
class_name = f"r{classes[rules]}"
if style.reverse:
has_background = True
background = (
_theme.foreground_color.hex
if style.color is None
else style.color.get_truecolor(_theme).hex
)
else:
bgcolor = style.bgcolor
has_background = bgcolor is not None and not bgcolor.is_default
background = (
_theme.background_color.hex
if style.bgcolor is None
else style.bgcolor.get_truecolor(_theme).hex
)
text_length = cell_len(text)
if has_background:
text_backgrounds.append(
make_tag(
"rect",
fill=background,
x=x * char_width,
y=y * line_height + 1.5,
width=char_width * text_length,
height=line_height + 0.25,
shape_rendering="crispEdges",
)
)
if text != " " * len(text):
text_group.append(
make_tag(
"text",
escape_text(text),
_class=f"{unique_id}-{class_name}",
x=x * char_width,
y=y * line_height + char_height,
textLength=char_width * len(text),
clip_path=f"url(#{unique_id}-line-{y})",
)
)
x += cell_len(text)
line_offsets = [line_no * line_height + 1.5 for line_no in range(y)]
lines = "\n".join(
f"""<clipPath id="{unique_id}-line-{line_no}">
{make_tag("rect", x=0, y=offset, width=char_width * width, height=line_height + 0.25)}
</clipPath>"""
for line_no, offset in enumerate(line_offsets)
)
styles = "\n".join(
f".{unique_id}-r{rule_no} {{ {css} }}" for css, rule_no in classes.items()
)
backgrounds = "".join(text_backgrounds)
matrix = "".join(text_group)
terminal_width = ceil(width * char_width + padding_width)
terminal_height = (y + 1) * line_height + padding_height
chrome = make_tag(
"rect",
fill=_theme.background_color.hex,
stroke="rgba(255,255,255,0.35)",
stroke_width="1",
x=margin_left,
y=margin_top,
width=terminal_width,
height=terminal_height,
rx=8,
)
title_color = _theme.foreground_color.hex
if title:
chrome += make_tag(
"text",
escape_text(title),
_class=f"{unique_id}-title",
fill=title_color,
text_anchor="middle",
x=terminal_width // 2,
y=margin_top + char_height + 6,
)
chrome += """
<g transform="translate(32,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
"""
svg = code_format.format(
unique_id=unique_id,
char_width=char_width,
char_height=char_height,
line_height=line_height,
terminal_width=char_width * width - 1,
terminal_height=(y + 1) * line_height - 1,
width=terminal_width + margin_width,
height=terminal_height + margin_height,
terminal_x=margin_left + padding_left,
terminal_y=margin_top + padding_top,
styles=styles,
chrome=chrome,
backgrounds=backgrounds,
matrix=matrix,
lines=lines,
)
return svg