Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
258 lines
8.0 KiB
TypeScript
258 lines
8.0 KiB
TypeScript
import {
|
|
DEFAULT_FONT_SIZE,
|
|
MAX_FONT_SIZE,
|
|
MIN_FONT_SIZE,
|
|
} from '../../infrastructure/config/fonts';
|
|
|
|
export type TerminalFontSizeMutationSource = 'local' | 'incoming';
|
|
|
|
export type TerminalFontSizeRecord = {
|
|
fontSize: number;
|
|
version: number;
|
|
origin: string;
|
|
};
|
|
|
|
const LEGACY_TERMINAL_FONT_SIZE_ORIGIN = 'legacy';
|
|
|
|
function normalizeTerminalFontSizeOrigin(origin: unknown): string {
|
|
return typeof origin === 'string' && origin.length > 0
|
|
? origin
|
|
: LEGACY_TERMINAL_FONT_SIZE_ORIGIN;
|
|
}
|
|
|
|
export function clampTerminalFontSizeValue(fontSize: unknown): number {
|
|
const value = typeof fontSize === 'number' ? fontSize : Number(fontSize);
|
|
if (!Number.isFinite(value)) return DEFAULT_FONT_SIZE;
|
|
return Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, Math.round(value)));
|
|
}
|
|
|
|
/**
|
|
* Parse persisted / IPC terminal-font-size payloads.
|
|
* Accepts legacy plain numbers ("16") and versioned records.
|
|
*/
|
|
export function parseTerminalFontSizeRecord(raw: unknown): TerminalFontSizeRecord {
|
|
if (typeof raw === 'number' || typeof raw === 'string') {
|
|
const trimmed = typeof raw === 'string' ? raw.trim() : raw;
|
|
if (typeof trimmed === 'string' && trimmed.startsWith('{')) {
|
|
try {
|
|
return parseTerminalFontSizeRecord(JSON.parse(trimmed));
|
|
} catch {
|
|
// fall through to Number()
|
|
}
|
|
}
|
|
if (typeof trimmed === 'string') {
|
|
const [fontSizePart, versionPart, encodedOrigin, ...rest] = trimmed.split('|');
|
|
if (encodedOrigin && rest.length === 0) {
|
|
const fontSize = Number(fontSizePart);
|
|
const version = Number(versionPart);
|
|
if (Number.isFinite(fontSize) && Number.isFinite(version)) {
|
|
let origin = encodedOrigin;
|
|
try {
|
|
origin = decodeURIComponent(encodedOrigin);
|
|
} catch {
|
|
// Keep the encoded value as a stable tie-breaker.
|
|
}
|
|
return {
|
|
fontSize: clampTerminalFontSizeValue(fontSize),
|
|
version: version > 0 ? Math.floor(version) : 0,
|
|
origin: normalizeTerminalFontSizeOrigin(origin),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
const asNumber = Number(trimmed);
|
|
if (Number.isFinite(asNumber)) {
|
|
return {
|
|
fontSize: clampTerminalFontSizeValue(asNumber),
|
|
version: 0,
|
|
origin: LEGACY_TERMINAL_FONT_SIZE_ORIGIN,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (raw && typeof raw === 'object') {
|
|
const record = raw as { fontSize?: unknown; version?: unknown; origin?: unknown };
|
|
const fontSize = clampTerminalFontSizeValue(record.fontSize);
|
|
const version = Number(record.version);
|
|
return {
|
|
fontSize,
|
|
version: Number.isFinite(version) && version > 0 ? Math.floor(version) : 0,
|
|
origin: normalizeTerminalFontSizeOrigin(record.origin),
|
|
};
|
|
}
|
|
|
|
return {
|
|
fontSize: DEFAULT_FONT_SIZE,
|
|
version: 0,
|
|
origin: LEGACY_TERMINAL_FONT_SIZE_ORIGIN,
|
|
};
|
|
}
|
|
|
|
export function serializeTerminalFontSizeRecord(record: TerminalFontSizeRecord): string {
|
|
const fontSize = clampTerminalFontSizeValue(record.fontSize);
|
|
const version = Math.max(0, Math.floor(record.version) || 0);
|
|
const origin = encodeURIComponent(normalizeTerminalFontSizeOrigin(record.origin));
|
|
// Keep the font size first so older Netcatty versions using parseInt() can
|
|
// still read the preference after a downgrade.
|
|
return `${fontSize}|${version}|${origin}`;
|
|
}
|
|
|
|
export function createTerminalFontSizeSyncOrigin(): string {
|
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
}
|
|
|
|
export function nextTerminalFontSizeSyncVersion(
|
|
currentVersion: number,
|
|
persistedVersion: number,
|
|
now: number = Date.now(),
|
|
): number {
|
|
return Math.max(
|
|
Math.floor(now),
|
|
Math.floor(currentVersion) + 1,
|
|
Math.floor(persistedVersion) + 1,
|
|
);
|
|
}
|
|
|
|
export function createLocalTerminalFontSizeRecord(
|
|
current: TerminalFontSizeRecord,
|
|
persistedRaw: unknown,
|
|
nextFontSizeValue: unknown,
|
|
origin: string,
|
|
now: number = Date.now(),
|
|
): TerminalFontSizeRecord {
|
|
const nextFontSize = clampTerminalFontSizeValue(nextFontSizeValue);
|
|
if (nextFontSize === current.fontSize) return current;
|
|
const persisted = parseTerminalFontSizeRecord(persistedRaw);
|
|
return {
|
|
fontSize: nextFontSize,
|
|
version: nextTerminalFontSizeSyncVersion(current.version, persisted.version, now),
|
|
origin: normalizeTerminalFontSizeOrigin(origin),
|
|
};
|
|
}
|
|
|
|
export function areTerminalFontSizeRecordsEqual(
|
|
left: TerminalFontSizeRecord,
|
|
right: TerminalFontSizeRecord,
|
|
): boolean {
|
|
return left.fontSize === right.fontSize
|
|
&& left.version === right.version
|
|
&& left.origin === right.origin;
|
|
}
|
|
|
|
/**
|
|
* Incoming peer updates must not clobber a newer local revision. A stable
|
|
* origin tie-break makes simultaneous writers converge on the same record.
|
|
*/
|
|
export function shouldApplyTerminalFontSizeRecord(
|
|
current: TerminalFontSizeRecord,
|
|
incoming: TerminalFontSizeRecord,
|
|
): boolean {
|
|
if (incoming.version > current.version) return true;
|
|
if (incoming.version < current.version) return false;
|
|
if (
|
|
incoming.version === 0
|
|
&& incoming.origin === LEGACY_TERMINAL_FONT_SIZE_ORIGIN
|
|
&& current.origin === LEGACY_TERMINAL_FONT_SIZE_ORIGIN
|
|
) {
|
|
return incoming.fontSize !== current.fontSize;
|
|
}
|
|
if (incoming.origin !== current.origin) return incoming.origin > current.origin;
|
|
return incoming.fontSize > current.fontSize;
|
|
}
|
|
|
|
export type TerminalFontSizeStorageResolution = {
|
|
record: TerminalFontSizeRecord;
|
|
serializedRecord: string;
|
|
shouldAdopt: boolean;
|
|
shouldPersist: boolean;
|
|
};
|
|
|
|
export type TerminalFontSizeIncomingResolution = {
|
|
record: TerminalFontSizeRecord;
|
|
shouldUpdate: boolean;
|
|
repairSerializedRecord: string | null;
|
|
};
|
|
|
|
export function resolveTerminalFontSizeStorage(
|
|
current: TerminalFontSizeRecord,
|
|
storedRaw: unknown,
|
|
): TerminalFontSizeStorageResolution {
|
|
const stored = parseTerminalFontSizeRecord(storedRaw);
|
|
if (shouldApplyTerminalFontSizeRecord(current, stored)) {
|
|
return {
|
|
record: stored,
|
|
serializedRecord: serializeTerminalFontSizeRecord(stored),
|
|
shouldAdopt: true,
|
|
shouldPersist: false,
|
|
};
|
|
}
|
|
|
|
const serializedRecord = serializeTerminalFontSizeRecord(current);
|
|
return {
|
|
record: current,
|
|
serializedRecord,
|
|
shouldAdopt: false,
|
|
shouldPersist: storedRaw !== serializedRecord,
|
|
};
|
|
}
|
|
|
|
export function resolveAuthoritativeTerminalFontSizeStorage(
|
|
currentRef: Readonly<{ current: TerminalFontSizeRecord }>,
|
|
storedRaw: unknown,
|
|
): TerminalFontSizeStorageResolution {
|
|
return resolveTerminalFontSizeStorage(currentRef.current, storedRaw);
|
|
}
|
|
|
|
export function resolveIncomingTerminalFontSize(
|
|
current: TerminalFontSizeRecord,
|
|
incomingRaw: unknown,
|
|
storedRaw: unknown,
|
|
): TerminalFontSizeIncomingResolution {
|
|
const incoming = parseTerminalFontSizeRecord(incomingRaw);
|
|
if (shouldApplyTerminalFontSizeRecord(current, incoming)) {
|
|
return {
|
|
record: incoming,
|
|
shouldUpdate: true,
|
|
repairSerializedRecord: null,
|
|
};
|
|
}
|
|
|
|
const storageResolution = resolveTerminalFontSizeStorage(current, storedRaw);
|
|
if (storageResolution.shouldAdopt) {
|
|
return {
|
|
record: storageResolution.record,
|
|
shouldUpdate: true,
|
|
repairSerializedRecord: null,
|
|
};
|
|
}
|
|
|
|
return {
|
|
record: current,
|
|
shouldUpdate: false,
|
|
repairSerializedRecord: storageResolution.shouldPersist
|
|
? storageResolution.serializedRecord
|
|
: null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decide whether a terminal-font-size state change should be rebroadcast to
|
|
* peer windows. Incoming IPC/storage updates must not notify again —
|
|
* otherwise +/- clicks in the Settings window ping-pong with the main window
|
|
* and terminals oscillate between the last two sizes (see #2689, same class
|
|
* as window-opacity #2018).
|
|
*/
|
|
export function shouldBroadcastTerminalFontSizeChange(
|
|
mutationSource: TerminalFontSizeMutationSource,
|
|
persistMounted: boolean,
|
|
): { shouldBroadcast: boolean; nextSource: TerminalFontSizeMutationSource } {
|
|
if (mutationSource === 'incoming') {
|
|
return { shouldBroadcast: false, nextSource: 'local' };
|
|
}
|
|
return { shouldBroadcast: persistMounted, nextSource: 'local' };
|
|
}
|