mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
import { execFileSync, spawn } from "node:child_process";
|
|
import type { NotificationBackend, NotifyConfig } from "./types.ts";
|
|
|
|
const ESC = "\x1b";
|
|
const BEL = "\x07";
|
|
const ST = `${ESC}\\`;
|
|
|
|
type NotifierDependencies = {
|
|
spawn: typeof spawn;
|
|
readTmuxClientInfo: () => string | undefined;
|
|
platform: NodeJS.Platform;
|
|
};
|
|
|
|
export function createNotifier(dependencies: Partial<NotifierDependencies> = {}) {
|
|
const spawnCommand = dependencies.spawn ?? spawn;
|
|
const tmuxClientInfo = dependencies.readTmuxClientInfo ?? readTmuxClientInfo;
|
|
const platform = dependencies.platform ?? process.platform;
|
|
|
|
return {
|
|
getBackend(): NotificationBackend {
|
|
return detectNotificationBackend(tmuxClientInfo, platform);
|
|
},
|
|
sendNotification(config: NotifyConfig): void {
|
|
try {
|
|
const backend = detectNotificationBackend(tmuxClientInfo, platform);
|
|
if (backend === "kitty") notifyKitty(config);
|
|
else if (backend === "apple-script") notifyAppleScript(config.title, config.body, spawnCommand);
|
|
else notifyOsc777(config.title, config.body);
|
|
|
|
runSoundHook(config.soundCommand, spawnCommand);
|
|
} catch {}
|
|
},
|
|
};
|
|
}
|
|
|
|
const defaultNotifier = createNotifier();
|
|
|
|
export function sendNotification(config: NotifyConfig): void {
|
|
defaultNotifier.sendNotification(config);
|
|
}
|
|
|
|
export function getNotificationBackend(): NotificationBackend {
|
|
return defaultNotifier.getBackend();
|
|
}
|
|
|
|
export function detectNotificationBackend(
|
|
readClientInfo: () => string | undefined = readTmuxClientInfo,
|
|
platform: NodeJS.Platform = process.platform,
|
|
): NotificationBackend {
|
|
if (isKitty(readClientInfo)) return "kitty";
|
|
return platform === "darwin" ? "apple-script" : "osc-777";
|
|
}
|
|
|
|
function isKitty(readClientInfo: () => string | undefined): boolean {
|
|
if (!process.env.TMUX) return Boolean(process.env.KITTY_WINDOW_ID);
|
|
|
|
const info = readClientInfo()?.toLowerCase();
|
|
if (info) return info.includes("kitty");
|
|
return Boolean(process.env.KITTY_WINDOW_ID);
|
|
}
|
|
|
|
function readTmuxClientInfo(): string | undefined {
|
|
try {
|
|
return execFileSync("tmux", ["display-message", "-p", "#{client_termname} #{client_termtype}"], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
});
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function notifyKitty(config: NotifyConfig): void {
|
|
const safeTitle = Buffer.from(config.title, "utf8").toString("base64");
|
|
const safeBody = Buffer.from(config.body, "utf8").toString("base64");
|
|
const id = sanitizeMetadataValue(config.notificationId);
|
|
writeSequence(
|
|
`${ESC}]99;i=${id}:d=0:e=1:o=${config.visibility}:a=${config.action};${safeTitle}${ST}`,
|
|
);
|
|
writeSequence(`${ESC}]99;i=${id}:p=body:e=1;${safeBody}${ST}`);
|
|
}
|
|
|
|
function notifyAppleScript(title: string, body: string, spawnCommand: typeof spawn): void {
|
|
const script = `display notification ${appleScriptString(body)} with title ${appleScriptString(title)}`;
|
|
spawnDetached(spawnCommand, "osascript", ["-e", script]);
|
|
}
|
|
|
|
function notifyOsc777(title: string, body: string): void {
|
|
process.stdout.write(`${ESC}]777;notify;${sanitizeOsc777(title)};${sanitizeOsc777(body)}${BEL}`);
|
|
}
|
|
|
|
function writeSequence(sequence: string): void {
|
|
process.stdout.write(wrapForTmux(sequence));
|
|
}
|
|
|
|
function wrapForTmux(sequence: string): string {
|
|
if (!process.env.TMUX) return sequence;
|
|
return `${ESC}Ptmux;${sequence.replaceAll(ESC, ESC + ESC)}${ST}`;
|
|
}
|
|
|
|
function sanitizeMetadataValue(value: string): string {
|
|
const sanitized = value.replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 128);
|
|
return sanitized || `pi-${process.pid}`;
|
|
}
|
|
|
|
function sanitizeOsc777(value: string): string {
|
|
return value.replace(/[\x00-\x1f\x7f;]/g, " ").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function appleScriptString(value: string): string {
|
|
return `"${value.replace(/[\x00-\x1f\x7f]/g, " ").replace(/\\/g, "\\\\").replace(/"/g, '\\"').trim()}"`;
|
|
}
|
|
|
|
function runSoundHook(command: string | undefined, spawnCommand: typeof spawn): void {
|
|
if (!command) return;
|
|
spawnDetached(spawnCommand, command, [], true);
|
|
}
|
|
|
|
function spawnDetached(spawnCommand: typeof spawn, command: string, args: string[], shell = false): void {
|
|
const child = spawnCommand(command, args, {
|
|
shell,
|
|
detached: true,
|
|
stdio: "ignore",
|
|
});
|
|
child.on?.("error", () => {});
|
|
child.unref();
|
|
}
|