@d3lm/pr-stats 0.2.14 → 0.2.15

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/README.md CHANGED
@@ -125,11 +125,13 @@ The Desktop notifications row in the settings dialog makes the TUI send a deskto
125
125
  }
126
126
  ```
127
127
 
128
- Notifications go through the terminal itself when it supports a notification escape sequence, which covers iTerm2, Kitty, Ghostty, WezTerm, and most VTE-based terminals like GNOME Terminal. The terminal posts the notification under its own notification permission and the sequence travels through SSH, so this path needs no setup. Some terminals only show the banner while their window is unfocused. Terminals without such support get the platform's own command instead, `osascript` on macOS and `notify-send` on Linux. A notification that fails to send reports in the footer, for example when `notify-send` is missing on a Linux machine.
128
+ Notifications go through the terminal itself when it supports a notification escape sequence, which covers iTerm2, Kitty, Ghostty, WezTerm, and most VTE-based terminals like GNOME Terminal. The terminal posts the notification under its own notification permission and the sequence travels through SSH, so this path needs no setup beyond what the terminal asks for. Some terminals only show the banner while their window is unfocused. Terminals without such support get the platform's own command instead, `osascript` on macOS and `notify-send` on Linux. A notification that fails to send reports in the footer, for example when `notify-send` is missing on a Linux machine.
129
129
 
130
- The "Notification channel" row picks the path, and the choice persists as `notifyChannel` with the values `auto`, `terminal`, and `command`. On `auto` the TUI tries the terminal first and falls back to the platform command, while the other two force one path. The dialog shows the `command` value as the command's name on this platform. Forcing the command pays off inside editor terminals like the one in VS Code, which render the terminal path as a small in-editor toast instead of a system notification. The "Send test notification" row below it names the channel the next send takes and sends a sample notification, so you can check that your desktop displays it before relying on it.
130
+ The "Notification channel" row picks the path, and the choice persists as `notifyChannel` with the values `auto`, `terminal`, `command`, and `bell`. On `auto` the TUI tries the terminal first and falls back to the platform command, `terminal` and `command` force one of those paths, and `bell` rings the terminal bell instead of posting any text. The dialog shows the `command` value as the command's name on this platform. Forcing the command pays off inside editor terminals like the one in VS Code, which render the terminal path as a small in-editor toast instead of a system notification. The bell reaches every terminal, including the ones that swallow notifications, and each terminal turns it into its own signal, a sound, a flashing screen, a bell icon on the tab, or a badge on the dock icon. The "Send test notification" row below the channel names the channel the next send takes and sends a sample notification, so you can check that your desktop displays it before relying on it.
131
131
 
132
- The macOS fallback posts through the built-in Script Editor, and since macOS 15 those notifications stay invisible until Script Editor holds notification permission, without any prompt appearing. To grant it once, open the Script Editor app, run the one-line script `display notification "test"`, and allow the prompt that appears. Script Editor then shows up under Notifications in the System Settings like any other app.
132
+ Two terminals accept the notification sequence without showing anything, and the test row warns about both. iTerm2 drops it until the profile has "Notification Center Alerts" turned on under Settings → Profiles → Terminal, which is off by default. Behind the "Filter Alerts…" button next to it, "Send escape sequence-generated alerts" has to stay on as well, and macOS asks for iTerm2's notification permission on the first send. iTerm2 prefixes the banner with the session name unless its advanced setting "Omit session identifier from notifications" is on. Apple Terminal never implemented the sequence at all, so `auto` skips the terminal there and goes straight to the platform command.
133
+
134
+ The macOS command posts through the built-in Script Editor, and since macOS 15 those notifications stay invisible until Script Editor holds notification permission, without any prompt appearing. To grant it once, open the Script Editor app, run the one-line script `display notification "test"`, and allow the prompt that appears. Script Editor then shows up under Notifications in the System Settings like any other app.
133
135
 
134
136
  ## Theming
135
137
 
package/dist/tui-app.mjs CHANGED
@@ -279,7 +279,7 @@ function percentile(sorted, percent) {
279
279
  }
280
280
 
281
281
  // src/settings.ts
282
- var NOTIFY_CHANNELS = ["auto", "terminal", "command"];
282
+ var NOTIFY_CHANNELS = ["auto", "terminal", "command", "bell"];
283
283
  var DEFAULT_RELOAD_INTERVAL = "10m";
284
284
  var MAX_RELOAD_INTERVAL_MS = 24 * 60 * 60 * 1e3;
285
285
  function reloadIntervalMs(input) {
@@ -348,7 +348,7 @@ function loadSettings() {
348
348
  throw new CliError(`"notifications" in ${settingsFile()} must be true or false`);
349
349
  }
350
350
  if (settings.notifyChannel !== void 0 && !NOTIFY_CHANNELS.includes(settings.notifyChannel)) {
351
- throw new CliError(`"notifyChannel" in ${settingsFile()} must be "auto", "terminal", or "command"`);
351
+ throw new CliError(`"notifyChannel" in ${settingsFile()} must be "auto", "terminal", "command", or "bell"`);
352
352
  }
353
353
  current = settings;
354
354
  return current;
@@ -4095,7 +4095,7 @@ var SETTINGS = [
4095
4095
  key: "notifyChannel",
4096
4096
  section: "Notifications",
4097
4097
  label: "Notification channel",
4098
- hint: "auto posts through the terminal and falls back to the platform command \xB7 the other values force one channel"
4098
+ hint: "auto tries the terminal, then the platform command \xB7 the others force one path \xB7 bell rings the terminal bell"
4099
4099
  },
4100
4100
  {
4101
4101
  key: "testNotification",
@@ -4169,24 +4169,75 @@ var CACHE_MESSAGES = {
4169
4169
 
4170
4170
  // src/tui/utils/notify.ts
4171
4171
  import { spawn } from "node:child_process";
4172
+ function notificationBoundary(renderer2) {
4173
+ const raw = renderer2;
4174
+ return {
4175
+ get capabilities() {
4176
+ return renderer2.capabilities;
4177
+ },
4178
+ triggerNotification: (message, title) => renderer2.triggerNotification(message, title),
4179
+ writeOut: (data) => {
4180
+ if (typeof raw.writeOut === "function") {
4181
+ raw.writeOut(data);
4182
+ } else {
4183
+ process.stdout.write(data);
4184
+ }
4185
+ }
4186
+ };
4187
+ }
4188
+ var BELL = "\x07";
4189
+ function terminalQuirk(renderer2) {
4190
+ const name = renderer2.capabilities?.terminal.name ?? "";
4191
+ if (/iterm/i.test(name)) {
4192
+ return "iterm";
4193
+ }
4194
+ if (/apple_terminal|terminal\.app/i.test(name)) {
4195
+ return "apple-terminal";
4196
+ }
4197
+ return null;
4198
+ }
4172
4199
  function createNotifier(renderer2, channel) {
4173
4200
  return (title, body, onError) => {
4174
- if (channel !== "command" && renderer2.triggerNotification(body, title)) {
4201
+ if (channel === "bell") {
4202
+ renderer2.writeOut(BELL);
4203
+ return;
4204
+ }
4205
+ const tryTerminal = channel === "terminal" || channel === "auto" && terminalQuirk(renderer2) !== "apple-terminal";
4206
+ if (tryTerminal && renderer2.triggerNotification(body, title)) {
4175
4207
  return;
4176
4208
  }
4177
4209
  if (channel === "terminal") {
4178
- onError("the terminal has no notification support, switch the channel to auto or the platform command");
4210
+ onError("the terminal has no notification support, switch the channel to auto, the platform command, or bell");
4179
4211
  return;
4180
4212
  }
4181
4213
  sendNotification(title, body, onError);
4182
4214
  };
4183
4215
  }
4184
4216
  function notificationChannel(renderer2, channel) {
4185
- if (channel === "terminal" || channel === "auto" && renderer2.capabilities?.notifications === true) {
4217
+ if (channel === "bell" || channel === "terminal") {
4218
+ return channel;
4219
+ }
4220
+ if (channel === "auto" && renderer2.capabilities?.notifications === true && terminalQuirk(renderer2) !== "apple-terminal") {
4186
4221
  return "terminal";
4187
4222
  }
4188
4223
  return notificationTool();
4189
4224
  }
4225
+ function notificationCaveat(renderer2, channel) {
4226
+ if (channel === "command" || channel === "bell") {
4227
+ return null;
4228
+ }
4229
+ switch (terminalQuirk(renderer2)) {
4230
+ case "iterm": {
4231
+ return "iTerm2 shows these only once Notification Center Alerts is on under Settings \u203A Profiles \u203A Terminal";
4232
+ }
4233
+ case "apple-terminal": {
4234
+ return channel === "auto" ? "Terminal.app ignores terminal notifications, so auto takes the platform command here" : "Terminal.app ignores terminal notifications \xB7 switch the channel to auto, command, or bell";
4235
+ }
4236
+ default: {
4237
+ return null;
4238
+ }
4239
+ }
4240
+ }
4190
4241
  function notificationTool() {
4191
4242
  switch (process.platform) {
4192
4243
  case "darwin": {
@@ -4273,9 +4324,11 @@ function SettingsModal({
4273
4324
  onSubmit
4274
4325
  }) {
4275
4326
  const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
4276
- const renderer2 = useRenderer2();
4327
+ const renderer2 = notificationBoundary(useRenderer2());
4277
4328
  const channelValue = notifyChannel2 === "command" ? notificationTool() ?? "unsupported" : notifyChannel2;
4278
4329
  const deliveryValue = notificationChannel(renderer2, notifyChannel2) ?? "unsupported";
4330
+ const caveat = SETTINGS[selected].key === "testNotification" ? notificationCaveat(renderer2, notifyChannel2) : null;
4331
+ const bottomLine = error !== null ? { text: error, fg: theme.error } : message !== null ? { text: message.text, fg: message.warn ? theme.warn : theme.muted } : caveat !== null ? { text: caveat, fg: theme.warn } : { text: SETTINGS[selected].hint, fg: theme.muted };
4279
4332
  return /* @__PURE__ */ jsxs11(ModalFrame, { title: "Settings", children: [
4280
4333
  SECTIONS2.map((section) => /* @__PURE__ */ jsxs11("box", { flexDirection: "column", marginBottom: 1, children: [
4281
4334
  /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: theme.accent, marginLeft: 2, children: section.title }),
@@ -4302,17 +4355,7 @@ function SettingsModal({
4302
4355
  ) }, setting.key);
4303
4356
  })
4304
4357
  ] }, section.title)),
4305
- /* @__PURE__ */ jsx12(
4306
- "text",
4307
- {
4308
- wrapMode: "word",
4309
- height: 2,
4310
- fg: error !== null ? theme.error : message?.warn ? theme.warn : theme.muted,
4311
- marginLeft: 2,
4312
- marginRight: 2,
4313
- children: error ?? message?.text ?? SETTINGS[selected].hint
4314
- }
4315
- )
4358
+ /* @__PURE__ */ jsx12("text", { wrapMode: "word", height: 2, fg: bottomLine.fg, marginLeft: 2, marginRight: 2, children: bottomLine.text })
4316
4359
  ] });
4317
4360
  }
4318
4361
  function SettingValue({
@@ -6905,7 +6948,10 @@ function App({
6905
6948
  const [notifyChannel2, setNotifyChannel] = useState4(initialNotifyChannel);
6906
6949
  const [copyLinks2, setCopyLinks] = useState4(initialCopyLinks);
6907
6950
  const [themeState, setThemeState] = useState4(initialTheme);
6908
- const defaultNotify = useMemo2(() => createNotifier(renderer2, notifyChannel2), [renderer2, notifyChannel2]);
6951
+ const defaultNotify = useMemo2(
6952
+ () => createNotifier(notificationBoundary(renderer2), notifyChannel2),
6953
+ [renderer2, notifyChannel2]
6954
+ );
6909
6955
  const notifier = notify ?? defaultNotify;
6910
6956
  const copyRow = (row) => {
6911
6957
  copyLink(row.url, (message) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3lm/pr-stats",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "GitHub PR stats in an interactive terminal UI, via the gh CLI or an access token",
5
5
  "type": "module",
6
6
  "author": "Dominic Elm",