mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: add Kitty-aware completion notifications
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user