fix: terminal rendering and dashboard issues
- Fix escape sequence display: filter DA1 responses that can be split across socket reads - Fix font rendering: use ghostty-web renderer API (setFontFamily/remeasureFont) - Fix sparklines: update slug-to-service mapping when containers are added/removed - Improve typeahead thumbnails: increase to 96x72px (4:3 ratio)
This commit is contained in:
@@ -6,6 +6,7 @@ import asyncio
|
|||||||
import contextlib
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import socket
|
import socket
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -27,6 +28,10 @@ REPLAY_BUFFER_SIZE = 256 * 1024 # 256KB
|
|||||||
DEFAULT_SCREEN_WIDTH = 132
|
DEFAULT_SCREEN_WIDTH = 132
|
||||||
DEFAULT_SCREEN_HEIGHT = 45
|
DEFAULT_SCREEN_HEIGHT = 45
|
||||||
|
|
||||||
|
# Pattern to filter out terminal device attribute responses that cause display issues
|
||||||
|
# These are responses to queries that shouldn't be displayed as text
|
||||||
|
DA_RESPONSE_PATTERN = re.compile(rb'\x1b\[\?[\d;]+c')
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class DockerExecSpec:
|
class DockerExecSpec:
|
||||||
@@ -173,8 +178,8 @@ class DockerExecSession(Session):
|
|||||||
sock.close()
|
sock.close()
|
||||||
detail = body.decode("utf-8", errors="replace")
|
detail = body.decode("utf-8", errors="replace")
|
||||||
raise RuntimeError(f"Docker API exec start failed ({status}): {detail}")
|
raise RuntimeError(f"Docker API exec start failed ({status}): {detail}")
|
||||||
if body:
|
# Don't save body from HTTP upgrade - it contains protocol handshake data,
|
||||||
self._pending_output += body
|
# not real terminal output (e.g., device attribute responses like "\x1b[?1;10;0c")
|
||||||
sock.settimeout(None)
|
sock.settimeout(None)
|
||||||
return sock
|
return sock
|
||||||
|
|
||||||
@@ -297,6 +302,11 @@ class DockerExecSession(Session):
|
|||||||
data = await queue.get()
|
data = await queue.get()
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
|
# Filter out device attribute responses that can cause display issues
|
||||||
|
# when split across socket reads
|
||||||
|
data = DA_RESPONSE_PATTERN.sub(b'', data)
|
||||||
|
if not data:
|
||||||
|
continue
|
||||||
await self._add_to_replay_buffer(data)
|
await self._add_to_replay_buffer(data)
|
||||||
await self._update_screen(data)
|
await self._update_screen(data)
|
||||||
if self._connector:
|
if self._connector:
|
||||||
|
|||||||
@@ -360,12 +360,19 @@ class LocalServer:
|
|||||||
def _on_docker_container_added(self, slug: str, name: str, command: str) -> None:
|
def _on_docker_container_added(self, slug: str, name: str, command: str) -> None:
|
||||||
"""Callback when a Docker container is added."""
|
"""Callback when a Docker container is added."""
|
||||||
log.info("Container added to dashboard: %s -> %s", name, slug)
|
log.info("Container added to dashboard: %s -> %s", name, slug)
|
||||||
|
# Update slug-to-service mapping for sparklines
|
||||||
|
if self._docker_stats:
|
||||||
|
self._slug_to_service[slug] = name
|
||||||
|
log.debug("Added sparkline mapping: %s -> %s", slug, name)
|
||||||
# Notify SSE subscribers about dashboard change
|
# Notify SSE subscribers about dashboard change
|
||||||
self._notify_activity("__dashboard__")
|
self._notify_activity("__dashboard__")
|
||||||
|
|
||||||
def _on_docker_container_removed(self, slug: str) -> None:
|
def _on_docker_container_removed(self, slug: str) -> None:
|
||||||
"""Callback when a Docker container is removed."""
|
"""Callback when a Docker container is removed."""
|
||||||
log.info("Container removed from dashboard: %s", slug)
|
log.info("Container removed from dashboard: %s", slug)
|
||||||
|
# Remove slug-to-service mapping
|
||||||
|
if self._docker_stats and slug in self._slug_to_service:
|
||||||
|
del self._slug_to_service[slug]
|
||||||
# Invalidate any cached screenshots
|
# Invalidate any cached screenshots
|
||||||
self._screenshot_cache.pop(slug, None)
|
self._screenshot_cache.pop(slug, None)
|
||||||
self._screenshot_cache_etag.pop(slug, None)
|
self._screenshot_cache_etag.pop(slug, None)
|
||||||
@@ -769,7 +776,7 @@ class LocalServer:
|
|||||||
.floating-results .search-query {{ font-size: 18px; font-weight: bold; color: #3b82f6; }}
|
.floating-results .search-query {{ font-size: 18px; font-weight: bold; color: #3b82f6; }}
|
||||||
.floating-results .result-item {{ display: flex; align-items: center; gap: 12px; padding: 12px; margin: 6px 0; border: 1px solid #334155; border-radius: 6px; cursor: pointer; transition: all 0.15s; }}
|
.floating-results .result-item {{ display: flex; align-items: center; gap: 12px; padding: 12px; margin: 6px 0; border: 1px solid #334155; border-radius: 6px; cursor: pointer; transition: all 0.15s; }}
|
||||||
.floating-results .result-item:hover, .floating-results .result-item.active {{ background: #334155; border-color: #3b82f6; }}
|
.floating-results .result-item:hover, .floating-results .result-item.active {{ background: #334155; border-color: #3b82f6; }}
|
||||||
.floating-results .result-thumb {{ width: 72px; height: 40px; flex: 0 0 auto; border-radius: 4px; border: 1px solid #334155; background: #0b1220; object-fit: contain; }}
|
.floating-results .result-thumb {{ width: 96px; height: 72px; flex: 0 0 auto; border-radius: 4px; border: 1px solid #334155; background: #0b1220; object-fit: contain; }}
|
||||||
.floating-results .result-content {{ display: flex; flex-direction: column; gap: 2px; }}
|
.floating-results .result-content {{ display: flex; flex-direction: column; gap: 2px; }}
|
||||||
.floating-results .result-title {{ font-weight: bold; margin-bottom: 4px; }}
|
.floating-results .result-title {{ font-weight: bold; margin-bottom: 4px; }}
|
||||||
.floating-results .result-meta {{ font-size: 12px; color: #94a3b8; }}
|
.floating-results .result-meta {{ font-size: 12px; color: #94a3b8; }}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -515,9 +515,12 @@ class WebTerminal {
|
|||||||
// Wait for fonts to load before fitting to ensure correct measurements
|
// Wait for fonts to load before fitting to ensure correct measurements
|
||||||
this.waitForFonts().then(() => {
|
this.waitForFonts().then(() => {
|
||||||
console.log("[webterm:init] Fonts loaded, reapplying font family and fitting...");
|
console.log("[webterm:init] Fonts loaded, reapplying font family and fitting...");
|
||||||
this.terminal.options.fontFamily = this.fontFamily;
|
// Use renderer's setFontFamily method to properly update fonts
|
||||||
if (typeof (this.terminal as unknown as { loadFonts?: () => void }).loadFonts === "function") {
|
const renderer = (this.terminal as unknown as { renderer?: { setFontFamily: (family: string) => void; remeasureFont: () => void } }).renderer;
|
||||||
(this.terminal as unknown as { loadFonts: () => void }).loadFonts();
|
if (renderer) {
|
||||||
|
renderer.setFontFamily(this.fontFamily);
|
||||||
|
renderer.remeasureFont();
|
||||||
|
console.log("[webterm:init] Font family updated via renderer");
|
||||||
}
|
}
|
||||||
this.fit();
|
this.fit();
|
||||||
console.log("[webterm:init] fit() completed");
|
console.log("[webterm:init] fit() completed");
|
||||||
|
|||||||
Reference in New Issue
Block a user