klaudio 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/notify.js CHANGED
@@ -1,139 +1,139 @@
1
- import { spawn } from "node:child_process";
2
- import { platform } from "node:os";
3
-
4
- /**
5
- * Send a native OS notification (fire-and-forget).
6
- * Click-to-focus: activates the terminal or editor that triggered it.
7
- *
8
- * Windows: WinRT toast (Win10+), focuses Windows Terminal or VS Code on click
9
- * macOS: terminal-notifier (if installed) or osascript fallback
10
- * Linux: notify-send
11
- */
12
- export function sendNotification(title, body) {
13
- const os = platform();
14
- try {
15
- if (os === "win32") return notifyWindows(title, body);
16
- if (os === "darwin") return notifyMac(title, body);
17
- return notifyLinux(title, body);
18
- } catch {
19
- return Promise.resolve();
20
- }
21
- }
22
-
23
- /**
24
- * Detect the terminal/editor environment.
25
- */
26
- function detectTerminal() {
27
- const tp = process.env.TERM_PROGRAM;
28
- if (tp === "vscode") return "vscode";
29
- if (tp === "cursor") return "cursor";
30
- if (tp === "iTerm.app") return "iterm";
31
- if (tp === "Apple_Terminal") return "terminal";
32
- if (process.env.WT_SESSION) return "windows-terminal";
33
- // Fallback: check PATH for clues (hooks inherit the terminal's env)
34
- const path = process.env.PATH || "";
35
- if (/cursor[/\\]/i.test(path) && /resources[/\\]app[/\\]bin/i.test(path)) return "cursor";
36
- if (/VS Code[/\\]bin/i.test(path) || /Code[/\\]bin/i.test(path)) return "vscode";
37
- return "unknown";
38
- }
39
-
40
- function escapeXml(s) {
41
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
42
- }
43
-
44
- // ── Windows ──────────────────────────────────────────────────────
45
-
46
- function notifyWindows(title, body) {
47
- const safeTitle = escapeXml(title);
48
- const safeBody = escapeXml(body);
49
-
50
- // Determine activation strategy based on the terminal we're running in
51
- let toastAttrs = "";
52
- let appId;
53
- const terminal = detectTerminal();
54
-
55
- // Windows requires a registered AUMID for toasts to actually show.
56
- // Use Windows Terminal's AUMID as default (works on most Win10+ systems).
57
- // For VS Code/Cursor, also add protocol activation so clicking focuses the editor.
58
- appId = "Microsoft.WindowsTerminal_8wekyb3d8bbwe!App";
59
-
60
- if (terminal === "vscode" || terminal === "cursor") {
61
- const protocol = terminal === "cursor" ? "cursor://" : "vscode://";
62
- toastAttrs = ` activationType="protocol" launch="${protocol}"`;
63
- }
64
-
65
- const toastXml = `<toast${toastAttrs}><visual><binding template="ToastGeneric"><text>${safeTitle}</text><text>${safeBody}</text></binding></visual></toast>`;
66
-
67
- // PowerShell script: show WinRT toast notification
68
- // Use -EncodedCommand to avoid all escaping issues with special chars
69
- const ps = `\
70
- [void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
71
- [void][Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime]
72
- $x = [Windows.Data.Xml.Dom.XmlDocument]::new()
73
- $x.LoadXml('${toastXml.replace(/'/g, "''")}')
74
- $t = [Windows.UI.Notifications.ToastNotification]::new($x)
75
- [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${appId}').Show($t)`;
76
-
77
- const encoded = Buffer.from(ps, "utf16le").toString("base64");
78
- const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], {
79
- windowsHide: true,
80
- detached: true,
81
- stdio: "ignore",
82
- });
83
- child.unref();
84
- return Promise.resolve();
85
- }
86
-
87
- // ── macOS ────────────────────────────────────────────────────────
88
-
89
- function notifyMac(title, body) {
90
- try {
91
- // Determine which app to activate when the notification is clicked
92
- const terminal = detectTerminal();
93
- const bundleIds = {
94
- vscode: "com.microsoft.VSCode",
95
- cursor: "com.todesktop.230313mzl4w4u92",
96
- iterm: "com.googlecode.iterm2",
97
- terminal: "com.apple.Terminal",
98
- };
99
- const bundleId = bundleIds[terminal] || "com.apple.Terminal";
100
-
101
- // Try terminal-notifier first (best UX: click-to-focus), fall back to osascript
102
- return new Promise((resolve) => {
103
- const child = spawn("terminal-notifier", [
104
- "-title", title, "-message", body,
105
- "-activate", bundleId, "-sender", bundleId,
106
- ], { stdio: "ignore" });
107
-
108
- child.on("error", () => {
109
- // terminal-notifier not installed — fall back to osascript
110
- try {
111
- const safeTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
112
- const safeBody = body.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
113
- const script = `display notification "${safeBody}" with title "${safeTitle}"`;
114
- const child2 = spawn("osascript", ["-e", script], {
115
- stdio: "ignore",
116
- detached: true,
117
- });
118
- child2.unref();
119
- } catch { /* ignore */ }
120
- resolve();
121
- });
122
-
123
- child.on("close", () => resolve());
124
- });
125
- } catch {
126
- return Promise.resolve();
127
- }
128
- }
129
-
130
- // ── Linux ────────────────────────────────────────────────────────
131
-
132
- function notifyLinux(title, body) {
133
- const child = spawn("notify-send", ["-a", "klaudio", title, body], {
134
- stdio: "ignore",
135
- detached: true,
136
- });
137
- child.unref();
138
- return Promise.resolve();
139
- }
1
+ import { spawn } from "node:child_process";
2
+ import { platform } from "node:os";
3
+
4
+ /**
5
+ * Send a native OS notification (fire-and-forget).
6
+ * Click-to-focus: activates the terminal or editor that triggered it.
7
+ *
8
+ * Windows: WinRT toast (Win10+), focuses Windows Terminal or VS Code on click
9
+ * macOS: terminal-notifier (if installed) or osascript fallback
10
+ * Linux: notify-send
11
+ */
12
+ export function sendNotification(title, body) {
13
+ const os = platform();
14
+ try {
15
+ if (os === "win32") return notifyWindows(title, body);
16
+ if (os === "darwin") return notifyMac(title, body);
17
+ return notifyLinux(title, body);
18
+ } catch {
19
+ return Promise.resolve();
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Detect the terminal/editor environment.
25
+ */
26
+ function detectTerminal() {
27
+ const tp = process.env.TERM_PROGRAM;
28
+ if (tp === "vscode") return "vscode";
29
+ if (tp === "cursor") return "cursor";
30
+ if (tp === "iTerm.app") return "iterm";
31
+ if (tp === "Apple_Terminal") return "terminal";
32
+ if (process.env.WT_SESSION) return "windows-terminal";
33
+ // Fallback: check PATH for clues (hooks inherit the terminal's env)
34
+ const path = process.env.PATH || "";
35
+ if (/cursor[/\\]/i.test(path) && /resources[/\\]app[/\\]bin/i.test(path)) return "cursor";
36
+ if (/VS Code[/\\]bin/i.test(path) || /Code[/\\]bin/i.test(path)) return "vscode";
37
+ return "unknown";
38
+ }
39
+
40
+ function escapeXml(s) {
41
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
42
+ }
43
+
44
+ // ── Windows ──────────────────────────────────────────────────────
45
+
46
+ function notifyWindows(title, body) {
47
+ const safeTitle = escapeXml(title);
48
+ const safeBody = escapeXml(body);
49
+
50
+ // Determine activation strategy based on the terminal we're running in
51
+ let toastAttrs = "";
52
+ let appId;
53
+ const terminal = detectTerminal();
54
+
55
+ // Windows requires a registered AUMID for toasts to actually show.
56
+ // Use Windows Terminal's AUMID as default (works on most Win10+ systems).
57
+ // For VS Code/Cursor, also add protocol activation so clicking focuses the editor.
58
+ appId = "Microsoft.WindowsTerminal_8wekyb3d8bbwe!App";
59
+
60
+ if (terminal === "vscode" || terminal === "cursor") {
61
+ const protocol = terminal === "cursor" ? "cursor://" : "vscode://";
62
+ toastAttrs = ` activationType="protocol" launch="${protocol}"`;
63
+ }
64
+
65
+ const toastXml = `<toast${toastAttrs}><visual><binding template="ToastGeneric"><text>${safeTitle}</text><text>${safeBody}</text></binding></visual></toast>`;
66
+
67
+ // PowerShell script: show WinRT toast notification
68
+ // Use -EncodedCommand to avoid all escaping issues with special chars
69
+ const ps = `\
70
+ [void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
71
+ [void][Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime]
72
+ $x = [Windows.Data.Xml.Dom.XmlDocument]::new()
73
+ $x.LoadXml('${toastXml.replace(/'/g, "''")}')
74
+ $t = [Windows.UI.Notifications.ToastNotification]::new($x)
75
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${appId}').Show($t)`;
76
+
77
+ const encoded = Buffer.from(ps, "utf16le").toString("base64");
78
+ const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], {
79
+ windowsHide: true,
80
+ detached: true,
81
+ stdio: "ignore",
82
+ });
83
+ child.unref();
84
+ return Promise.resolve();
85
+ }
86
+
87
+ // ── macOS ────────────────────────────────────────────────────────
88
+
89
+ function notifyMac(title, body) {
90
+ try {
91
+ // Determine which app to activate when the notification is clicked
92
+ const terminal = detectTerminal();
93
+ const bundleIds = {
94
+ vscode: "com.microsoft.VSCode",
95
+ cursor: "com.todesktop.230313mzl4w4u92",
96
+ iterm: "com.googlecode.iterm2",
97
+ terminal: "com.apple.Terminal",
98
+ };
99
+ const bundleId = bundleIds[terminal] || "com.apple.Terminal";
100
+
101
+ // Try terminal-notifier first (best UX: click-to-focus), fall back to osascript
102
+ return new Promise((resolve) => {
103
+ const child = spawn("terminal-notifier", [
104
+ "-title", title, "-message", body,
105
+ "-activate", bundleId, "-sender", bundleId,
106
+ ], { stdio: "ignore" });
107
+
108
+ child.on("error", () => {
109
+ // terminal-notifier not installed — fall back to osascript
110
+ try {
111
+ const safeTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
112
+ const safeBody = body.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
113
+ const script = `display notification "${safeBody}" with title "${safeTitle}"`;
114
+ const child2 = spawn("osascript", ["-e", script], {
115
+ stdio: "ignore",
116
+ detached: true,
117
+ });
118
+ child2.unref();
119
+ } catch { /* ignore */ }
120
+ resolve();
121
+ });
122
+
123
+ child.on("close", () => resolve());
124
+ });
125
+ } catch {
126
+ return Promise.resolve();
127
+ }
128
+ }
129
+
130
+ // ── Linux ────────────────────────────────────────────────────────
131
+
132
+ function notifyLinux(title, body) {
133
+ const child = spawn("notify-send", ["-a", "klaudio", title, body], {
134
+ stdio: "ignore",
135
+ detached: true,
136
+ });
137
+ child.unref();
138
+ return Promise.resolve();
139
+ }