Files
OmniRoute/bin/cli/tray/trayWin.ts
diegosouzapw 77a4429bf4 feat(cli): add standalone system tray with PowerShell fallback on Windows
Cross-platform system tray for `omniroute --tray`: Windows uses a
PowerShell NotifyIcon script (zero native binary, AV-safe); macOS/Linux
use systray2 lazy-installed at runtime into ~/.omniroute/runtime/.
Includes per-platform autostart (LaunchAgent / .desktop / registry) and
`omniroute config tray <enable|disable>` CLI command.

DISPLAY added to env-sync allowlist as an OS/X11 variable, not an
OmniRoute config variable.
2026-05-14 22:42:01 -03:00

92 lines
2.1 KiB
TypeScript

import { spawn, type ChildProcess } from "node:child_process";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const PS1_PATH = join(__dirname, "tray.ps1");
export interface WinTrayEvent {
type: "click" | "ready" | "error";
index?: number;
error?: string;
}
export interface WinTrayOptions {
iconPath: string;
tooltip: string;
onEvent: (evt: WinTrayEvent) => void;
}
export interface WinTrayHandle {
update(items: Array<{ title: string; enabled: boolean }>): void;
setTooltip(text: string): void;
destroy(): void;
}
export function initWindowsTray(opts: WinTrayOptions): WinTrayHandle | null {
let child: ChildProcess;
try {
child = spawn(
"powershell.exe",
[
"-NoLogo",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
PS1_PATH,
"-IconPath",
opts.iconPath,
"-Tooltip",
opts.tooltip,
],
{ stdio: ["pipe", "pipe", "pipe"], windowsHide: true }
);
} catch (err) {
opts.onEvent({ type: "error", error: String(err) });
return null;
}
child.stdout?.setEncoding("utf-8");
let buffer = "";
child.stdout?.on("data", (chunk: string) => {
buffer += chunk;
let nl: number;
while ((nl = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, nl).trim();
buffer = buffer.slice(nl + 1);
if (!line) continue;
try {
const evt = JSON.parse(line) as WinTrayEvent;
opts.onEvent(evt);
} catch {
// ignore malformed JSON lines
}
}
});
child.on("error", (err) => opts.onEvent({ type: "error", error: err.message }));
function send(cmd: object): void {
if (!child.stdin?.writable) return;
child.stdin.write(JSON.stringify(cmd) + "\n");
}
return {
update(items) {
send({ type: "setMenu", items });
},
setTooltip(text) {
send({ type: "setTooltip", text });
},
destroy() {
try {
send({ type: "quit" });
} catch {
// already dead
}
setTimeout(() => child.kill(), 500);
},
};
}