| 1 | import { existsSync, readFileSync, renameSync, writeFileSync, mkdirSync } from "node:fs"; |
| 2 | import { dirname, join } from "node:path"; |
| 3 | |
| 4 | export type GraphicsOverride = "none" | "environment" | "command-line"; |
| 5 | export type GraphicsWarning = "invalid-config" | "unreadable-config" | "unsupported-version"; |
| 6 | export interface GraphicsSettingsState { |
| 7 | hardwareAcceleration: boolean; |
| 8 | startupEnabled: boolean; |
| 9 | override: GraphicsOverride; |
| 10 | restartRequired: boolean; |
| 11 | writable: boolean; |
| 12 | warning: GraphicsWarning | null; |
| 13 | } |
| 14 | export interface GraphicsBootstrap { |
| 15 | state: GraphicsSettingsState; |
| 16 | shouldDisable: boolean; |
| 17 | configPath: string; |
| 18 | } |
| 19 | |
| 20 | type Stored = { version: 1; hardwareAcceleration: boolean; [key: string]: unknown }; |
| 21 | |
| 22 | function parseStored(path: string): { value: Stored | null; warning: GraphicsWarning | null } { |
| 23 | if (!existsSync(path)) return { value: null, warning: null }; |
| 24 | try { |
| 25 | const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>; |
| 26 | if (parsed.version !== 1) return { value: null, warning: "unsupported-version" }; |
| 27 | if (typeof parsed.hardwareAcceleration !== "boolean") return { value: null, warning: "invalid-config" }; |
| 28 | return { value: parsed as Stored, warning: null }; |
| 29 | } catch { |
| 30 | return { value: null, warning: "unreadable-config" }; |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | export function loadGraphicsBootstrap(dataHome: string, env: NodeJS.ProcessEnv, argv: readonly string[]): GraphicsBootstrap { |
| 35 | // `dataHome/desktop-shell` is Electron's userData profile (set by |
| 36 | // claimShellInstance), so keep the preference directly in that profile. |
| 37 | const configPath = join(dataHome, "graphics.json"); |
| 38 | const parsed = parseStored(configPath); |
| 39 | const hardwareAcceleration = parsed.value?.hardwareAcceleration ?? true; |
| 40 | const environmentOverride = env.REASONIX_DISABLE_GPU === "1"; |
| 41 | const commandLineOverride = argv.includes("--disable-gpu"); |
| 42 | const override: GraphicsOverride = environmentOverride ? "environment" : commandLineOverride ? "command-line" : "none"; |
| 43 | const startupEnabled = override === "none" ? hardwareAcceleration : false; |
| 44 | return { |
| 45 | configPath, |
| 46 | shouldDisable: !startupEnabled, |
| 47 | state: { |
| 48 | hardwareAcceleration, |
| 49 | startupEnabled, |
| 50 | override, |
| 51 | restartRequired: override === "none" && hardwareAcceleration !== startupEnabled, |
| 52 | writable: parsed.warning !== "unsupported-version", |
| 53 | warning: parsed.warning, |
| 54 | }, |
| 55 | }; |
| 56 | } |
| 57 | |
| 58 | export class GraphicsSettingsStore { |
| 59 | private stored: Stored | null; |
| 60 | constructor(private readonly path: string, bootstrap: GraphicsBootstrap) { |
| 61 | this.stored = parseStored(path).value ?? { version: 1, hardwareAcceleration: bootstrap.state.hardwareAcceleration }; |
| 62 | this.state = bootstrap.state; |
| 63 | } |
| 64 | private state: GraphicsSettingsState; |
| 65 | private writeQueue: Promise<void> = Promise.resolve(); |
| 66 | get current(): GraphicsSettingsState { return this.state; } |
| 67 | async setHardwareAcceleration(enabled: boolean): Promise<GraphicsSettingsState> { |
| 68 | if (typeof enabled !== "boolean") throw new Error("hardwareAcceleration must be boolean"); |
| 69 | if (!this.state.writable) throw new Error("graphics settings are read-only"); |
| 70 | const next: Stored = { ...this.stored, version: 1, hardwareAcceleration: enabled }; |
| 71 | const write = this.writeQueue.catch(() => undefined).then(() => { |
| 72 | mkdirSync(dirname(this.path), { recursive: true }); |
| 73 | const existing = parseStored(this.path); |
| 74 | if (existing.warning) { |
| 75 | let backup = `${this.path}.invalid`; |
| 76 | let suffix = 1; |
| 77 | while (existsSync(backup)) backup = `${this.path}.invalid.${suffix++}`; |
| 78 | renameSync(this.path, backup); |
| 79 | } |
| 80 | const temp = `${this.path}.tmp`; |
| 81 | writeFileSync(temp, `${JSON.stringify(next)}\n`, "utf8"); |
| 82 | renameSync(temp, this.path); |
| 83 | }); |
| 84 | this.writeQueue = write.catch(() => undefined); |
| 85 | await write; |
| 86 | this.stored = next; |
| 87 | this.state = { ...this.state, hardwareAcceleration: enabled, restartRequired: this.state.override === "none" && enabled !== this.state.startupEnabled, warning: null }; |
| 88 | return this.state; |
| 89 | } |
| 90 | } |
| 91 |