Enforce monospace in screenshots

This commit is contained in:
GitHub Copilot
2026-01-22 13:02:28 +00:00
parent f6d986fb8f
commit 557eafc163
2 changed files with 26 additions and 0 deletions
+24
View File
@@ -8,6 +8,7 @@ import hashlib
import io import io
import json import json
import logging import logging
import re
import signal import signal
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -37,6 +38,12 @@ SCREENSHOT_MAX_BYTES = 65536
SCREENSHOT_CACHE_SECONDS = 1.0 SCREENSHOT_CACHE_SECONDS = 1.0
SCREENSHOT_MAX_CACHE_SECONDS = 60.0 SCREENSHOT_MAX_CACHE_SECONDS = 60.0
SVG_MONO_FONT_STACK = (
'ui-monospace, "SFMono-Regular", "FiraCode Nerd Font", "FiraMono Nerd Font", '
'"Fira Code", "Roboto Mono", Menlo, Monaco, Consolas, "Liberation Mono", '
'"DejaVu Sans Mono", "Courier New", monospace'
)
WEBTERM_STATIC_PATH = Path(__file__).parent / "static" WEBTERM_STATIC_PATH = Path(__file__).parent / "static"
@@ -84,6 +91,22 @@ class LocalClientConnector(SessionConnector):
await self.server.handle_session_close(self.session_id, self.route_key) await self.server.handle_session_close(self.session_id, self.route_key)
def _rewrite_svg_fonts(svg: str) -> str:
"""Make Rich SVG output self-contained and aligned with our monospace styling."""
# Rich export_svg embeds @font-face rules that reference external CDNs.
svg = re.sub(r"@font-face\s*\{.*?\}\s*", "", svg, flags=re.DOTALL)
# Force our local monospace stack even if Rich sets font-family to Fira Code.
override = f"\ntext {{ font-family: {SVG_MONO_FONT_STACK} !important; }}\n"
if "</style>" in svg:
svg = svg.replace("</style>", override + "</style>", 1)
else:
svg = svg.replace("<svg ", f"<svg><style>{override}</style> ", 1)
return svg
def _apply_carriage_returns(text: str) -> list[str]: def _apply_carriage_returns(text: str) -> list[str]:
"""Interpret \r as 'return to start of line' (overwrite), not a newline. """Interpret \r as 'return to start of line' (overwrite), not a newline.
@@ -529,6 +552,7 @@ class LocalServer:
) )
svg = await asyncio.to_thread(_render_svg) svg = await asyncio.to_thread(_render_svg)
svg = _rewrite_svg_fonts(svg)
etag = hashlib.sha1(svg.encode("utf-8"), usedforsecurity=False).hexdigest() etag = hashlib.sha1(svg.encode("utf-8"), usedforsecurity=False).hexdigest()
self._screenshot_cache[route_key] = (asyncio.get_event_loop().time(), svg) self._screenshot_cache[route_key] = (asyncio.get_event_loop().time(), svg)
self._screenshot_cache_etag[route_key] = etag self._screenshot_cache_etag[route_key] = etag
+2
View File
@@ -239,6 +239,8 @@ class TestLocalServerHelpers:
response = await server._handle_screenshot(request) response = await server._handle_screenshot(request)
assert response.content_type == "image/svg+xml" assert response.content_type == "image/svg+xml"
assert "<svg" in response.text assert "<svg" in response.text
assert "ui-monospace" in response.text
assert "cdnjs.cloudflare.com" not in response.text
assert created["called"][0] == "known" assert created["called"][0] == "known"
assert created["called"][1:] == (132, 45) assert created["called"][1:] == (132, 45)