@pi-unipi/unipi 2.0.3 → 2.0.4

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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @pi-unipi/notify — Focus detection abstraction
3
+ *
4
+ * Unified interface for checking whether the terminal window is the
5
+ * foreground (active) window. Platform-specific implementations are
6
+ * dispatched based on process.platform.
7
+ *
8
+ * Currently implemented:
9
+ * - Windows (win32): calls focus-win.ts
10
+ *
11
+ * Unimplemented platforms always return false (no suppression).
12
+ */
13
+
14
+ import { isWindowFocusedOnWindows } from "./focus-win.js";
15
+
16
+ /**
17
+ * Check whether the current terminal/console window is the foreground
18
+ * (active) window. Used by sendNativeNotification to optionally
19
+ * suppress notifications when the user is already looking at the screen.
20
+ *
21
+ * @returns true if the terminal is the foreground window, false otherwise.
22
+ * On unimplemented platforms, always returns false.
23
+ */
24
+ export async function isWindowFocused(): Promise<boolean> {
25
+ switch (process.platform) {
26
+ case "win32":
27
+ return await isWindowFocusedOnWindows();
28
+ // TODO: macOS — use osascript to check frontmost application
29
+ // TODO: Linux — use xdotool (X11) or per-compositor tool (Wayland)
30
+ default:
31
+ return false;
32
+ }
33
+ }
@@ -8,22 +8,54 @@
8
8
  */
9
9
 
10
10
  import notifier from "node-notifier";
11
+ import { isWindowFocused } from "./focus.js";
11
12
 
12
13
  /** Options for native notification */
13
14
  export interface NativeNotificationOptions {
14
15
  /** Windows appID to show instead of "SnoreToast" */
15
16
  windowsAppId?: string;
17
+ /**
18
+ * When true, suppresses the notification if the terminal window is
19
+ * the foreground (active) window. Only effective on platforms where
20
+ * `isWindowFocused` is implemented (currently Windows).
21
+ */
22
+ suppressWhenFocused?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Thrown by sendNativeNotification when the notification was suppressed
27
+ * because suppressWhenFocused is set and the terminal window is focused.
28
+ *
29
+ * Callers should catch this and treat it as intentional suppression,
30
+ * NOT as a send failure.
31
+ */
32
+ export class SuppressedError extends Error {
33
+ constructor() {
34
+ super("Notification suppressed: terminal window is focused");
35
+ this.name = "SuppressedError";
36
+ }
16
37
  }
17
38
 
18
39
  /**
19
40
  * Send a native OS notification.
20
- * Resolves when notification is shown, rejects on error.
41
+ *
42
+ * When `suppressWhenFocused` is true and `isWindowFocused()` returns true
43
+ * (i.e. the terminal is the foreground window), the notification is
44
+ * suppressed and the promise rejects with SuppressedError.
45
+ *
46
+ * Resolves when notification is shown, rejects with SuppressedError on
47
+ * suppression or with a standard Error on failure.
21
48
  */
22
49
  export async function sendNativeNotification(
23
50
  title: string,
24
51
  message: string,
25
52
  options?: NativeNotificationOptions
26
53
  ): Promise<void> {
54
+ // Suppress if the terminal window is currently focused
55
+ if (options?.suppressWhenFocused && await isWindowFocused()) {
56
+ throw new SuppressedError();
57
+ }
58
+
27
59
  return new Promise((resolve, reject) => {
28
60
  notifier.notify(
29
61
  {
@@ -30,6 +30,7 @@ export const DEFAULT_CONFIG: NotifyConfig = {
30
30
  },
31
31
  native: {
32
32
  enabled: true,
33
+ suppressWhenFocused: false,
33
34
  },
34
35
  gotify: {
35
36
  enabled: false,
@@ -85,7 +85,7 @@ export class NotifySettingsOverlay implements Component {
85
85
  }
86
86
 
87
87
  private get maxItems(): number {
88
- if (this.section === "platforms") return 4; // native, gotify, telegram, ntfy
88
+ if (this.section === "platforms") return 5; // native, gotify, telegram, ntfy + suppress option
89
89
  if (this.section === "recap") return 1; // toggle
90
90
  return Object.keys(this.config.events).length;
91
91
  }
@@ -98,12 +98,17 @@ export class NotifySettingsOverlay implements Component {
98
98
  "telegram",
99
99
  "ntfy",
100
100
  ];
101
- const key = platforms[this.selectedIndex];
102
- if (key === "ntfy") {
103
- // ntfy toggle updates the resolved ntfy config
104
- this.ntfyConfig.enabled = !this.ntfyConfig.enabled;
105
- } else if (key) {
106
- this.config[key].enabled = !this.config[key].enabled;
101
+ if (this.selectedIndex < platforms.length) {
102
+ const key = platforms[this.selectedIndex];
103
+ if (key === "ntfy") {
104
+ // ntfy toggle updates the resolved ntfy config
105
+ this.ntfyConfig.enabled = !this.ntfyConfig.enabled;
106
+ } else if (key) {
107
+ this.config[key].enabled = !this.config[key].enabled;
108
+ }
109
+ } else {
110
+ // suppressWhenFocused toggle (index 4)
111
+ this.config.native.suppressWhenFocused = !this.config.native.suppressWhenFocused;
107
112
  }
108
113
  } else if (this.section === "recap") {
109
114
  this.config.recap.enabled = !this.config.recap.enabled;
@@ -273,6 +278,27 @@ export class NotifySettingsOverlay implements Component {
273
278
  )
274
279
  );
275
280
  }
281
+
282
+ // suppressWhenFocused toggle (index 4)
283
+ {
284
+ const i = platforms.length;
285
+ const isSelected = i === this.selectedIndex;
286
+ const isEnabled = this.config.native.suppressWhenFocused === true;
287
+ const toggleOn = this.fg("success", "●");
288
+ const toggleOff = this.fg("dim", "○");
289
+ const toggle = isEnabled ? toggleOn : toggleOff;
290
+ const label = isSelected
291
+ ? this.bold("Suppress when focused")
292
+ : this.fg("dim", "Suppress when focused");
293
+ const detail = this.fg("dim", isEnabled ? "Windows only — terminal in foreground → skip" : "Windows only");
294
+
295
+ lines.push(
296
+ this.frameLine(
297
+ `${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label} ${detail}`,
298
+ innerWidth
299
+ )
300
+ );
301
+ }
276
302
  }
277
303
 
278
304
  private renderEvents(lines: string[], innerWidth: number): void {
@@ -19,6 +19,12 @@ export interface NativeConfig {
19
19
  enabled: boolean;
20
20
  /** Windows appID to show instead of "SnoreToast" */
21
21
  windowsAppId?: string;
22
+ /**
23
+ * When true, suppresses the notification if the terminal window is the
24
+ * foreground (active) window. Only effective on supported platforms
25
+ * (currently Windows). Default: false.
26
+ */
27
+ suppressWhenFocused?: boolean;
22
28
  }
23
29
 
24
30
  /** Gotify notification platform config */
@@ -101,6 +107,8 @@ export interface NotifyResult {
101
107
  platform: NotifyPlatform;
102
108
  /** Whether the send succeeded */
103
109
  success: boolean;
110
+ /** True when the notification was intentionally suppressed (e.g. window focused) */
111
+ suppressed?: boolean;
104
112
  /** Error message if failed */
105
113
  error?: string;
106
114
  }