Files
NetMesh/application/state/terminalFontSizeSync.ts

258 lines
8.0 KiB
TypeScript
Raw Normal View History

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' };
}