源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,589 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import {
|
||||
access,
|
||||
copyFile,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rename,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
/** @typedef {import("@openwork/types/desktop-ipc").DesktopIntegrationIssue} DesktopIntegrationIssue */
|
||||
/** @typedef {import("@openwork/types/desktop-ipc").DesktopIntegrationResult} DesktopIntegrationResult */
|
||||
/** @typedef {import("@openwork/types/desktop-ipc").DesktopIntegrationStatus} DesktopIntegrationStatus */
|
||||
|
||||
export const OPENWORK_DESKTOP_ID = "com.differentai.openwork.desktop";
|
||||
export const OPENWORK_DESKTOP_NAME = "com.differentai.openwork";
|
||||
export const OPENWORK_PROTOCOL_MIME = "x-scheme-handler/openwork";
|
||||
|
||||
const INTEGRATION_STATE_VERSION = 1;
|
||||
const OWNERSHIP_MARKER = "X-OpenWork-Managed";
|
||||
const MANAGED_VERSION_MARKER = "X-OpenWork-Version";
|
||||
const ICON_SIZES = [16, 24, 32, 48, 64, 96, 128, 256, 512];
|
||||
|
||||
function defaultCommandRunner(command, args) {
|
||||
return new Promise((resolve) => {
|
||||
execFile(command, args, { encoding: "utf8", timeout: 10_000 }, (error, stdout, stderr) => {
|
||||
resolve({
|
||||
ok: !error,
|
||||
stdout: typeof stdout === "string" ? stdout : "",
|
||||
stderr: typeof stderr === "string" ? stderr : error?.message ?? "",
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cleanDesktopValue(value) {
|
||||
return String(value ?? "").replace(/[\0\r\n]/g, " ").trim();
|
||||
}
|
||||
|
||||
export function quoteDesktopExec(value) {
|
||||
const cleaned = cleanDesktopValue(value);
|
||||
const quoted = cleaned
|
||||
.replaceAll("%", "%%")
|
||||
.replace(/([\\`"$])/g, "\\$1");
|
||||
return `"${quoted}"`;
|
||||
}
|
||||
|
||||
function parseDesktopEntry(content) {
|
||||
const fields = new Map();
|
||||
let inDesktopEntry = false;
|
||||
for (const rawLine of String(content ?? "").split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
inDesktopEntry = line === "[Desktop Entry]";
|
||||
continue;
|
||||
}
|
||||
if (!inDesktopEntry || !line || line.startsWith("#")) continue;
|
||||
const separator = line.indexOf("=");
|
||||
if (separator <= 0) continue;
|
||||
fields.set(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function unquoteDesktopValue(value) {
|
||||
const trimmed = String(value ?? "").trim();
|
||||
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
||||
return trimmed.slice(1, -1).replace(/\\([\\`"$])/g, "$1");
|
||||
}
|
||||
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
||||
return trimmed.slice(1, -1);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function entryTargetsAppImage(fields, appImagePath) {
|
||||
const tryExec = unquoteDesktopValue(fields.get("TryExec"));
|
||||
if (tryExec && path.resolve(tryExec) === appImagePath) return true;
|
||||
const exec = fields.get("Exec") ?? "";
|
||||
return exec.includes(appImagePath)
|
||||
|| exec.includes(quoteDesktopExec(appImagePath))
|
||||
|| exec.includes(`'${appImagePath.replaceAll("'", "'\\''")}'`);
|
||||
}
|
||||
|
||||
function entryHandlesOpenwork(fields) {
|
||||
return (fields.get("MimeType") ?? "")
|
||||
.split(";")
|
||||
.map((value) => value.trim())
|
||||
.includes(OPENWORK_PROTOCOL_MIME);
|
||||
}
|
||||
|
||||
function entryAcceptsUrl(fields) {
|
||||
return /%(?:u|U)(?:\s|$)/.test(fields.get("Exec") ?? "");
|
||||
}
|
||||
|
||||
function emptyState() {
|
||||
return {
|
||||
version: INTEGRATION_STATE_VERSION,
|
||||
dismissedAppImages: [],
|
||||
previousProtocolHandler: null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeState(value) {
|
||||
if (!value || typeof value !== "object") return emptyState();
|
||||
const dismissed = Array.isArray(value.dismissedAppImages)
|
||||
? value.dismissedAppImages.filter((item) => typeof item === "string")
|
||||
: [];
|
||||
return {
|
||||
version: INTEGRATION_STATE_VERSION,
|
||||
dismissedAppImages: dismissed,
|
||||
previousProtocolHandler:
|
||||
typeof value.previousProtocolHandler === "string" && value.previousProtocolHandler.trim()
|
||||
? value.previousProtocolHandler.trim()
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
async function fileExists(target) {
|
||||
return access(target, fsConstants.F_OK).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
async function atomicWrite(target, content) {
|
||||
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
||||
await mkdir(path.dirname(target), { recursive: true });
|
||||
await writeFile(temporary, content, { encoding: "utf8", mode: 0o644 });
|
||||
await rename(temporary, target);
|
||||
}
|
||||
|
||||
async function removeDesktopAssociation(filePath, desktopId) {
|
||||
const content = await readFile(filePath, "utf8").catch(() => null);
|
||||
if (content == null) return false;
|
||||
let section = "";
|
||||
let changed = false;
|
||||
const next = content.split(/\r?\n/).map((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||
section = trimmed;
|
||||
return line;
|
||||
}
|
||||
if (
|
||||
(section !== "[Default Applications]" && section !== "[Added Associations]")
|
||||
|| !trimmed.startsWith(`${OPENWORK_PROTOCOL_MIME}=`)
|
||||
) {
|
||||
return line;
|
||||
}
|
||||
const separator = line.indexOf("=");
|
||||
const handlers = line
|
||||
.slice(separator + 1)
|
||||
.split(";")
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value && value !== desktopId);
|
||||
changed = true;
|
||||
return `${line.slice(0, separator + 1)}${handlers.length ? `${handlers.join(";")};` : ""}`;
|
||||
}).join("\n");
|
||||
if (changed) await atomicWrite(filePath, next);
|
||||
return changed;
|
||||
}
|
||||
|
||||
/** @returns {DesktopIntegrationStatus} */
|
||||
function unsupportedStatus() {
|
||||
return {
|
||||
supported: false,
|
||||
state: "unsupported",
|
||||
ownership: "none",
|
||||
appImagePath: null,
|
||||
desktopEntryPath: null,
|
||||
handlerDesktopId: null,
|
||||
issues: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOpenworkDesktopEntry({
|
||||
appImagePath,
|
||||
appName,
|
||||
appVersion,
|
||||
distribution,
|
||||
}) {
|
||||
return `[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.0
|
||||
Name=${cleanDesktopValue(appName)}
|
||||
Comment=Run agents, skills, and MCP with OpenWork
|
||||
Exec=${quoteDesktopExec(appImagePath)} %U
|
||||
TryExec=${cleanDesktopValue(appImagePath)}
|
||||
Icon=${OPENWORK_DESKTOP_NAME}
|
||||
StartupWMClass=${OPENWORK_DESKTOP_NAME}
|
||||
Terminal=false
|
||||
Categories=Development;Utility;
|
||||
MimeType=${OPENWORK_PROTOCOL_MIME};
|
||||
X-AppImage-Name=${cleanDesktopValue(appName)}
|
||||
X-AppImage-Version=${cleanDesktopValue(appVersion)}
|
||||
${OWNERSHIP_MARKER}=true
|
||||
${MANAGED_VERSION_MARKER}=${cleanDesktopValue(appVersion)}
|
||||
X-OpenWork-Distribution=${cleanDesktopValue(distribution)}
|
||||
X-OpenWork-AppImage=${cleanDesktopValue(appImagePath)}
|
||||
`;
|
||||
}
|
||||
|
||||
export function createLinuxDesktopIntegration({
|
||||
app,
|
||||
dialog,
|
||||
appName,
|
||||
distribution,
|
||||
env = process.env,
|
||||
platform = process.platform,
|
||||
homeDir = os.homedir(),
|
||||
resourcesPath = process.resourcesPath,
|
||||
runCommand = defaultCommandRunner,
|
||||
}) {
|
||||
const rawAppImagePath = typeof env.APPIMAGE === "string" ? env.APPIMAGE.trim() : "";
|
||||
const appImagePath = rawAppImagePath && path.isAbsolute(rawAppImagePath)
|
||||
? path.resolve(rawAppImagePath)
|
||||
: null;
|
||||
const supported = platform === "linux" && app.isPackaged && appImagePath != null;
|
||||
const dataHome = env.XDG_DATA_HOME?.trim() || path.join(homeDir, ".local", "share");
|
||||
const configHome = env.XDG_CONFIG_HOME?.trim() || path.join(homeDir, ".config");
|
||||
const desktopEntryPath = path.join(dataHome, "applications", OPENWORK_DESKTOP_ID);
|
||||
const iconPaths = Object.fromEntries(ICON_SIZES.map((size) => [
|
||||
size,
|
||||
path.join(dataHome, "icons", "hicolor", `${size}x${size}`, "apps", `${OPENWORK_DESKTOP_NAME}.png`),
|
||||
]));
|
||||
const iconSources = Object.fromEntries(ICON_SIZES.map((size) => [
|
||||
size,
|
||||
path.join(resourcesPath ?? "", "icons", "linux", `${size}x${size}.png`),
|
||||
]));
|
||||
const statePath = path.join(configHome, "openwork", "desktop-integration.json");
|
||||
|
||||
async function readState() {
|
||||
const raw = await readFile(statePath, "utf8").catch(() => null);
|
||||
if (raw == null) return emptyState();
|
||||
try {
|
||||
return normalizeState(JSON.parse(raw));
|
||||
} catch {
|
||||
return emptyState();
|
||||
}
|
||||
}
|
||||
|
||||
async function writeState(state) {
|
||||
await atomicWrite(statePath, `${JSON.stringify(normalizeState(state), null, 2)}\n`);
|
||||
}
|
||||
|
||||
async function queryDefaultHandler() {
|
||||
const result = await runCommand("xdg-mime", ["query", "default", OPENWORK_PROTOCOL_MIME]);
|
||||
return result.ok && result.stdout.trim() ? result.stdout.trim() : null;
|
||||
}
|
||||
|
||||
function desktopSearchRoots() {
|
||||
const systemRoots = (env.XDG_DATA_DIRS?.trim() || "/usr/local/share:/usr/share")
|
||||
.split(":")
|
||||
.filter(Boolean);
|
||||
return [dataHome, ...systemRoots].map((root) => path.join(root, "applications"));
|
||||
}
|
||||
|
||||
async function locateDesktopEntry(desktopId) {
|
||||
if (!desktopId || desktopId.includes("/") || desktopId.includes("\\")) return null;
|
||||
for (const root of desktopSearchRoots()) {
|
||||
const candidate = path.join(root, desktopId);
|
||||
if (await fileExists(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function inspectDesktopFile(candidate, desktopId) {
|
||||
const content = await readFile(candidate, "utf8").catch(() => null);
|
||||
if (content == null) return null;
|
||||
const fields = parseDesktopEntry(content);
|
||||
if (!entryTargetsAppImage(fields, appImagePath)) return null;
|
||||
return {
|
||||
desktopId,
|
||||
path: candidate,
|
||||
managed: fields.get(OWNERSHIP_MARKER) === "true",
|
||||
acceptsUrl: entryAcceptsUrl(fields),
|
||||
handlesProtocol: entryHandlesOpenwork(fields),
|
||||
};
|
||||
}
|
||||
|
||||
async function findExternalEntry(handlerDesktopId) {
|
||||
const inspected = handlerDesktopId
|
||||
? await locateDesktopEntry(handlerDesktopId)
|
||||
.then((candidate) => candidate && inspectDesktopFile(candidate, handlerDesktopId))
|
||||
: null;
|
||||
if (inspected && !inspected.managed) return inspected;
|
||||
|
||||
const applicationsRoot = path.join(dataHome, "applications");
|
||||
const entries = await readdir(applicationsRoot, { withFileTypes: true }).catch(() => []);
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith(".desktop") || entry.name === OPENWORK_DESKTOP_ID) continue;
|
||||
const candidate = await inspectDesktopFile(path.join(applicationsRoot, entry.name), entry.name);
|
||||
if (candidate && !candidate.managed) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @returns {Promise<DesktopIntegrationStatus>} */
|
||||
async function getStatus() {
|
||||
if (!supported) return unsupportedStatus();
|
||||
const handlerDesktopId = await queryDefaultHandler();
|
||||
const ownContent = await readFile(desktopEntryPath, "utf8").catch(() => null);
|
||||
const ownFields = ownContent == null ? null : parseDesktopEntry(ownContent);
|
||||
const ownManaged = ownFields?.get(OWNERSHIP_MARKER) === "true";
|
||||
|
||||
if (ownFields && ownManaged) {
|
||||
/** @type {DesktopIntegrationIssue[]} */
|
||||
const issues = [];
|
||||
if (!entryTargetsAppImage(ownFields, appImagePath)) issues.push("appimage-path");
|
||||
if (!entryHandlesOpenwork(ownFields)) issues.push("desktop-entry");
|
||||
if (ownFields.get(MANAGED_VERSION_MARKER) !== app.getVersion()) issues.push("version");
|
||||
const iconsPresent = await Promise.all(
|
||||
ICON_SIZES.map((size) => fileExists(iconPaths[size])),
|
||||
);
|
||||
if (!iconsPresent.every(Boolean)) issues.push("icon");
|
||||
if (handlerDesktopId !== OPENWORK_DESKTOP_ID) issues.push("protocol-handler");
|
||||
return {
|
||||
supported: true,
|
||||
state: issues.length ? "needs_repair" : "integrated",
|
||||
ownership: "openwork",
|
||||
appImagePath,
|
||||
desktopEntryPath,
|
||||
handlerDesktopId,
|
||||
issues,
|
||||
};
|
||||
}
|
||||
|
||||
const external = await findExternalEntry(handlerDesktopId);
|
||||
if (external) {
|
||||
/** @type {DesktopIntegrationIssue[]} */
|
||||
const issues = [];
|
||||
if (!external.acceptsUrl || !external.handlesProtocol) issues.push("desktop-entry");
|
||||
if (handlerDesktopId !== external.desktopId) issues.push("protocol-handler");
|
||||
return {
|
||||
supported: true,
|
||||
state: issues.length ? "needs_repair" : "managed_externally",
|
||||
ownership: "external",
|
||||
appImagePath,
|
||||
desktopEntryPath: external.path,
|
||||
handlerDesktopId,
|
||||
issues,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
supported: true,
|
||||
state: "not_integrated",
|
||||
ownership: "none",
|
||||
appImagePath,
|
||||
desktopEntryPath: null,
|
||||
handlerDesktopId,
|
||||
issues: ["desktop-entry", "icon", "protocol-handler"],
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshDesktopCaches() {
|
||||
await runCommand("update-desktop-database", [path.join(dataHome, "applications")]);
|
||||
await runCommand("gtk-update-icon-cache", ["-f", "-t", path.join(dataHome, "icons", "hicolor")]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ useExternalLauncher?: boolean }} [options]
|
||||
* @returns {Promise<DesktopIntegrationResult>}
|
||||
*/
|
||||
async function install(options = {}) {
|
||||
if (!supported) {
|
||||
return { ok: false, status: unsupportedStatus(), error: "Desktop integration is available only for packaged Linux AppImages." };
|
||||
}
|
||||
const before = await getStatus();
|
||||
if (before.ownership === "external" && options.useExternalLauncher !== true) {
|
||||
return {
|
||||
ok: false,
|
||||
status: before,
|
||||
error: "This AppImage is managed by an external integration tool.",
|
||||
};
|
||||
}
|
||||
if (before.ownership === "external") {
|
||||
if (before.issues.includes("desktop-entry")) {
|
||||
return {
|
||||
ok: false,
|
||||
status: before,
|
||||
error: "The external launcher cannot accept browser callback URLs. Repair it with the tool that manages this AppImage.",
|
||||
};
|
||||
}
|
||||
if (!before.desktopEntryPath) {
|
||||
return {
|
||||
ok: false,
|
||||
status: before,
|
||||
error: "The external launcher path is unavailable.",
|
||||
};
|
||||
}
|
||||
const registration = await runCommand("xdg-mime", [
|
||||
"default",
|
||||
path.basename(before.desktopEntryPath),
|
||||
OPENWORK_PROTOCOL_MIME,
|
||||
]);
|
||||
if (!registration.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
status: await getStatus(),
|
||||
error: registration.stderr || "xdg-mime could not register the external launcher.",
|
||||
};
|
||||
}
|
||||
const status = await getStatus();
|
||||
if (status.state === "managed_externally") return { ok: true, status };
|
||||
return {
|
||||
ok: false,
|
||||
status,
|
||||
error: "The external launcher was not selected for openwork:// callbacks.",
|
||||
};
|
||||
}
|
||||
if (
|
||||
await fileExists(desktopEntryPath)
|
||||
&& before.ownership !== "openwork"
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
status: before,
|
||||
error: "The canonical OpenWork launcher is externally managed and will not be overwritten.",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
for (const size of ICON_SIZES) {
|
||||
await mkdir(path.dirname(iconPaths[size]), { recursive: true });
|
||||
await copyFile(iconSources[size], iconPaths[size]);
|
||||
}
|
||||
await atomicWrite(desktopEntryPath, buildOpenworkDesktopEntry({
|
||||
appImagePath,
|
||||
appName,
|
||||
appVersion: app.getVersion(),
|
||||
distribution,
|
||||
}));
|
||||
|
||||
const state = await readState();
|
||||
if (before.handlerDesktopId && before.handlerDesktopId !== OPENWORK_DESKTOP_ID) {
|
||||
state.previousProtocolHandler = before.handlerDesktopId;
|
||||
}
|
||||
state.dismissedAppImages = state.dismissedAppImages.filter((candidate) => candidate !== appImagePath);
|
||||
await writeState(state);
|
||||
|
||||
await refreshDesktopCaches();
|
||||
const registration = await runCommand("xdg-mime", ["default", OPENWORK_DESKTOP_ID, OPENWORK_PROTOCOL_MIME]);
|
||||
if (!registration.ok) {
|
||||
throw new Error(registration.stderr || "xdg-mime could not register openwork://.");
|
||||
}
|
||||
const status = await getStatus();
|
||||
if (status.state !== "integrated") {
|
||||
throw new Error("The desktop entry was installed, but the desktop did not select it as the openwork:// handler.");
|
||||
}
|
||||
return { ok: true, status };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
status: await getStatus(),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {Promise<DesktopIntegrationResult>} */
|
||||
async function remove() {
|
||||
if (!supported) {
|
||||
return { ok: false, status: unsupportedStatus(), error: "Desktop integration is unavailable." };
|
||||
}
|
||||
const before = await getStatus();
|
||||
if (before.ownership !== "openwork") {
|
||||
return {
|
||||
ok: false,
|
||||
status: before,
|
||||
error: before.ownership === "external"
|
||||
? "Remove this AppImage with the tool that manages it."
|
||||
: "OpenWork does not own a desktop integration to remove.",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const state = await readState();
|
||||
await rm(desktopEntryPath, { force: true });
|
||||
await Promise.all(Object.values(iconPaths).map((target) => rm(target, { force: true })));
|
||||
|
||||
if (before.handlerDesktopId === OPENWORK_DESKTOP_ID) {
|
||||
const previousPath = await locateDesktopEntry(state.previousProtocolHandler);
|
||||
if (state.previousProtocolHandler && previousPath) {
|
||||
await runCommand("xdg-mime", ["default", state.previousProtocolHandler, OPENWORK_PROTOCOL_MIME]);
|
||||
} else {
|
||||
await removeDesktopAssociation(path.join(configHome, "mimeapps.list"), OPENWORK_DESKTOP_ID);
|
||||
await removeDesktopAssociation(path.join(dataHome, "applications", "mimeapps.list"), OPENWORK_DESKTOP_ID);
|
||||
}
|
||||
}
|
||||
|
||||
state.previousProtocolHandler = null;
|
||||
if (!state.dismissedAppImages.includes(appImagePath)) state.dismissedAppImages.push(appImagePath);
|
||||
await writeState(state);
|
||||
await refreshDesktopCaches();
|
||||
return { ok: true, status: await getStatus() };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
status: await getStatus(),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function shouldSkipPrompt() {
|
||||
if (!supported) return true;
|
||||
const state = await readState();
|
||||
return state.dismissedAppImages.includes(appImagePath);
|
||||
}
|
||||
|
||||
async function rememberPromptDismissal() {
|
||||
if (!supported) return;
|
||||
const state = await readState();
|
||||
if (!state.dismissedAppImages.includes(appImagePath)) state.dismissedAppImages.push(appImagePath);
|
||||
await writeState(state);
|
||||
}
|
||||
|
||||
async function maybePrompt(window) {
|
||||
const status = await getStatus();
|
||||
if (
|
||||
!status.supported
|
||||
|| status.state === "integrated"
|
||||
|| status.state === "managed_externally"
|
||||
|| status.ownership === "external"
|
||||
) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// The user already accepted this integration, so drift in the files OpenWork
|
||||
// owns is maintenance rather than a new decision. A self-update lands under a
|
||||
// new versioned filename and a moved AppImage changes its path; both stale the
|
||||
// launcher. Repair silently instead of prompting after every release.
|
||||
if (status.ownership === "openwork") {
|
||||
const repaired = await install();
|
||||
if (!repaired.ok) {
|
||||
console.warn("[desktop-integration] silent repair failed", repaired.error);
|
||||
}
|
||||
return repaired.status;
|
||||
}
|
||||
|
||||
if (await shouldSkipPrompt()) return status;
|
||||
|
||||
const { response, checkboxChecked } = await dialog.showMessageBox(window, {
|
||||
type: "question",
|
||||
title: "Add OpenWork to your applications?",
|
||||
message: "Add OpenWork to your application launcher and register browser sign-in callbacks?",
|
||||
detail: `The launcher will use this AppImage in its current location:\n${appImagePath}\n\nIf you move or update it later, OpenWork repairs the launcher automatically. You can change or remove this in Settings → Preferences → AppImage desktop integration.`,
|
||||
buttons: ["Not now", "Integrate"],
|
||||
defaultId: 1,
|
||||
cancelId: 0,
|
||||
checkboxLabel: "Don’t ask again for this AppImage",
|
||||
checkboxChecked: false,
|
||||
});
|
||||
|
||||
if (response !== 1) {
|
||||
if (checkboxChecked) await rememberPromptDismissal();
|
||||
return status;
|
||||
}
|
||||
|
||||
const result = await install();
|
||||
if (!result.ok) {
|
||||
await dialog.showMessageBox(window, {
|
||||
type: "warning",
|
||||
title: "Desktop integration failed",
|
||||
message: "OpenWork could not complete desktop integration.",
|
||||
detail: result.error ?? "Unknown error",
|
||||
buttons: ["OK"],
|
||||
});
|
||||
}
|
||||
return result.status;
|
||||
}
|
||||
|
||||
return {
|
||||
getStatus,
|
||||
install,
|
||||
remove,
|
||||
maybePrompt,
|
||||
paths: {
|
||||
dataHome,
|
||||
configHome,
|
||||
desktopEntryPath,
|
||||
iconPaths,
|
||||
statePath,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user