diff --git a/Screenshot 2026-01-24 at 20.02.12.png b/Screenshot 2026-01-24 at 20.02.12.png new file mode 100644 index 0000000..49ca9b5 Binary files /dev/null and b/Screenshot 2026-01-24 at 20.02.12.png differ diff --git a/pyproject.toml b/pyproject.toml index bf3a4b7..924ce5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual-webterm" -version = "0.3.13" +version = "0.3.14" description = "Serve terminal sessions over the web" authors = ["Will McGugan "] license = "MIT" diff --git a/src/textual_webterm/svg_exporter.py b/src/textual_webterm/svg_exporter.py index 8549ee0..a56bfd3 100644 --- a/src/textual_webterm/svg_exporter.py +++ b/src/textual_webterm/svg_exporter.py @@ -50,6 +50,25 @@ FONT_SIZE = 14 LINE_HEIGHT = 1.2 CHAR_WIDTH = 8 # Width of monospace character at 14px (typically ~0.57 ratio) +# Box drawing characters that need vertical scaling to fill line height +# These are designed to connect between lines but the font's em-box is smaller +# than our line height, creating gaps +BOX_DRAWING_CHARS = frozenset( + # Light and heavy box drawing (U+2500-U+257F) + "─━│┃┄┅┆┇┈┉┊┋┌┍┎┏┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿╀╁╂╃╄╅╆╇╈╉╊╋" + # Double box drawing + "═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬" + # Rounded corners + "╭╮╯╰" + # Light and heavy dashed (U+2571-U+257F) + "\u2571\u2572\u2573╴╵╶╷╸╹╺╻╼╽╾╿" +) + + +def _is_box_drawing(char: str) -> bool: + """Check if character is a box-drawing character that needs scaling.""" + return len(char) == 1 and char in BOX_DRAWING_CHARS + class CharData(TypedDict): """Character data from pyte screen buffer.""" @@ -232,7 +251,20 @@ def render_terminal_svg( if classes: attrs.append(f'class="{" ".join(classes)}"') - row_tspans.append(f'{_escape_xml(char_data)}') + # Box-drawing characters need vertical scaling to fill line height + # Render them as separate text elements with transform + if _is_box_drawing(char_data): + # Scale vertically by line_height ratio, anchored at top of cell + # The transform scales around (x, rect_y) to stretch the glyph + fill_attr = f' fill="{fg}"' if fg != foreground else "" + class_attr = f' class="{" ".join(classes)}"' if classes else "" + row_bg_rects.append( + f'{_escape_xml(char_data)}' + ) + else: + row_tspans.append(f'{_escape_xml(char_data)}') col += char_cols diff --git a/tests/test_svg_exporter.py b/tests/test_svg_exporter.py index 2ec9b04..9a6bca2 100644 --- a/tests/test_svg_exporter.py +++ b/tests/test_svg_exporter.py @@ -328,6 +328,22 @@ class TestRenderTerminalSvg: # Text tspan with yellow assert f'fill="{ANSI_COLORS["yellow"]}"' in svg + def test_box_drawing_vertical_scale(self) -> None: + """Box-drawing characters are scaled vertically to fill line height.""" + buffer = [[self._char("│")]] # Vertical line + svg = render_terminal_svg(buffer, width=80, height=24) + # Box drawing chars rendered with transform for vertical scaling + assert 'scale(1,1.2)' in svg + # Should be a separate text element, not a tspan + assert ' None: """Unicode text is preserved.""" buffer = self._make_buffer(["你好世界"])