klaudio 0.11.2 → 0.11.3

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,135 +1,138 @@
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
- if (terminal === "vscode" || terminal === "cursor") {
56
- // VS Code / Cursor: use protocol handler to focus the editor on click
57
- const protocol = terminal === "cursor" ? "cursor://" : "vscode://";
58
- toastAttrs = ` activationType="protocol" launch="${protocol}"`;
59
- appId = "klaudio";
60
- } else if (terminal === "windows-terminal") {
61
- // Windows Terminal: use its AUMID so clicking focuses WT
62
- appId = "Microsoft.WindowsTerminal_8wekyb3d8bbwe!App";
63
- } else {
64
- appId = "klaudio";
65
- }
66
-
67
- const toastXml = `<toast${toastAttrs}><visual><binding template="ToastGeneric"><text>${safeTitle}</text><text>${safeBody}</text></binding></visual></toast>`;
68
-
69
- // PowerShell script: show WinRT toast notification
70
- const ps = `\
71
- [void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
72
- [void][Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime]
73
- $x = [Windows.Data.Xml.Dom.XmlDocument]::new()
74
- $x.LoadXml('${toastXml.replace(/'/g, "''")}')
75
- $t = [Windows.UI.Notifications.ToastNotification]::new($x)
76
- [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${appId}').Show($t)`;
77
-
78
- // Run detached so the Node process can exit immediately
79
- const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", ps], {
80
- windowsHide: true,
81
- detached: true,
82
- stdio: "ignore",
83
- });
84
- child.unref();
85
- return Promise.resolve();
86
- }
87
-
88
- // ── macOS ────────────────────────────────────────────────────────
89
-
90
- function notifyMac(title, body) {
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
- "-sound", "default",
107
- ], { stdio: "ignore", timeout: 10000 });
108
-
109
- child.on("error", () => {
110
- // terminal-notifier not installed fall back to osascript
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
- resolve();
120
- });
121
-
122
- child.on("close", () => resolve());
123
- });
124
- }
125
-
126
- // ── Linux ────────────────────────────────────────────────────────
127
-
128
- function notifyLinux(title, body) {
129
- const child = spawn("notify-send", ["-a", "klaudio", title, body], {
130
- stdio: "ignore",
131
- detached: true,
132
- });
133
- child.unref();
134
- return Promise.resolve();
135
- }
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
+ const ps = `\
69
+ [void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
70
+ [void][Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime]
71
+ $x = [Windows.Data.Xml.Dom.XmlDocument]::new()
72
+ $x.LoadXml('${toastXml.replace(/'/g, "''")}')
73
+ $t = [Windows.UI.Notifications.ToastNotification]::new($x)
74
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${appId}').Show($t)`;
75
+
76
+ // Run detached so the Node process can exit immediately
77
+ const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", ps], {
78
+ windowsHide: true,
79
+ detached: true,
80
+ stdio: "ignore",
81
+ });
82
+ child.unref();
83
+ return Promise.resolve();
84
+ }
85
+
86
+ // ── macOS ────────────────────────────────────────────────────────
87
+
88
+ function notifyMac(title, body) {
89
+ try {
90
+ // Determine which app to activate when the notification is clicked
91
+ const terminal = detectTerminal();
92
+ const bundleIds = {
93
+ vscode: "com.microsoft.VSCode",
94
+ cursor: "com.todesktop.230313mzl4w4u92",
95
+ iterm: "com.googlecode.iterm2",
96
+ terminal: "com.apple.Terminal",
97
+ };
98
+ const bundleId = bundleIds[terminal] || "com.apple.Terminal";
99
+
100
+ // Try terminal-notifier first (best UX: click-to-focus), fall back to osascript
101
+ return new Promise((resolve) => {
102
+ const child = spawn("terminal-notifier", [
103
+ "-title", title, "-message", body,
104
+ "-activate", bundleId, "-sender", bundleId,
105
+ ], { stdio: "ignore" });
106
+
107
+ child.on("error", () => {
108
+ // terminal-notifier not installed — fall back to osascript
109
+ try {
110
+ const safeTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
111
+ const safeBody = body.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
112
+ const script = `display notification "${safeBody}" with title "${safeTitle}"`;
113
+ const child2 = spawn("osascript", ["-e", script], {
114
+ stdio: "ignore",
115
+ detached: true,
116
+ });
117
+ child2.unref();
118
+ } catch { /* ignore */ }
119
+ resolve();
120
+ });
121
+
122
+ child.on("close", () => resolve());
123
+ });
124
+ } catch {
125
+ return Promise.resolve();
126
+ }
127
+ }
128
+
129
+ // ── Linux ────────────────────────────────────────────────────────
130
+
131
+ function notifyLinux(title, body) {
132
+ const child = spawn("notify-send", ["-a", "klaudio", title, body], {
133
+ stdio: "ignore",
134
+ detached: true,
135
+ });
136
+ child.unref();
137
+ return Promise.resolve();
138
+ }