Prevent idle system sleep on supported desktop runtimes without intentionally keeping the display awake. Narrow wake-lock activation to true active work states and drop the web screen-wake fallback where the platform cannot provide system-sleep-only behavior.
161 lines
4.7 KiB
TypeScript
161 lines
4.7 KiB
TypeScript
import { BrowserWindow, Notification, dialog, ipcMain, powerSaveBlocker, type OpenDialogOptions } from "electron"
|
|
import fs from "fs"
|
|
import { requestMicrophoneAccess } from "./permissions"
|
|
import type { CliProcessManager, CliStatus } from "./process-manager"
|
|
|
|
let wakeLockId: number | null = null
|
|
|
|
interface DialogOpenRequest {
|
|
mode: "directory" | "file"
|
|
title?: string
|
|
defaultPath?: string
|
|
filters?: Array<{ name?: string; extensions: string[] }>
|
|
}
|
|
|
|
interface DialogOpenResult {
|
|
canceled: boolean
|
|
paths: string[]
|
|
}
|
|
|
|
export function setupCliIPC(mainWindow: BrowserWindow, cliManager: CliProcessManager) {
|
|
cliManager.on("status", (status: CliStatus) => {
|
|
if (!mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send("cli:status", status)
|
|
}
|
|
})
|
|
|
|
cliManager.on("ready", (status: CliStatus) => {
|
|
if (!mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send("cli:ready", status)
|
|
}
|
|
})
|
|
|
|
cliManager.on("error", (error: Error) => {
|
|
if (!mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send("cli:error", { message: error.message })
|
|
}
|
|
})
|
|
|
|
ipcMain.handle("cli:getStatus", async () => cliManager.getStatus())
|
|
|
|
ipcMain.handle("cli:restart", async () => {
|
|
const devMode = process.env.NODE_ENV === "development"
|
|
await cliManager.stop()
|
|
return cliManager.start({ dev: devMode })
|
|
})
|
|
|
|
ipcMain.handle("dialog:open", async (_, request: DialogOpenRequest): Promise<DialogOpenResult> => {
|
|
const properties: OpenDialogOptions["properties"] =
|
|
request.mode === "directory" ? ["openDirectory", "createDirectory"] : ["openFile"]
|
|
|
|
const filters = request.filters?.map((filter) => ({
|
|
name: filter.name ?? "Files",
|
|
extensions: filter.extensions,
|
|
}))
|
|
|
|
const windowTarget = mainWindow.isDestroyed() ? undefined : mainWindow
|
|
const dialogOptions: OpenDialogOptions = {
|
|
title: request.title,
|
|
defaultPath: request.defaultPath,
|
|
properties,
|
|
filters,
|
|
}
|
|
const result = windowTarget
|
|
? await dialog.showOpenDialog(windowTarget, dialogOptions)
|
|
: await dialog.showOpenDialog(dialogOptions)
|
|
|
|
return { canceled: result.canceled, paths: result.filePaths }
|
|
})
|
|
|
|
ipcMain.handle("filesystem:getDirectoryPaths", async (_event, paths: unknown): Promise<string[]> => {
|
|
if (!Array.isArray(paths)) {
|
|
return []
|
|
}
|
|
|
|
const directories = paths.filter((value): value is string => {
|
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
return false
|
|
}
|
|
try {
|
|
return fs.statSync(value).isDirectory()
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
return directories
|
|
})
|
|
|
|
ipcMain.handle("power:setWakeLock", async (_event, enabled: boolean): Promise<{ enabled: boolean }> => {
|
|
const next = Boolean(enabled)
|
|
if (next) {
|
|
if (wakeLockId !== null && powerSaveBlocker.isStarted(wakeLockId)) {
|
|
return { enabled: true }
|
|
}
|
|
try {
|
|
wakeLockId = powerSaveBlocker.start("prevent-app-suspension")
|
|
} catch {
|
|
wakeLockId = null
|
|
return { enabled: false }
|
|
}
|
|
return { enabled: true }
|
|
}
|
|
|
|
if (wakeLockId !== null) {
|
|
try {
|
|
if (powerSaveBlocker.isStarted(wakeLockId)) {
|
|
powerSaveBlocker.stop(wakeLockId)
|
|
}
|
|
} finally {
|
|
wakeLockId = null
|
|
}
|
|
}
|
|
return { enabled: false }
|
|
})
|
|
|
|
ipcMain.handle(
|
|
"media:requestMicrophoneAccess",
|
|
async (): Promise<{ granted: boolean }> => ({ granted: await requestMicrophoneAccess() }),
|
|
)
|
|
|
|
ipcMain.handle(
|
|
"remote:openWindow",
|
|
async (
|
|
_event,
|
|
payload: { id: string; name: string; baseUrl: string; skipTlsVerify: boolean },
|
|
): Promise<{ ok: boolean }> => {
|
|
const opener = (mainWindow as BrowserWindow & {
|
|
__codenomadOpenRemoteWindow?: (payload: {
|
|
id: string
|
|
name: string
|
|
baseUrl: string
|
|
skipTlsVerify: boolean
|
|
}) => Promise<void>
|
|
}).__codenomadOpenRemoteWindow
|
|
if (!opener) {
|
|
throw new Error("Remote window opening is not available")
|
|
}
|
|
await opener(payload)
|
|
return { ok: true }
|
|
},
|
|
)
|
|
|
|
ipcMain.handle(
|
|
"notifications:show",
|
|
async (_event, payload: { title?: unknown; body?: unknown }): Promise<{ ok: boolean; reason?: string }> => {
|
|
if (!Notification.isSupported()) {
|
|
return { ok: false, reason: "unsupported" }
|
|
}
|
|
|
|
const title = typeof payload?.title === "string" ? payload.title : "CodeNomad"
|
|
const body = typeof payload?.body === "string" ? payload.body : ""
|
|
try {
|
|
const notification = new Notification({ title, body })
|
|
notification.show()
|
|
return { ok: true }
|
|
} catch (error) {
|
|
return { ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
}
|
|
},
|
|
)
|
|
}
|