desktop/window-chrome.ts
import { readFileSync, writeFileSync } from "node:fs";
// Window controls outside macOS: the app's drawn traffic lights, or the
// operating system's own caption buttons over the top-right corner.
export type WindowControls = "mac" | "windows";
/** The saved window-controls style. Anything missing or unreadable is the drawn lights. */
export function readWindowControls(file: string): WindowControls {
try {
const saved: unknown = JSON.parse(readFileSync(file, "utf8"));
return saved && typeof saved === "object" && (saved as { controls?: unknown }).controls === "windows" ? "windows" : "mac";
} catch {
return "mac";
}
}
export function writeWindowControls(file: string, controls: WindowControls) {
writeFileSync(file, JSON.stringify({ controls }));
}
const hex = /^#[\da-f]{6}$/i;
/** Caption-button colors from the renderer: the pane surface and its text, as #rrggbb. */
export function overlayColors(input: unknown): { color: string; symbolColor: string } | null {
if (!input || typeof input !== "object") return null;
const { color, symbolColor } = input as Record<string, unknown>;
return typeof color === "string" && typeof symbolColor === "string" && hex.test(color) && hex.test(symbolColor)
? { color, symbolColor } : null;
}
/** Where a keyboard- or pointer-opened menu appears, in window coordinates. */
export function menuPosition(input: unknown): { x: number; y: number } | Record<string, never> {
if (!input || typeof input !== "object") return {};
const { x, y } = input as Record<string, unknown>;
return Number.isInteger(x) && Number.isInteger(y) && (x as number) >= 0 && (y as number) >= 0 && (x as number) < 20000 && (y as number) < 20000
? { x: x as number, y: y as number } : {};
}
export type NativeMenuItem = { id: string; label: string; enabled: boolean; accelerator?: string; separator?: boolean };
// Display-only shortcut text that Electron accepts as an accelerator.
const accelerator = /^((Ctrl|Cmd|Alt|Shift)\+){0,3}([A-Z0-9]|Left|Right|Up|Down|Enter|[,./;'[\]\\])$/;
/** A mail context menu from the renderer, checked before it reaches the native menu. */
export function nativeMenuItems(input: unknown): NativeMenuItem[] | null {
if (!Array.isArray(input) || input.length === 0 || input.length > 20) return null;
const items: NativeMenuItem[] = [];
for (const item of input) {
if (!item || typeof item !== "object") return null;
const { id, label, enabled, accelerator: key, separator } = item as Record<string, unknown>;
if (typeof id !== "string" || !/^[a-z-]{1,40}$/.test(id) || typeof label !== "string" || !label || label.length > 80 ||
typeof enabled !== "boolean") return null;
items.push({
id, label, enabled,
...(typeof key === "string" && accelerator.test(key) ? { accelerator: key } : {}),
...(separator === true ? { separator: true } : {}),
});
}
return items;
}