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,120 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { afterEach, beforeEach, describe, it } from "node:test";
|
||||
import { createNotifier, detectNotificationBackend } from "../src/terminal.ts";
|
||||
import type { NotifyConfig } from "../src/types.ts";
|
||||
|
||||
const ESC = "\x1b";
|
||||
const ST = `${ESC}\\`;
|
||||
const savedEnv = {
|
||||
KITTY_WINDOW_ID: process.env.KITTY_WINDOW_ID,
|
||||
TMUX: process.env.TMUX,
|
||||
};
|
||||
|
||||
let originalWrite: typeof process.stdout.write;
|
||||
let output = "";
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.KITTY_WINDOW_ID;
|
||||
delete process.env.TMUX;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.stdout.write = originalWrite;
|
||||
output = "";
|
||||
for (const [key, value] of Object.entries(savedEnv)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
function captureStdout(): void {
|
||||
originalWrite = process.stdout.write;
|
||||
process.stdout.write = ((chunk: string | Uint8Array) => {
|
||||
output += chunk.toString();
|
||||
return true;
|
||||
}) as typeof process.stdout.write;
|
||||
}
|
||||
|
||||
function config(title = "Title", body = "Body"): NotifyConfig {
|
||||
return {
|
||||
enabled: true,
|
||||
title,
|
||||
body,
|
||||
durationMs: 5_000,
|
||||
minDurationMs: 3_000,
|
||||
notificationId: "pi-session:unsafe",
|
||||
visibility: "unfocused",
|
||||
action: "focus",
|
||||
};
|
||||
}
|
||||
|
||||
function captureSpawn(): { calls: unknown[][]; spawn: typeof import("node:child_process").spawn } {
|
||||
const calls: unknown[][] = [];
|
||||
const spawn = ((...args: unknown[]) => {
|
||||
calls.push(args);
|
||||
return { on() {}, unref() {} };
|
||||
}) as unknown as typeof import("node:child_process").spawn;
|
||||
return { calls, spawn };
|
||||
}
|
||||
|
||||
describe("terminal notifications", () => {
|
||||
it("writes encoded Kitty OSC 99 payloads with exact-window focus metadata", () => {
|
||||
process.env.KITTY_WINDOW_ID = "1";
|
||||
const notifier = createNotifier();
|
||||
captureStdout();
|
||||
notifier.sendNotification(config("π - pi-notify", "done\nnow"));
|
||||
|
||||
const title = Buffer.from("π - pi-notify", "utf8").toString("base64");
|
||||
const body = Buffer.from("done\nnow", "utf8").toString("base64");
|
||||
assert.equal(
|
||||
output,
|
||||
`${ESC}]99;i=pi-session-unsafe:d=0:e=1:o=unfocused:a=focus;${title}${ST}` +
|
||||
`${ESC}]99;i=pi-session-unsafe:p=body:e=1;${body}${ST}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("wraps Kitty OSC 99 sequences for tmux passthrough", () => {
|
||||
process.env.TMUX = "/tmp/tmux";
|
||||
const notifier = createNotifier({ readTmuxClientInfo: () => "xterm-kitty xterm-kitty" });
|
||||
captureStdout();
|
||||
notifier.sendNotification(config("Pi", "done"));
|
||||
|
||||
const title = Buffer.from("Pi", "utf8").toString("base64");
|
||||
const body = Buffer.from("done", "utf8").toString("base64");
|
||||
const titleSequence = `${ESC}]99;i=pi-session-unsafe:d=0:e=1:o=unfocused:a=focus;${title}${ST}`.replaceAll(ESC, ESC + ESC);
|
||||
const bodySequence = `${ESC}]99;i=pi-session-unsafe:p=body:e=1;${body}${ST}`.replaceAll(ESC, ESC + ESC);
|
||||
assert.equal(output, `${ESC}Ptmux;${titleSequence}${ST}${ESC}Ptmux;${bodySequence}${ST}`);
|
||||
});
|
||||
|
||||
it("uses AppleScript safely on macOS when Kitty is not detected", () => {
|
||||
const { calls, spawn } = captureSpawn();
|
||||
const notifier = createNotifier({ spawn, platform: "darwin" });
|
||||
captureStdout();
|
||||
notifier.sendNotification(config('Pi "notify"', "done\\now"));
|
||||
|
||||
assert.equal(output, "");
|
||||
assert.deepEqual(calls, [
|
||||
[
|
||||
"osascript",
|
||||
["-e", 'display notification "done\\\\now" with title "Pi \\"notify\\""'],
|
||||
{ shell: false, detached: true, stdio: "ignore" },
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not let a stale Kitty variable override the active tmux client", () => {
|
||||
process.env.TMUX = "/tmp/tmux";
|
||||
process.env.KITTY_WINDOW_ID = "stale";
|
||||
assert.equal(
|
||||
detectNotificationBackend(() => "xterm-ghostty ghostty", "darwin"),
|
||||
"apple-script",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses a sanitized OSC 777 fallback outside macOS", () => {
|
||||
const notifier = createNotifier({ platform: "linux" });
|
||||
captureStdout();
|
||||
notifier.sendNotification(config("Pi;bad", "done\u0007now"));
|
||||
assert.equal(output, `${ESC}]777;notify;Pi bad;done now\x07`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user