Replace the legacy terminal flow with a shared authenticated WebSocket runtime used across web, desktop, relay, and mobile surfaces. - introduce the v3 terminal protocol with scoped attachments, snapshots, ordered output, bounded replay history, reconnects, and explicit lifecycle - harden PTY creation, restart, resize, close, force-kill, idle cleanup, shell selection, login mode, environment sanitization, and appearance sync - add runtime-aware terminal APIs with relay authentication and Electron parity - add a fullscreen mobile terminal workspace with touch scrolling, long-press selection, safe-area controls, quick keys, and Ctrl/Alt input - add terminal selection attachments, preview detection, project actions, shell settings, and localized UI - harden Ghostty rendering, resize recovery, Unicode handling, block characters, line height, and stale-row behavior - remove the obsolete terminal SSE path and update reverse-proxy guidance - expand terminal runtime, transport, input, selection, and store coverage - avoid duplicate web builds when preparing mobile assets in root CI builds
53 lines
2.2 KiB
TypeScript
53 lines
2.2 KiB
TypeScript
// ghostty-web 0.4.0 leaves recycled rows dirty after default SGR resets (#138).
|
|
// Keep the theme background explicit until a stable release includes the upstream WASM fix.
|
|
const DEFAULT_BACKGROUND_RESETS = ['\u001b[0m', '\u001b[m'] as const;
|
|
|
|
const parseCssRgb = (color: string): [number, number, number] | null => {
|
|
const value = color.trim();
|
|
const hex = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(value)?.[1];
|
|
if (hex) {
|
|
const expanded = hex.length <= 4
|
|
? hex.slice(0, 3).split('').map((part) => part + part).join('')
|
|
: hex.slice(0, 6);
|
|
return [
|
|
Number.parseInt(expanded.slice(0, 2), 16),
|
|
Number.parseInt(expanded.slice(2, 4), 16),
|
|
Number.parseInt(expanded.slice(4, 6), 16),
|
|
];
|
|
}
|
|
|
|
const rgb = /^rgba?\(\s*(\d{1,3})\s*[, ]\s*(\d{1,3})\s*[, ]\s*(\d{1,3})(?:\s*[,/]\s*[\d.]+)?\s*\)$/i.exec(value);
|
|
if (!rgb) return null;
|
|
const channels = rgb.slice(1, 4).map(Number);
|
|
if (channels.some((channel) => channel < 0 || channel > 255)) return null;
|
|
return channels as [number, number, number];
|
|
};
|
|
|
|
export const getGhosttySafeResetSequence = (background: string): string | null => {
|
|
const rgb = parseCssRgb(background);
|
|
return rgb ? `\u001b[0;48;2;${rgb[0]};${rgb[1]};${rgb[2]}m` : null;
|
|
};
|
|
|
|
export const rewriteGhosttyDefaultBackgroundResets = (
|
|
data: string,
|
|
carry: string,
|
|
safeReset: string | null,
|
|
): { data: string; carry: string } => {
|
|
const combined = carry + data;
|
|
if (!safeReset) return { data: combined, carry: '' };
|
|
|
|
let carryLength = 0;
|
|
const maxPrefixLength = Math.max(...DEFAULT_BACKGROUND_RESETS.map((reset) => reset.length)) - 1;
|
|
for (let length = 1; length <= Math.min(maxPrefixLength, combined.length); length += 1) {
|
|
const suffix = combined.slice(-length);
|
|
if (DEFAULT_BACKGROUND_RESETS.some((reset) => reset.length > suffix.length && reset.startsWith(suffix))) {
|
|
carryLength = length;
|
|
}
|
|
}
|
|
|
|
const nextCarry = carryLength > 0 ? combined.slice(-carryLength) : '';
|
|
let output = carryLength > 0 ? combined.slice(0, -carryLength) : combined;
|
|
for (const reset of DEFAULT_BACKGROUND_RESETS) output = output.replaceAll(reset, safeReset);
|
|
return { data: output, carry: nextCarry };
|
|
};
|