136 lines
4.5 KiB
JavaScript
136 lines
4.5 KiB
JavaScript
import { existsSync, readFileSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { isAutostartEnabled } from "./autostart.mjs";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
const MENU_INDEX = { STATUS: 0, DASHBOARD: 1, LOGS: 2, AUTOSTART: 3, QUIT: 4 };
|
|
|
|
export function isTraySupported() {
|
|
const p = process.platform;
|
|
if (!["darwin", "linux", "win32"].includes(p)) return false;
|
|
if (p === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
|
|
return true;
|
|
}
|
|
|
|
// systray2 is NOT a static dependency — it is lazily installed into
|
|
// ~/.omniroute/runtime by trayRuntime.ts (loadSystray). The previous inline
|
|
// loader called `require("module")`, which throws `ReferenceError: require is
|
|
// not defined` in this ESM file (package "type":"module"); the throw was
|
|
// silently swallowed, so the tray never appeared on macOS/Linux with no error
|
|
// printed (#4605, regressed in v3.8.34). Delegate to the runtime loader, which
|
|
// resolves systray2 from the runtime dir and surfaces install/import failures.
|
|
async function loadSystray2() {
|
|
const { loadSystray } = await import("../runtime/trayRuntime.ts");
|
|
return loadSystray();
|
|
}
|
|
|
|
function getIconBase64() {
|
|
// Icon ships at bin/cli/tray/icon.png — the previous "icons/icon.png" path
|
|
// never existed, so the tray was created with an empty icon (#4605).
|
|
const iconPath = join(__dirname, "icon.png");
|
|
if (existsSync(iconPath)) return readFileSync(iconPath).toString("base64");
|
|
return "";
|
|
}
|
|
|
|
export async function initSystrayUnix(
|
|
{ port, onQuit, onOpenDashboard, onShowLogs },
|
|
loadCtor = loadSystray2
|
|
) {
|
|
const SysTray = await loadCtor();
|
|
if (!SysTray) return null;
|
|
|
|
const autostartEnabled = isAutostartEnabled();
|
|
const items = [
|
|
{ title: `OmniRoute • port ${port}`, tooltip: "Server running", enabled: false },
|
|
{ title: "Open Dashboard", enabled: true },
|
|
{ title: "Show Logs", enabled: true },
|
|
{
|
|
title: autostartEnabled ? "✓ Auto-start (click to disable)" : "Enable Auto-start",
|
|
enabled: true,
|
|
},
|
|
{ title: "Quit OmniRoute", enabled: true },
|
|
];
|
|
|
|
let tray;
|
|
try {
|
|
tray = new SysTray({
|
|
menu: {
|
|
icon: getIconBase64(),
|
|
// isTemplateIcon must be false: icon.png is a full-color RGBA logo, and
|
|
// macOS template mode uses only the alpha channel → a solid white square
|
|
// (the icon looked "missing" even when the tray loaded). (PR #1080)
|
|
isTemplateIcon: false,
|
|
title: "",
|
|
tooltip: `OmniRoute — port ${port}`,
|
|
items,
|
|
},
|
|
debug: false,
|
|
copyDir: false,
|
|
});
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
tray.onClick(async (action) => {
|
|
if (action.seq_id === MENU_INDEX.DASHBOARD) {
|
|
onOpenDashboard?.();
|
|
} else if (action.seq_id === MENU_INDEX.LOGS) {
|
|
onShowLogs?.();
|
|
} else if (action.seq_id === MENU_INDEX.AUTOSTART) {
|
|
const { enable, disable, isAutostartEnabled: isEnabled } = await import("./autostart.mjs");
|
|
const wasOn = isEnabled();
|
|
if (wasOn) disable();
|
|
else enable();
|
|
const nowOn = !wasOn;
|
|
tray.sendAction({
|
|
type: "update-item",
|
|
item: {
|
|
title: nowOn ? "✓ Auto-start (click to disable)" : "Enable Auto-start",
|
|
enabled: true,
|
|
},
|
|
seq_id: MENU_INDEX.AUTOSTART,
|
|
});
|
|
} else if (action.seq_id === MENU_INDEX.QUIT) {
|
|
onQuit?.();
|
|
}
|
|
});
|
|
|
|
tray.ready().catch((err) => {
|
|
process.stderr.write(`[omniroute][tray] systray2 failed: ${err?.message ?? String(err)}\n`);
|
|
});
|
|
|
|
return tray;
|
|
}
|
|
|
|
/**
|
|
* Resolve the Go systray2 child subprocess PID from a tray instance.
|
|
* systray2 exposes the spawned binary either as the `_process` field or via a
|
|
* `process()` accessor depending on version. Returns the numeric PID or null.
|
|
*/
|
|
export function getSystrayChildPid(tray) {
|
|
if (!tray) return null;
|
|
try {
|
|
const proc = tray._process || (typeof tray.process === "function" ? tray.process() : null);
|
|
if (proc && typeof proc.pid === "number") return proc.pid;
|
|
} catch {}
|
|
return null;
|
|
}
|
|
|
|
export function killSystrayUnix(tray) {
|
|
try {
|
|
// systray2.kill(false) closes the IPC channel but leaves the Go tray binary
|
|
// subprocess running, which keeps an orphan NSStatusItem on macOS and blocks
|
|
// a freshly spawned tray (e.g. on respawn / hide-to-tray) from registering.
|
|
// SIGKILL the child PID directly first, then close IPC.
|
|
const pid = getSystrayChildPid(tray);
|
|
if (pid) {
|
|
try {
|
|
process.kill(pid, "SIGKILL");
|
|
} catch {}
|
|
}
|
|
tray.kill(false);
|
|
} catch {}
|
|
}
|