diff --git a/install-dev.sh b/install-dev.sh index 4ab30a5..7d92f56 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -46,7 +46,7 @@ sudo tee "$GREETD_CONFIG" >/dev/null </dev/null < void; @@ -243,7 +244,8 @@ export async function createBootSequenceUI( if (!loggedCompletion) { loggedCompletion = true; console.info("[debug] [bootsequence/questions] finished questions", answers); - writeFileSync(join(homedir(), ".deltaboot.json"), JSON.stringify(answers)) + writeFileSync(join(homedir(), ".deltaboot.json"), JSON.stringify(answers)); + writeLock(answers); } return; } diff --git a/src/desktop.ts b/src/desktop.ts index 916b8f8..372a2dd 100644 --- a/src/desktop.ts +++ b/src/desktop.ts @@ -6,6 +6,12 @@ const KNOWN_SESSIONS: Record = { hyprland: "Hyprland", plasma: "startplasma-wayland" }; + +export const KNOWN_SESSIONS_FRIENDLY: Record = { + "hyprland": "Hyprland", + "plasma": "KDE" +}; + const GREETD_TIMEOUT_MS = Number(process.env.GREETD_TIMEOUT_MS ?? 5_000); export async function handoffToGreetd(desktopHint?: string): Promise { diff --git a/src/renderers/recoverymenu/index.ts b/src/renderers/recoverymenu/index.ts index 90778cd..a2f58cc 100644 --- a/src/renderers/recoverymenu/index.ts +++ b/src/renderers/recoverymenu/index.ts @@ -12,6 +12,8 @@ import { measureTextWidth } from "../../bootsequence/font"; import type { RendererInstance, RendererProps } from "../types"; +import { clearLock, readLock, type DeviceContactLock } from "../../ui/lock"; +import { handoffToGreetd } from "../../desktop"; /* ---------------- */ @@ -58,6 +60,7 @@ type RecoveryMenuProps = { question?: string; yesLabel?: string; noLabel?: string; + restoreSession?: DeviceContactLock }; /* ---------------- */ @@ -107,7 +110,8 @@ export function createRecoveryMenuRenderer( ? props.question : "????????/?", yesLabel: typeof props.yesLabel === "string" ? props.yesLabel : "Yes", - noLabel: typeof props.noLabel === "string" ? props.noLabel : "No" + noLabel: typeof props.noLabel === "string" ? props.noLabel : "No", + restoreSession: props.restoreSession as DeviceContactLock || readLock() }; const resources = createLazyResource(async () => { @@ -122,6 +126,7 @@ export function createRecoveryMenuRenderer( let blinkMs = 0; let selection: "yes" | "no" = "yes"; let confirmed = false; + let isIniting = false; const render = async ({ ctx, @@ -245,9 +250,37 @@ export function createRecoveryMenuRenderer( ctx.restore(); }; + const tryRestoreSession = async () => { + const session = config.restoreSession; + const desktop = session?.answers?.desktop; + if (!desktop) { + console.error("[renderers/recoverymenu] missing restore session"); + return; + } + try { + await handoffToGreetd(desktop); + clearLock(); + } catch (error) { + console.error("[ui/app] greetd handoff failed\n", error); + console.error("[ui/app] Press CTRL+ALT+F[0-9] to switch to a different VT"); + if (process.env.NODE_ENV !== "development") { + process.exit(1); + } + } + }; + const onSelect = (dir: "yes" | "no") => { + if (isIniting) return; const changed = dir !== selection; selection = dir; + if (confirmed === true) { + if (dir === "yes") { + isIniting = true; + void tryRestoreSession(); + return true; + } + clearLock(); + } void resources.load().then((r) => { (changed ? r.sndMove : r.sndSelect).play(); }); @@ -307,7 +340,7 @@ export function createRecoveryMenuRenderer( return true; }, shouldExit() { - return confirmed; + return confirmed && !isIniting; }, getResult() { return selection; @@ -325,6 +358,7 @@ export function createRecoveryMenuRenderer( return { loaded: resources.isLoaded(), selection, + confirmed, blinkMs: Number(blinkMs.toFixed(1)) }; }, diff --git a/src/ui/app.ts b/src/ui/app.ts index e802845..fd59e4e 100644 --- a/src/ui/app.ts +++ b/src/ui/app.ts @@ -3,7 +3,7 @@ import { dirname } from "node:path"; import { type CanvasRenderingContext2D } from "@napi-rs/canvas"; import { createBootSequenceUI } from "../bootsequence/questions"; -import { GREETD_SOCKET, handoffToGreetd } from "../desktop"; +import { GREETD_SOCKET, handoffToGreetd, KNOWN_SESSIONS_FRIENDLY } from "../desktop"; import type { BootsequenceAnswers } from "../types"; import { loadImageAsset } from "../renderer/assets"; import { parseCli } from "../renderer/cli"; @@ -14,6 +14,7 @@ import { createRenderer } from "../renderer/index"; import { SDLWindow } from "../renderer/window"; import { createRendererRegistry } from "../renderers"; import type { RendererInstance, RendererProps } from "../renderers/types"; +import { clearLock, readLock } from "./lock"; const BUNVERS = `Bun ${Bun.version} ${process.platform} ${process.arch}`; const DEFAULT_DEBUG_LOG_FILE = "/tmp/device_contact.debug.log"; @@ -43,7 +44,10 @@ export async function runDeviceContactUI(argv: string[] = process.argv) { console.debug("[debug] ESTABLISHING CONNECTION"); console.debug("[debug]", BUNVERS); - const isCrashRecovery = !!(cli.crashRecoverySession ?? process.env.CRASH_RECOVERY_SESSION); + const crashLock = readLock(); + + const crashDesktop = crashLock?.answers?.desktop; + const isCrashRecovery = !!(cli.crashRecoverySession ?? process.env.CRASH_RECOVERY_SESSION ?? crashDesktop); const forcedErrorScreen = Boolean(cli.errorScreenRequested); const requestedRenderer = cli.rendererId ?? "device_contact"; const defaultRendererId = forcedErrorScreen @@ -98,7 +102,12 @@ export async function runDeviceContactUI(argv: string[] = process.argv) { const viewWidth = 1280; const viewHeight = 960; const uiScale = 0.6; - const crashRecoverySession = cli.crashRecoverySession ?? process.env.CRASH_RECOVERY_SESSION; + const crashRecoverySession = + cli.crashRecoverySession ?? + process.env.CRASH_RECOVERY_SESSION ?? + (crashDesktop + ? KNOWN_SESSIONS_FRIENDLY[crashDesktop] ?? crashDesktop + : undefined); const renderer = createRenderer({ window }); renderer.ctx.imageSmoothingEnabled = false; @@ -110,7 +119,8 @@ export async function runDeviceContactUI(argv: string[] = process.argv) { : "?????", yesLabel: "Yes", noLabel: "No", - session: crashRecoverySession + session: crashRecoverySession, + restoreSession: crashLock ?? undefined }, errormessage: { title: cli.errorScreenTitle ?? process.env.ERROR_TITLE ?? "ERROR", @@ -312,6 +322,7 @@ Options: const desktopHint = typeof desktopHintRaw === "string" ? desktopHintRaw : undefined; try { await handoffToGreetd(desktopHint); + // clearLock(); } catch (error) { console.error("[ui/app] greetd handoff failed\n", error); console.error("[ui/app] Press CTRL+ALT+F[0-9] to switch to a different VT"); diff --git a/src/ui/lock.ts b/src/ui/lock.ts new file mode 100644 index 0000000..aa284da --- /dev/null +++ b/src/ui/lock.ts @@ -0,0 +1,70 @@ +import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs"; +import { dirname, join } from "path"; +import type { BootsequenceAnswers } from "../types"; + +const dir = "/run/deltabootd"; +const LOCK_PATH = join(dir, "DEVICE_CONTACT.lock"); + +try { + mkdirSync(dir, { recursive: true, mode: 0o755 }); +} catch { } + +export type DeviceContactLock = { + lock: true, + answers: BootsequenceAnswers +}; + +function ensureDir() { + const dir = dirname(LOCK_PATH); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true, mode: 0o755 }); + } +} + +function isBootsequenceAnswers(value: any): value is BootsequenceAnswers { + return Boolean( + value && + typeof value === "object" && + typeof value.char === "string" && + typeof value.desktop === "string" && + typeof value.color === "string" && + typeof value.gift === "string" + ); +} + +export function readLock(): DeviceContactLock | null { + try { + if (!existsSync(LOCK_PATH)) return null; + const raw = readFileSync(LOCK_PATH, "utf8"); + const parsed = JSON.parse(raw) as Partial; + if (!parsed || parsed.lock !== true) return null; + if (!isBootsequenceAnswers((parsed as any).answers)) return null; + return { + lock: true, + answers: parsed.answers! + }; + } catch { + return null; + } +} + +export function writeLock(answers: BootsequenceAnswers): void { + ensureDir(); + + const lock: DeviceContactLock = { + lock: true, + answers + }; + + writeFileSync(LOCK_PATH, JSON.stringify(lock), { + mode: 0o644, + }); +} + +export function clearLock(): void { + try { + if (existsSync(LOCK_PATH)) { + unlinkSync(LOCK_PATH); + } + } catch { } +}