From 26504332ffb8840770b0209390200797d8715e70 Mon Sep 17 00:00:00 2001 From: CoderLuii Date: Wed, 17 Jun 2026 20:26:30 -0400 Subject: [PATCH] fix: preserve terminal glyph rendering --- README.md | 10 ++++++++++ src/index.ts | 17 +++++++++++------ src/server.ts | 17 ++++++++++++++--- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 02a528d..49df479 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,16 @@ A full-featured terminal plugin for [CloudCLI UI](https://github.com/cloudcli-ai - **Unicode 11** — full emoji and wide-character support - **Auto-resize** — terminal reflows when the panel is resized +## Rendering Fallback + +The terminal uses WebGL by default. If a browser/driver combination renders +box drawing, emoji, or CJK text as black squares, disable WebGL for that browser +profile and reopen the terminal: + +```js +localStorage.setItem('web-terminal-disable-webgl', 'true') +``` + ## Installation **From CloudCLI UI (recommended):** diff --git a/src/index.ts b/src/index.ts index e8aadac..9257419 100644 --- a/src/index.ts +++ b/src/index.ts @@ -114,6 +114,9 @@ const THEMES: Record = { // ── Persistent prefs ────────────────────────────────────────────────────────── const PREFS_KEY = 'web-terminal-prefs'; +const WEBGL_DISABLED_KEY = 'web-terminal-disable-webgl'; +const DEFAULT_FONT_FAMILY = '"Cascadia Mono", Consolas, "DejaVu Sans Mono", "Liberation Mono", "Noto Sans Mono", "Noto Sans Mono CJK JP", "Noto Sans CJK JP", "Microsoft YaHei", "MS Gothic", Meiryo, "PingFang SC", "Hiragino Sans GB", "Noto Color Emoji", Menlo, Monaco, "Courier New", monospace'; +function isWebglDisabled(): boolean { try { return localStorage.getItem(WEBGL_DISABLED_KEY) === 'true'; } catch { return false; } } function loadPrefs(): Partial { try { return JSON.parse(localStorage.getItem(PREFS_KEY) || '{}'); } catch { return {}; } } function savePrefs(p: Prefs): void { try { localStorage.setItem(PREFS_KEY, JSON.stringify(p)); } catch { /* ignore */ } } @@ -353,7 +356,7 @@ class TerminalSession { this.terminal = new opts.Terminal({ cursorBlink: true, fontSize: opts.prefs.fontSize || 14, - fontFamily: opts.prefs.fontFamily || "Menlo, Monaco, 'Courier New', monospace", + fontFamily: opts.prefs.fontFamily || DEFAULT_FONT_FAMILY, allowProposedApi: true, convertEol: true, scrollback: 10000, tabStopWidth: 4, macOptionIsMeta: true, macOptionClickForcesSelection: true, theme: THEMES[opts.prefs.theme || 'VS Dark'], @@ -368,11 +371,13 @@ class TerminalSession { this.terminal.open(this.el); - try { - const webgl = new opts.WebglAddon(); - webgl.onContextLoss(() => { try { webgl.dispose(); } catch { /* ignore */ } }); - this.terminal.loadAddon(webgl); - } catch { /* ignore */ } + if (!isWebglDisabled()) { + try { + const webgl = new opts.WebglAddon(); + webgl.onContextLoss(() => { try { webgl.dispose(); } catch { /* ignore */ } }); + this.terminal.loadAddon(webgl); + } catch { /* ignore */ } + } this.terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => { if (e.type !== 'keydown') return true; diff --git a/src/server.ts b/src/server.ts index f903f52..44680ab 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,7 +16,7 @@ interface PtyProcess { kill(): void; pause(): void; resume(): void; - onData(callback: (data: string) => void): void; + onData(callback: (data: string | Buffer) => void): void; onExit(callback: (event: { exitCode: number; signal?: number }) => void): void; spawn(shell: string, args: string[], opts: any): PtyProcess; } @@ -128,6 +128,7 @@ wss.on('connection', (ws: any) => { rows: 24, cwd, env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor', TERM_PROGRAM: 'web-terminal' }, + encoding: null, }); } catch (err) { safeSend(ws, { type: 'error', message: `Failed to spawn shell: ${(err as Error).message}` }); @@ -138,10 +139,20 @@ wss.on('connection', (ws: any) => { sessions.set(sessionId, { pty: ptyProc, ws }); safeSend(ws, { type: 'ready', sessionId, shell, cwd }); - ptyProc.onData((chunk: string) => { + const decoder = new TextDecoder('utf-8', { fatal: false }); + + ptyProc.onData((chunk: string | Buffer) => { + const text = typeof chunk === 'string' + ? chunk + : decoder.decode(chunk, { stream: true }); + + if (!text) { + return; + } + ptyProc.pause(); if (ws.readyState === WebSocket.OPEN) { - ws.send(chunk, () => ptyProc.resume()); + ws.send(text, () => ptyProc.resume()); } else { ptyProc.resume(); }