refactor(desktop-electron): improve main process architecture
- Refactor index.ts for better organization and readability - Update ipc.ts with improved type safety - Enhance menu.ts with better structure - Optimize server.ts with cleaner implementation - Improve windows.ts window management - Update preload types for better type definitions - Clean up loading.tsx renderer component
This commit is contained in:
parent
4b65b1e053
commit
1821714479
7 changed files with 410 additions and 303 deletions
|
|
@ -1,12 +1,11 @@
|
||||||
import { randomUUID } from "node:crypto"
|
import { randomUUID } from "node:crypto"
|
||||||
import { EventEmitter } from "node:events"
|
|
||||||
import { existsSync } from "node:fs"
|
import { existsSync } from "node:fs"
|
||||||
import { createServer } from "node:net"
|
import { createServer } from "node:net"
|
||||||
import { homedir } from "node:os"
|
import { homedir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import type { Event } from "electron"
|
|
||||||
import { app, BrowserWindow, dialog } from "electron"
|
import { app, BrowserWindow, dialog } from "electron"
|
||||||
import pkg from "electron-updater"
|
import pkg from "electron-updater"
|
||||||
|
import { Data, Deferred, Effect, Fiber, Option, PubSub, Queue, Ref, Stream, SubscriptionRef } from "effect"
|
||||||
|
|
||||||
import contextMenu from "electron-context-menu"
|
import contextMenu from "electron-context-menu"
|
||||||
contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false })
|
contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false })
|
||||||
|
|
@ -34,14 +33,15 @@ app.setAppUserModelId(appId)
|
||||||
app.setPath("userData", join(app.getPath("appData"), appId))
|
app.setPath("userData", join(app.getPath("appData"), appId))
|
||||||
const { autoUpdater } = pkg
|
const { autoUpdater } = pkg
|
||||||
|
|
||||||
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
|
import { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
|
||||||
|
import type { Server } from "virtual:opencode-server"
|
||||||
import { checkAppExists, resolveAppPath, wslPath } from "./apps"
|
import { checkAppExists, resolveAppPath, wslPath } from "./apps"
|
||||||
import { CHANNEL, UPDATER_ENABLED } from "./constants"
|
import { CHANNEL, UPDATER_ENABLED } from "./constants"
|
||||||
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
|
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
|
||||||
import { initLogging } from "./logging"
|
import { initLogging } from "./logging"
|
||||||
import { parseMarkdown } from "./markdown"
|
import { parseMarkdown } from "./markdown"
|
||||||
import { createMenu } from "./menu"
|
import { createMenu } from "./menu"
|
||||||
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
|
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServerEffect } from "./server"
|
||||||
import {
|
import {
|
||||||
createLoadingWindow,
|
createLoadingWindow,
|
||||||
createMainWindow,
|
createMainWindow,
|
||||||
|
|
@ -49,230 +49,207 @@ import {
|
||||||
setBackgroundColor,
|
setBackgroundColor,
|
||||||
setDockIcon,
|
setDockIcon,
|
||||||
} from "./windows"
|
} from "./windows"
|
||||||
import { drizzle } from "drizzle-orm/node-sqlite/driver"
|
|
||||||
import type { Server } from "virtual:opencode-server"
|
|
||||||
|
|
||||||
const initEmitter = new EventEmitter()
|
// ---------------------------------------------------------------------------
|
||||||
let initStep: InitStep = { phase: "server_waiting" }
|
// State — individual pieces, synchronously allocated at module load.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
const initStep = Effect.runSync(SubscriptionRef.make<InitStep>({ _tag: "ServerWaiting" }))
|
||||||
let server: Server.Listener | null = null
|
const serverReady = Effect.runSync(Deferred.make<ServerReadyData>())
|
||||||
const loadingComplete = defer<void>()
|
const loadingComplete = Effect.runSync(Deferred.make<void>())
|
||||||
|
const deepLinkQueue = Effect.runSync(Queue.unbounded<string[]>())
|
||||||
|
const deepLinksConsumed = Effect.runSync(Deferred.make<void>())
|
||||||
|
const server = Effect.runSync(Ref.make<Option.Option<Server.Listener>>(Option.none()))
|
||||||
|
const menuCommands = Effect.runSync(PubSub.unbounded<string>())
|
||||||
|
const sqliteProgress = Effect.runSync(PubSub.unbounded<SqliteMigrationProgress>())
|
||||||
|
|
||||||
const pendingDeepLinks: string[] = []
|
// ---------------------------------------------------------------------------
|
||||||
|
// App events (Data.TaggedEnum)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const serverReady = defer<ServerReadyData>()
|
type AppEvent = Data.TaggedEnum<{
|
||||||
const logger = initLogging()
|
SecondInstance: { readonly argv: readonly string[] }
|
||||||
|
OpenUrl: { readonly url: string }
|
||||||
|
BeforeQuit: {}
|
||||||
|
WillQuit: {}
|
||||||
|
}>
|
||||||
|
|
||||||
logger.log("app starting", {
|
const appEvent = Data.taggedEnum<AppEvent>()
|
||||||
version: app.getVersion(),
|
|
||||||
packaged: app.isPackaged,
|
|
||||||
})
|
|
||||||
|
|
||||||
setupApp()
|
const handleAppEvent = (
|
||||||
|
event: AppEvent,
|
||||||
|
deepLinkQueue: Queue.Queue<string[]>,
|
||||||
|
mainWindow: BrowserWindow,
|
||||||
|
server: Ref.Ref<Option.Option<Server.Listener>>,
|
||||||
|
) =>
|
||||||
|
appEvent.$match(event, {
|
||||||
|
SecondInstance: ({ argv }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const urls = argv.filter((arg) => arg.startsWith("opencode://"))
|
||||||
|
if (urls.length) {
|
||||||
|
logger.log("deep link received via second-instance", { urls })
|
||||||
|
yield* Queue.offer(deepLinkQueue, urls)
|
||||||
|
}
|
||||||
|
focusMainWindow(mainWindow)
|
||||||
|
}),
|
||||||
|
OpenUrl: ({ url }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
logger.log("deep link received via open-url", { url })
|
||||||
|
yield* Queue.offer(deepLinkQueue, [url])
|
||||||
|
}),
|
||||||
|
BeforeQuit: () => stopServer(server),
|
||||||
|
WillQuit: () => stopServer(server),
|
||||||
|
})
|
||||||
|
|
||||||
function setupApp() {
|
// ---------------------------------------------------------------------------
|
||||||
ensureLoopbackNoProxy()
|
// Pure state helpers (explicit parameters — no hidden closures)
|
||||||
app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
if (!app.requestSingleInstanceLock()) {
|
const focusMainWindow = (win: BrowserWindow) => {
|
||||||
app.quit()
|
win.show()
|
||||||
return
|
win.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
app.on("second-instance", (_event: Event, argv: string[]) => {
|
const stopServer = (ref: Ref.Ref<Option.Option<Server.Listener>>) =>
|
||||||
const urls = argv.filter((arg: string) => arg.startsWith("opencode://"))
|
Effect.gen(function* () {
|
||||||
if (urls.length) {
|
const srv = yield* Ref.get(ref)
|
||||||
logger.log("deep link received via second-instance", { urls })
|
if (Option.isSome(srv)) {
|
||||||
emitDeepLinks(urls)
|
yield* Effect.promise(() => srv.value.stop())
|
||||||
|
yield* Ref.set(ref, Option.none())
|
||||||
}
|
}
|
||||||
focusMainWindow()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on("open-url", (event: Event, url: string) => {
|
// ---------------------------------------------------------------------------
|
||||||
event.preventDefault()
|
// Initialization flow (pure Effect — all state wired explicitly)
|
||||||
logger.log("deep link received via open-url", { url })
|
// ---------------------------------------------------------------------------
|
||||||
emitDeepLinks([url])
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on("before-quit", () => {
|
const initialize = Effect.fn("Main.initialize")(function* () {
|
||||||
killSidecar()
|
const needsMigration = !(yield* sqliteFileExists)
|
||||||
})
|
const sqliteDone = needsMigration ? yield* Deferred.make<void>() : undefined
|
||||||
|
|
||||||
app.on("will-quit", () => {
|
const port = yield* getSidecarPort
|
||||||
killSidecar()
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
||||||
process.on(signal, () => {
|
|
||||||
killSidecar()
|
|
||||||
app.exit(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
void app.whenReady().then(async () => {
|
|
||||||
app.setAsDefaultProtocolClient("opencode")
|
|
||||||
registerRendererProtocol()
|
|
||||||
setDockIcon()
|
|
||||||
setupAutoUpdater()
|
|
||||||
await initialize()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitDeepLinks(urls: string[]) {
|
|
||||||
if (urls.length === 0) return
|
|
||||||
pendingDeepLinks.push(...urls)
|
|
||||||
if (mainWindow) sendDeepLinks(mainWindow, urls)
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusMainWindow() {
|
|
||||||
if (!mainWindow) return
|
|
||||||
mainWindow.show()
|
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setInitStep(step: InitStep) {
|
|
||||||
initStep = step
|
|
||||||
logger.log("init step", { step })
|
|
||||||
initEmitter.emit("step", step)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initialize() {
|
|
||||||
const needsMigration = !sqliteFileExists()
|
|
||||||
const sqliteDone = needsMigration ? defer<void>() : undefined
|
|
||||||
let overlay: BrowserWindow | null = null
|
|
||||||
|
|
||||||
const port = await getSidecarPort()
|
|
||||||
const hostname = "127.0.0.1"
|
const hostname = "127.0.0.1"
|
||||||
const url = `http://${hostname}:${port}`
|
const url = `http://${hostname}:${port}`
|
||||||
const password = randomUUID()
|
const password = randomUUID()
|
||||||
|
|
||||||
const loadingTask = (async () => {
|
const loadingFiber = yield* Effect.gen(function* () {
|
||||||
logger.log("sidecar connection started", { url })
|
logger.log("sidecar connection started", { url })
|
||||||
|
|
||||||
initEmitter.on("sqlite", (progress: SqliteMigrationProgress) => {
|
if (needsMigration && sqliteDone) {
|
||||||
setInitStep({ phase: "sqlite_waiting" })
|
yield* Effect.gen(function* () {
|
||||||
if (overlay) sendSqliteMigrationProgress(overlay, progress)
|
const { Database, JsonMigration } = yield* Effect.promise(
|
||||||
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
|
() => import("virtual:opencode-server") as Promise<typeof import("virtual:opencode-server")>,
|
||||||
if (progress.type === "Done") sqliteDone?.resolve()
|
)
|
||||||
})
|
const client = yield* Effect.sync(() => Database.Client().$client)
|
||||||
|
const db = yield* Effect.promise(() =>
|
||||||
|
import("drizzle-orm/node-sqlite/driver").then((m) => m.drizzle({ client })),
|
||||||
|
)
|
||||||
|
|
||||||
if (needsMigration) {
|
yield* SubscriptionRef.set(initStep, InitStep.SqliteWaiting())
|
||||||
const { Database, JsonMigration } = await import("virtual:opencode-server")
|
|
||||||
await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
|
yield* Effect.promise(() =>
|
||||||
progress: (event: { current: number; total: number }) => {
|
JsonMigration.run(db, {
|
||||||
const percent = Math.round(event.current / event.total) * 100
|
progress: (event: { current: number; total: number }) => {
|
||||||
initEmitter.emit("sqlite", { type: "InProgress", value: percent })
|
const percent = Math.round((event.current / event.total) * 100)
|
||||||
},
|
const progress: SqliteMigrationProgress = { type: "InProgress", value: percent }
|
||||||
|
if (Option.isSome(overlay)) sendSqliteMigrationProgress(overlay.value, progress)
|
||||||
|
void Effect.runPromise(PubSub.publish(sqliteProgress, progress))
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* PubSub.publish(sqliteProgress, { type: "Done" })
|
||||||
|
yield* Deferred.succeed(sqliteDone, undefined)
|
||||||
})
|
})
|
||||||
initEmitter.emit("sqlite", { type: "Done" })
|
|
||||||
|
|
||||||
sqliteDone?.resolve()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsMigration) {
|
if (needsMigration && sqliteDone) {
|
||||||
await sqliteDone?.promise
|
yield* Deferred.await(sqliteDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("spawning sidecar", { url })
|
logger.log("spawning sidecar", { url })
|
||||||
const { listener, health } = await spawnLocalServer(hostname, port, password)
|
const { listener, health } = yield* spawnLocalServerEffect(hostname, port, password)
|
||||||
server = listener
|
yield* Ref.set(server, Option.some(listener))
|
||||||
serverReady.resolve({
|
|
||||||
|
yield* Deferred.succeed(serverReady, {
|
||||||
url,
|
url,
|
||||||
username: "opencode",
|
username: "opencode",
|
||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
|
|
||||||
await Promise.race([
|
yield* Effect.raceAll([
|
||||||
health.wait,
|
health,
|
||||||
delay(30_000).then(() => {
|
Effect.sleep("30 seconds").pipe(Effect.flatMap(() => Effect.fail(new Error("Sidecar health check timed out")))),
|
||||||
throw new Error("Sidecar health check timed out")
|
]).pipe(Effect.catch((error) => Effect.sync(() => logger.error("sidecar health check failed", error))))
|
||||||
}),
|
|
||||||
]).catch((error) => {
|
|
||||||
logger.error("sidecar health check failed", error)
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.log("loading task finished")
|
logger.log("loading task finished")
|
||||||
})()
|
|
||||||
|
|
||||||
if (needsMigration) {
|
return listener
|
||||||
const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
|
}).pipe(Effect.forkChild)
|
||||||
if (show) {
|
|
||||||
overlay = createLoadingWindow()
|
|
||||||
await delay(1_000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadingTask
|
const overlay = yield* Effect.gen(function* () {
|
||||||
setInitStep({ phase: "done" })
|
if (!needsMigration) return
|
||||||
|
|
||||||
if (overlay) {
|
const show = yield* Effect.raceAll([
|
||||||
await loadingComplete.promise
|
Fiber.join(loadingFiber).pipe(Effect.as(false)),
|
||||||
}
|
Effect.sleep("1 second").pipe(Effect.as(true)),
|
||||||
|
])
|
||||||
|
if (!show) return
|
||||||
|
|
||||||
mainWindow = createMainWindow()
|
const overlay = createLoadingWindow()
|
||||||
wireMenu()
|
yield* Effect.sleep("1 second")
|
||||||
|
return overlay
|
||||||
|
}).pipe(Effect.map(Option.fromNullishOr))
|
||||||
|
|
||||||
overlay?.close()
|
const listener = yield* Fiber.join(loadingFiber)
|
||||||
}
|
yield* SubscriptionRef.set(initStep, InitStep.Done())
|
||||||
|
|
||||||
function wireMenu() {
|
yield* Option.match(overlay, {
|
||||||
if (!mainWindow) return
|
onSome: Effect.fnUntraced(function* (overlay) {
|
||||||
createMenu({
|
yield* Deferred.await(loadingComplete)
|
||||||
trigger: (id) => mainWindow && sendMenuCommand(mainWindow, id),
|
overlay.close()
|
||||||
checkForUpdates: () => {
|
}),
|
||||||
void checkForUpdates(true)
|
onNone: () => Effect.void,
|
||||||
},
|
|
||||||
reload: () => mainWindow?.reload(),
|
|
||||||
relaunch: () => {
|
|
||||||
killSidecar()
|
|
||||||
app.relaunch()
|
|
||||||
app.exit(0)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
registerIpcHandlers({
|
|
||||||
killSidecar: () => killSidecar(),
|
|
||||||
awaitInitialization: async (sendStep) => {
|
|
||||||
sendStep(initStep)
|
|
||||||
const listener = (step: InitStep) => sendStep(step)
|
|
||||||
initEmitter.on("step", listener)
|
|
||||||
try {
|
|
||||||
logger.log("awaiting server ready")
|
|
||||||
const res = await serverReady.promise
|
|
||||||
logger.log("server ready", { url: res.url })
|
|
||||||
return res
|
|
||||||
} finally {
|
|
||||||
initEmitter.off("step", listener)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getWindowConfig: () => ({ updaterEnabled: UPDATER_ENABLED }),
|
|
||||||
consumeInitialDeepLinks: () => pendingDeepLinks.splice(0),
|
|
||||||
getDefaultServerUrl: () => getDefaultServerUrl(),
|
|
||||||
setDefaultServerUrl: (url) => setDefaultServerUrl(url),
|
|
||||||
getWslConfig: () => Promise.resolve(getWslConfig()),
|
|
||||||
setWslConfig: (config: WslConfig) => setWslConfig(config),
|
|
||||||
getDisplayBackend: async () => null,
|
|
||||||
setDisplayBackend: async () => undefined,
|
|
||||||
parseMarkdown: async (markdown) => parseMarkdown(markdown),
|
|
||||||
checkAppExists: async (appName) => checkAppExists(appName),
|
|
||||||
wslPath: async (path, mode) => wslPath(path, mode),
|
|
||||||
resolveAppPath: async (appName) => resolveAppPath(appName),
|
|
||||||
loadingWindowComplete: () => loadingComplete.resolve(),
|
|
||||||
runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail),
|
|
||||||
checkUpdate: async () => checkUpdate(),
|
|
||||||
installUpdate: async () => installUpdate(),
|
|
||||||
setBackgroundColor: (color) => setBackgroundColor(color),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function killSidecar() {
|
// ---------------------------------------------------------------------------
|
||||||
if (!server) return
|
// App lifecycle (imperative Electron shell, thin wrappers around Effects)
|
||||||
server.stop()
|
// ---------------------------------------------------------------------------
|
||||||
server = null
|
|
||||||
|
const logger = initLogging()
|
||||||
|
|
||||||
|
const shutdownEffect = Effect.gen(function* () {
|
||||||
|
yield* stopServer(server)
|
||||||
|
app.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const registerAppEventListeners = (appEvents: PubSub.PubSub<AppEvent>) => () => {
|
||||||
|
app.on("second-instance", (_event, argv) => {
|
||||||
|
PubSub.publishUnsafe(appEvents, appEvent.SecondInstance({ argv }))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on("open-url", (event, url) => {
|
||||||
|
event.preventDefault()
|
||||||
|
PubSub.publishUnsafe(appEvents, appEvent.OpenUrl({ url }))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
PubSub.publishUnsafe(appEvents, appEvent.BeforeQuit())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on("will-quit", () => {
|
||||||
|
PubSub.publishUnsafe(appEvents, appEvent.WillQuit())
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
||||||
|
process.on(signal, () => {
|
||||||
|
void Effect.runPromise(shutdownEffect)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureLoopbackNoProxy() {
|
const ensureLoopbackNoProxy = () => {
|
||||||
const loopback = ["127.0.0.1", "localhost", "::1"]
|
const loopback = ["127.0.0.1", "localhost", "::1"]
|
||||||
const upsert = (key: string) => {
|
const upsert = (key: string) => {
|
||||||
const items = (process.env[key] ?? "")
|
const items = (process.env[key] ?? "")
|
||||||
|
|
@ -292,14 +269,154 @@ function ensureLoopbackNoProxy() {
|
||||||
upsert("no_proxy")
|
upsert("no_proxy")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSidecarPort() {
|
const main = Effect.gen(function* () {
|
||||||
|
logger.log("app starting", {
|
||||||
|
version: app.getVersion(),
|
||||||
|
packaged: app.isPackaged,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!app.requestSingleInstanceLock()) {
|
||||||
|
app.quit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureLoopbackNoProxy()
|
||||||
|
app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")
|
||||||
|
|
||||||
|
const appEvents = yield* PubSub.unbounded<AppEvent>()
|
||||||
|
registerAppEventListeners(appEvents)
|
||||||
|
|
||||||
|
yield* Effect.promise(() => app.whenReady())
|
||||||
|
|
||||||
|
app.setAsDefaultProtocolClient("opencode")
|
||||||
|
registerRendererProtocol()
|
||||||
|
setDockIcon()
|
||||||
|
setupAutoUpdater()
|
||||||
|
|
||||||
|
registerIpcHandlersEffect()
|
||||||
|
|
||||||
|
yield* initialize()
|
||||||
|
|
||||||
|
const mainWindow = createMainWindow()
|
||||||
|
wireMenu(mainWindow)
|
||||||
|
|
||||||
|
yield* Stream.fromPubSub(appEvents).pipe(
|
||||||
|
Stream.runForEach((event) => handleAppEvent(event, deepLinkQueue, mainWindow, server)),
|
||||||
|
Effect.forkChild,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Deferred.await(deepLinksConsumed).pipe(
|
||||||
|
Effect.andThen(
|
||||||
|
Stream.fromQueue(deepLinkQueue).pipe(
|
||||||
|
Stream.runForEach((urls) => Effect.sync(() => sendDeepLinks(mainWindow, urls))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.forkChild,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Stream.fromPubSub(menuCommands).pipe(
|
||||||
|
Stream.runForEach((id) => Effect.sync(() => sendMenuCommand(mainWindow, id))),
|
||||||
|
Effect.forkChild,
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Effect.catch((error) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
logger.error("initialization failed", error)
|
||||||
|
app.exit(1)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
void Effect.runPromise(main)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Menu wiring
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const wireMenu = (win: BrowserWindow) => {
|
||||||
|
createMenu({
|
||||||
|
trigger: (id) => {
|
||||||
|
sendMenuCommand(win, id)
|
||||||
|
},
|
||||||
|
checkForUpdates: () => {
|
||||||
|
void checkForUpdates(true)
|
||||||
|
},
|
||||||
|
reload: () => win.reload(),
|
||||||
|
relaunch: () => {
|
||||||
|
void Effect.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* stopServer(server)
|
||||||
|
app.relaunch()
|
||||||
|
app.exit(0)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// IPC handlers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const registerIpcHandlersEffect = () =>
|
||||||
|
registerIpcHandlers({
|
||||||
|
killSidecar: () => Effect.runPromise(stopServer(server)),
|
||||||
|
awaitInitialization: (sendStep) =>
|
||||||
|
Effect.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const currentStep = SubscriptionRef.getUnsafe(initStep)
|
||||||
|
sendStep(currentStep)
|
||||||
|
|
||||||
|
const fiber = yield* SubscriptionRef.changes(initStep).pipe(
|
||||||
|
Stream.runForEach((step) => Effect.sync(() => sendStep(step))),
|
||||||
|
Effect.forkChild,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.log("awaiting server ready")
|
||||||
|
const res = yield* Deferred.await(serverReady)
|
||||||
|
logger.log("server ready", { url: res.url })
|
||||||
|
|
||||||
|
yield* Fiber.interrupt(fiber)
|
||||||
|
return res
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
getWindowConfig: () => ({ updaterEnabled: UPDATER_ENABLED }),
|
||||||
|
consumeInitialDeepLinks: () =>
|
||||||
|
Effect.runPromise(
|
||||||
|
Queue.clear(deepLinkQueue).pipe(
|
||||||
|
Effect.map((links) => links.flat()),
|
||||||
|
Effect.tap(() => Deferred.succeed(deepLinksConsumed, undefined)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
getDefaultServerUrl: () => Effect.runPromise(getDefaultServerUrl),
|
||||||
|
setDefaultServerUrl: (url) => Effect.runPromise(setDefaultServerUrl(url)),
|
||||||
|
getWslConfig: () => Effect.runPromise(getWslConfig),
|
||||||
|
setWslConfig: (config: WslConfig) => Effect.runPromise(setWslConfig(config)),
|
||||||
|
getDisplayBackend: () => Promise.resolve(null),
|
||||||
|
setDisplayBackend: () => Promise.resolve(undefined),
|
||||||
|
parseMarkdown: (markdown) => Promise.resolve(parseMarkdown(markdown)),
|
||||||
|
checkAppExists: (appName) => checkAppExists(appName),
|
||||||
|
wslPath: (path, mode) => Promise.resolve(wslPath(path, mode)),
|
||||||
|
resolveAppPath: (appName) => Promise.resolve(resolveAppPath(appName)),
|
||||||
|
loadingWindowComplete: () => Effect.runPromise(Deferred.succeed(loadingComplete, undefined)),
|
||||||
|
runUpdater: (alertOnFail) => checkForUpdates(alertOnFail),
|
||||||
|
checkUpdate: () => checkUpdate(),
|
||||||
|
installUpdate: () => installUpdate(),
|
||||||
|
setBackgroundColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Utilities
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const getSidecarPort = Effect.promise(() => {
|
||||||
const fromEnv = process.env.OPENCODE_PORT
|
const fromEnv = process.env.OPENCODE_PORT
|
||||||
if (fromEnv) {
|
if (fromEnv) {
|
||||||
const parsed = Number.parseInt(fromEnv, 10)
|
const parsed = Number.parseInt(fromEnv, 10)
|
||||||
if (!Number.isNaN(parsed)) return parsed
|
if (!Number.isNaN(parsed)) return Promise.resolve(parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await new Promise<number>((resolve, reject) => {
|
return new Promise<number>((resolve, reject) => {
|
||||||
const server = createServer()
|
const server = createServer()
|
||||||
server.on("error", reject)
|
server.on("error", reject)
|
||||||
server.listen(0, "127.0.0.1", () => {
|
server.listen(0, "127.0.0.1", () => {
|
||||||
|
|
@ -313,13 +430,13 @@ async function getSidecarPort() {
|
||||||
server.close(() => resolve(port))
|
server.close(() => resolve(port))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
|
|
||||||
function sqliteFileExists() {
|
const sqliteFileExists = Effect.sync(() => {
|
||||||
const xdg = process.env.XDG_DATA_HOME
|
const xdg = process.env.XDG_DATA_HOME
|
||||||
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".local", "share")
|
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".local", "share")
|
||||||
return existsSync(join(base, "opencode", "opencode.db"))
|
return existsSync(join(base, "opencode", "opencode.db"))
|
||||||
}
|
})
|
||||||
|
|
||||||
function setupAutoUpdater() {
|
function setupAutoUpdater() {
|
||||||
if (!UPDATER_ENABLED) return
|
if (!UPDATER_ENABLED) return
|
||||||
|
|
@ -390,7 +507,7 @@ async function installUpdate() {
|
||||||
logger.log("installing downloaded update", {
|
logger.log("installing downloaded update", {
|
||||||
version: downloadedUpdateVersion,
|
version: downloadedUpdateVersion,
|
||||||
})
|
})
|
||||||
killSidecar()
|
void Effect.runPromise(stopServer(server))
|
||||||
autoUpdater.quitAndInstall()
|
autoUpdater.quitAndInstall()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -436,17 +553,3 @@ async function checkForUpdates(alertOnFail: boolean) {
|
||||||
await installUpdate()
|
await installUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function delay(ms: number) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
||||||
}
|
|
||||||
|
|
||||||
function defer<T>() {
|
|
||||||
let resolve!: (value: T) => void
|
|
||||||
let reject!: (error: Error) => void
|
|
||||||
const promise = new Promise<T>((res, rej) => {
|
|
||||||
resolve = res
|
|
||||||
reject = rej
|
|
||||||
})
|
|
||||||
return { promise, resolve, reject }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -191,14 +191,7 @@ export function registerIpcHandlers(deps: Deps) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendSqliteMigrationProgress(win: BrowserWindow, progress: SqliteMigrationProgress) {
|
export const sendSqliteMigrationProgress = (win: BrowserWindow, progress: SqliteMigrationProgress) =>
|
||||||
win.webContents.send("sqlite-migration-progress", progress)
|
win.webContents.send("sqlite-migration-progress", progress)
|
||||||
}
|
export const sendMenuCommand = (win: BrowserWindow, id: string) => win.webContents.send("menu-command", id)
|
||||||
|
export const sendDeepLinks = (win: BrowserWindow, urls: string[]) => win.webContents.send("deep-link", urls)
|
||||||
export function sendMenuCommand(win: BrowserWindow, id: string) {
|
|
||||||
win.webContents.send("menu-command", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendDeepLinks(win: BrowserWindow, urls: string[]) {
|
|
||||||
win.webContents.send("deep-link", urls)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Menu, shell } from "electron"
|
import { Menu, shell } from "electron"
|
||||||
|
|
||||||
import { UPDATER_ENABLED } from "./constants"
|
import { UPDATER_ENABLED } from "./constants"
|
||||||
import { createMainWindow } from "./windows"
|
import { createMainWindow } from "./windows"
|
||||||
|
|
||||||
|
|
@ -47,7 +46,9 @@ export function createMenu(deps: Deps) {
|
||||||
{
|
{
|
||||||
label: "New Window",
|
label: "New Window",
|
||||||
accelerator: "Cmd+Shift+N",
|
accelerator: "Cmd+Shift+N",
|
||||||
click: () => createMainWindow(),
|
click: () => {
|
||||||
|
void createMainWindow()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{ role: "close" },
|
{ role: "close" },
|
||||||
|
|
|
||||||
|
|
@ -1,80 +1,81 @@
|
||||||
import { app } from "electron"
|
import { app } from "electron"
|
||||||
|
import { Effect } from "effect"
|
||||||
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
|
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
|
||||||
import { getUserShell, loadShellEnv } from "./shell-env"
|
import { getUserShell, loadShellEnv } from "./shell-env"
|
||||||
import { getStore } from "./store"
|
import { getStore } from "./store"
|
||||||
|
|
||||||
export type WslConfig = { enabled: boolean }
|
export type WslConfig = { enabled: boolean }
|
||||||
|
|
||||||
export type HealthCheck = { wait: Promise<void> }
|
export const getDefaultServerUrl = Effect.sync((): string | null => {
|
||||||
|
|
||||||
export function getDefaultServerUrl(): string | null {
|
|
||||||
const value = getStore().get(DEFAULT_SERVER_URL_KEY)
|
const value = getStore().get(DEFAULT_SERVER_URL_KEY)
|
||||||
return typeof value === "string" ? value : null
|
return typeof value === "string" ? value : null
|
||||||
}
|
})
|
||||||
|
|
||||||
export function setDefaultServerUrl(url: string | null) {
|
export const setDefaultServerUrl = (url: string | null) =>
|
||||||
if (url) {
|
Effect.sync(() => {
|
||||||
getStore().set(DEFAULT_SERVER_URL_KEY, url)
|
if (url) {
|
||||||
return
|
getStore().set(DEFAULT_SERVER_URL_KEY, url)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
getStore().delete(DEFAULT_SERVER_URL_KEY)
|
getStore().delete(DEFAULT_SERVER_URL_KEY)
|
||||||
}
|
|
||||||
|
|
||||||
export function getWslConfig(): WslConfig {
|
|
||||||
const value = getStore().get(WSL_ENABLED_KEY)
|
|
||||||
return { enabled: typeof value === "boolean" ? value : false }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setWslConfig(config: WslConfig) {
|
|
||||||
getStore().set(WSL_ENABLED_KEY, config.enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function spawnLocalServer(hostname: string, port: number, password: string) {
|
|
||||||
prepareServerEnv(password)
|
|
||||||
const { Log, Server } = await import("virtual:opencode-server")
|
|
||||||
await Log.init({ level: "WARN" })
|
|
||||||
const listener = await Server.listen({
|
|
||||||
port,
|
|
||||||
hostname,
|
|
||||||
username: "opencode",
|
|
||||||
password,
|
|
||||||
cors: ["oc://renderer"],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const wait = (async () => {
|
export const getWslConfig = Effect.sync((): WslConfig => {
|
||||||
const url = `http://${hostname}:${port}`
|
const value = getStore().get(WSL_ENABLED_KEY)
|
||||||
|
return { enabled: typeof value === "boolean" ? value : false }
|
||||||
|
})
|
||||||
|
|
||||||
const ready = async () => {
|
export const setWslConfig = (config: WslConfig) =>
|
||||||
|
Effect.sync(() => getStore().set(WSL_ENABLED_KEY, config.enabled))
|
||||||
|
|
||||||
|
export const spawnLocalServerEffect = Effect.fn("Server.spawnLocalServer")(
|
||||||
|
function* (hostname: string, port: number, password: string) {
|
||||||
|
yield* prepareServerEnv(password)
|
||||||
|
const { Log, Server } = yield* Effect.promise(() =>
|
||||||
|
import("virtual:opencode-server") as Promise<typeof import("virtual:opencode-server")>,
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => Log.init({ level: "WARN" }))
|
||||||
|
const listener = yield* Effect.promise(() =>
|
||||||
|
Server.listen({
|
||||||
|
port,
|
||||||
|
hostname,
|
||||||
|
username: "opencode",
|
||||||
|
password,
|
||||||
|
cors: ["oc://renderer"],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const healthCheck = Effect.gen(function* () {
|
||||||
|
const url = `http://${hostname}:${port}`
|
||||||
while (true) {
|
while (true) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
const healthy = yield* checkHealthEffect(url, password)
|
||||||
if (await checkHealth(url, password)) return
|
if (healthy) return
|
||||||
|
yield* Effect.sleep("100 millis")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { listener, health: healthCheck }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const prepareServerEnv = (password: string) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
const shell = process.platform === "win32" ? null : getUserShell()
|
||||||
|
const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {}
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
...shellEnv,
|
||||||
|
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
|
||||||
|
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
|
||||||
|
OPENCODE_CLIENT: "desktop",
|
||||||
|
OPENCODE_SERVER_USERNAME: "opencode",
|
||||||
|
OPENCODE_SERVER_PASSWORD: password,
|
||||||
|
XDG_STATE_HOME: app.getPath("userData"),
|
||||||
}
|
}
|
||||||
|
Object.assign(process.env, env)
|
||||||
|
})
|
||||||
|
|
||||||
await ready()
|
export const checkHealthEffect = Effect.fn("Server.checkHealth")(function* (url: string, password?: string | null) {
|
||||||
})()
|
|
||||||
|
|
||||||
return { listener, health: { wait } }
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareServerEnv(password: string) {
|
|
||||||
const shell = process.platform === "win32" ? null : getUserShell()
|
|
||||||
const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {}
|
|
||||||
const env = {
|
|
||||||
...process.env,
|
|
||||||
...shellEnv,
|
|
||||||
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
|
|
||||||
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
|
|
||||||
OPENCODE_CLIENT: "desktop",
|
|
||||||
OPENCODE_SERVER_USERNAME: "opencode",
|
|
||||||
OPENCODE_SERVER_PASSWORD: password,
|
|
||||||
XDG_STATE_HOME: app.getPath("userData"),
|
|
||||||
}
|
|
||||||
Object.assign(process.env, env)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
|
|
||||||
let healthUrl: URL
|
let healthUrl: URL
|
||||||
try {
|
try {
|
||||||
healthUrl = new URL("/global/health", url)
|
healthUrl = new URL("/global/health", url)
|
||||||
|
|
@ -89,13 +90,15 @@ export async function checkHealth(url: string, password?: string | null): Promis
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(healthUrl, {
|
const res = yield* Effect.promise(() =>
|
||||||
method: "GET",
|
fetch(healthUrl, {
|
||||||
headers,
|
method: "GET",
|
||||||
signal: AbortSignal.timeout(3000),
|
headers,
|
||||||
})
|
signal: AbortSignal.timeout(3000),
|
||||||
|
}),
|
||||||
|
)
|
||||||
return res.ok
|
return res.ok
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,13 @@ protocol.registerSchemesAsPrivileged([
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
let backgroundColor: string | undefined
|
let backgroundColor: string | undefined = undefined
|
||||||
|
|
||||||
export function setBackgroundColor(color: string) {
|
export const setBackgroundColor = (color: string) => {
|
||||||
backgroundColor = color
|
backgroundColor = color
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBackgroundColor(): string | undefined {
|
export const getBackgroundColor = () => backgroundColor
|
||||||
return backgroundColor
|
|
||||||
}
|
|
||||||
|
|
||||||
function iconsDir() {
|
function iconsDir() {
|
||||||
return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
|
return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
|
||||||
|
|
@ -52,7 +50,7 @@ function overlay(theme: Partial<TitlebarTheme> = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) {
|
export const setTitlebar = (win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) => {
|
||||||
if (process.platform !== "win32") return
|
if (process.platform !== "win32") return
|
||||||
win.setTitleBarOverlay(overlay(theme))
|
win.setTitleBarOverlay(overlay(theme))
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +68,7 @@ export function createMainWindow() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const mode = tone()
|
const mode = tone()
|
||||||
|
const bg = getBackgroundColor()
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
x: state.x,
|
x: state.x,
|
||||||
y: state.y,
|
y: state.y,
|
||||||
|
|
@ -78,7 +77,7 @@ export function createMainWindow() {
|
||||||
show: false,
|
show: false,
|
||||||
title: "OpenCode",
|
title: "OpenCode",
|
||||||
icon: iconPath(),
|
icon: iconPath(),
|
||||||
backgroundColor,
|
backgroundColor: bg,
|
||||||
...(process.platform === "darwin"
|
...(process.platform === "darwin"
|
||||||
? {
|
? {
|
||||||
titleBarStyle: "hidden" as const,
|
titleBarStyle: "hidden" as const,
|
||||||
|
|
@ -126,6 +125,7 @@ export function createMainWindow() {
|
||||||
|
|
||||||
export function createLoadingWindow() {
|
export function createLoadingWindow() {
|
||||||
const mode = tone()
|
const mode = tone()
|
||||||
|
const bg = getBackgroundColor()
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
width: 640,
|
width: 640,
|
||||||
height: 480,
|
height: 480,
|
||||||
|
|
@ -133,7 +133,7 @@ export function createLoadingWindow() {
|
||||||
center: true,
|
center: true,
|
||||||
show: true,
|
show: true,
|
||||||
icon: iconPath(),
|
icon: iconPath(),
|
||||||
backgroundColor,
|
backgroundColor: bg,
|
||||||
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
|
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
|
||||||
...(process.platform === "win32"
|
...(process.platform === "win32"
|
||||||
? {
|
? {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" }
|
import { Data } from "effect"
|
||||||
|
|
||||||
|
export type InitStep = Data.TaggedEnum<{
|
||||||
|
ServerWaiting: {}
|
||||||
|
SqliteWaiting: {}
|
||||||
|
Done: {}
|
||||||
|
}>
|
||||||
|
export const InitStep = Data.taggedEnum<InitStep>()
|
||||||
|
|
||||||
export type ServerReadyData = {
|
export type ServerReadyData = {
|
||||||
url: string
|
url: string
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,14 @@ render(() => {
|
||||||
const [line, setLine] = createSignal(0)
|
const [line, setLine] = createSignal(0)
|
||||||
const [percent, setPercent] = createSignal(0)
|
const [percent, setPercent] = createSignal(0)
|
||||||
|
|
||||||
const phase = createMemo(() => step()?.phase)
|
const phase = createMemo(() => step()?._tag)
|
||||||
|
|
||||||
const value = createMemo(() => {
|
const value = createMemo(() => {
|
||||||
if (phase() === "done") return 100
|
if (phase() === "Done") return 100
|
||||||
return Math.max(25, Math.min(100, percent()))
|
return Math.max(25, Math.min(100, percent()))
|
||||||
})
|
})
|
||||||
|
|
||||||
window.api.awaitInitialization((next) => setStep(next as InitStep)).catch(() => undefined)
|
window.api.awaitInitialization((next) => setStep(next)).catch(() => undefined)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setLine(0)
|
setLine(0)
|
||||||
|
|
@ -36,7 +36,7 @@ render(() => {
|
||||||
if (progress.type === "InProgress") setPercent(Math.max(0, Math.min(100, progress.value)))
|
if (progress.type === "InProgress") setPercent(Math.max(0, Math.min(100, progress.value)))
|
||||||
if (progress.type === "Done") {
|
if (progress.type === "Done") {
|
||||||
setPercent(100)
|
setPercent(100)
|
||||||
setStep({ phase: "done" })
|
setStep({ _tag: "Done" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -47,15 +47,15 @@ render(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (phase() !== "done") return
|
if (phase() !== "Done") return
|
||||||
|
|
||||||
const timer = setTimeout(() => window.api.loadingWindowComplete(), 1000)
|
const timer = setTimeout(() => window.api.loadingWindowComplete(), 1000)
|
||||||
onCleanup(() => clearTimeout(timer))
|
onCleanup(() => clearTimeout(timer))
|
||||||
})
|
})
|
||||||
|
|
||||||
const status = createMemo(() => {
|
const status = createMemo(() => {
|
||||||
if (phase() === "done") return "All done"
|
if (phase() === "Done") return "All done"
|
||||||
if (phase() === "sqlite_waiting") return lines[line()]
|
if (phase() === "SqliteWaiting") return lines[line()]
|
||||||
return "Just a moment..."
|
return "Just a moment..."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue