@d3lm/pr-stats 0.2.14 → 0.2.16
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 +7 -3
- package/dist/pr-stats.app/Contents/Info.plist +34 -0
- package/dist/pr-stats.app/Contents/MacOS/pr-stats-notifier +0 -0
- package/dist/pr-stats.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/pr-stats.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/tui-app.mjs +128 -31
- package/package.json +2 -1
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, `
|
|
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, the bundled `pr-stats.app` helper 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 `
|
|
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
|
-
|
|
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
|
+
On macOS the command path runs a small app bundle that ships inside the package, `dist/pr-stats.app`, so the banner carries the pr-stats name and icon and the app gets its own entry under System Settings › Notifications. The first send asks for permission the way any app does, and the helper waits for your answer, because macOS treats a prompt that goes unanswered until the requesting app quits as a refusal. Should notifications end up turned off, the footer says so, and the switch to turn them back on sits under System Settings › Notifications › pr-stats. The helper runs on macOS 13 and later on both Apple silicon and Intel Macs. A source checkout without the bundle falls back to `osascript`, whose notifications post under Script Editor and stay invisible since macOS 15 until Script Editor holds notification permission, which the Script Editor app grants after running the one-line script `display notification "test"` once.
|
|
133
135
|
|
|
134
136
|
## Theming
|
|
135
137
|
|
|
@@ -152,6 +154,8 @@ The `preset` key names the active theme, one of `default`, `green`, `blue`, `pur
|
|
|
152
154
|
|
|
153
155
|
The TUI runs with `bun tui/main.tsx`, or under Node with `pnpm tui:node`. The `--debug tui/testdata` flag serves canned data from the fake `gh` binary in that directory instead of fetching from GitHub, and `bun test tui` drives the whole pipeline against it. `pnpm build` bundles everything into `dist/`, where `tui.mjs` is the launcher and `tui-app.mjs` is the app bundle it loads.
|
|
154
156
|
|
|
157
|
+
The macOS notification helper lives in `native/macos` as a Swift source, an `Info.plist`, and the icon PNG, and `pnpm build:notifier` compiles it into `dist/pr-stats.app` as a universal, ad-hoc signed bundle. The build needs the Xcode command line tools and only runs on macOS, so the publish workflow builds the bundle on a macOS runner and adds it to the package before publishing. Without the bundle the TUI falls back to `osascript`, so the rest of the development setup does not depend on it. The bundle identifier `dev.d3lm.pr-stats.notifier` keys the notification permission, so it must stay the same across releases.
|
|
158
|
+
|
|
155
159
|
## License
|
|
156
160
|
|
|
157
161
|
OpenTUI is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
+
<string>en</string>
|
|
7
|
+
<key>CFBundleDisplayName</key>
|
|
8
|
+
<string>pr-stats</string>
|
|
9
|
+
<key>CFBundleExecutable</key>
|
|
10
|
+
<string>pr-stats-notifier</string>
|
|
11
|
+
<key>CFBundleIconFile</key>
|
|
12
|
+
<string>AppIcon</string>
|
|
13
|
+
<key>CFBundleIdentifier</key>
|
|
14
|
+
<string>dev.d3lm.pr-stats.notifier</string>
|
|
15
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
16
|
+
<string>6.0</string>
|
|
17
|
+
<key>CFBundleName</key>
|
|
18
|
+
<string>pr-stats</string>
|
|
19
|
+
<key>CFBundlePackageType</key>
|
|
20
|
+
<string>APPL</string>
|
|
21
|
+
<key>CFBundleShortVersionString</key>
|
|
22
|
+
<string>0.2.16</string>
|
|
23
|
+
<key>CFBundleVersion</key>
|
|
24
|
+
<string>0.2.16</string>
|
|
25
|
+
<key>LSMinimumSystemVersion</key>
|
|
26
|
+
<string>13.0</string>
|
|
27
|
+
<key>LSUIElement</key>
|
|
28
|
+
<true/>
|
|
29
|
+
<key>NSHighResolutionCapable</key>
|
|
30
|
+
<true/>
|
|
31
|
+
<key>NSPrincipalClass</key>
|
|
32
|
+
<string>NSApplication</string>
|
|
33
|
+
</dict>
|
|
34
|
+
</plist>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>files</key>
|
|
6
|
+
<dict>
|
|
7
|
+
<key>Resources/AppIcon.icns</key>
|
|
8
|
+
<data>
|
|
9
|
+
zC/K7ZKrclIys50SVpETzoH69zo=
|
|
10
|
+
</data>
|
|
11
|
+
</dict>
|
|
12
|
+
<key>files2</key>
|
|
13
|
+
<dict>
|
|
14
|
+
<key>Resources/AppIcon.icns</key>
|
|
15
|
+
<dict>
|
|
16
|
+
<key>hash2</key>
|
|
17
|
+
<data>
|
|
18
|
+
YNQOX7kwkBCY5/mW8PxQjE1wMJeXJdfF0dhZ3n7hBAo=
|
|
19
|
+
</data>
|
|
20
|
+
</dict>
|
|
21
|
+
</dict>
|
|
22
|
+
<key>rules</key>
|
|
23
|
+
<dict>
|
|
24
|
+
<key>^Resources/</key>
|
|
25
|
+
<true/>
|
|
26
|
+
<key>^Resources/.*\.lproj/</key>
|
|
27
|
+
<dict>
|
|
28
|
+
<key>optional</key>
|
|
29
|
+
<true/>
|
|
30
|
+
<key>weight</key>
|
|
31
|
+
<real>1000</real>
|
|
32
|
+
</dict>
|
|
33
|
+
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
34
|
+
<dict>
|
|
35
|
+
<key>omit</key>
|
|
36
|
+
<true/>
|
|
37
|
+
<key>weight</key>
|
|
38
|
+
<real>1100</real>
|
|
39
|
+
</dict>
|
|
40
|
+
<key>^Resources/Base\.lproj/</key>
|
|
41
|
+
<dict>
|
|
42
|
+
<key>weight</key>
|
|
43
|
+
<real>1010</real>
|
|
44
|
+
</dict>
|
|
45
|
+
<key>^version.plist$</key>
|
|
46
|
+
<true/>
|
|
47
|
+
</dict>
|
|
48
|
+
<key>rules2</key>
|
|
49
|
+
<dict>
|
|
50
|
+
<key>.*\.dSYM($|/)</key>
|
|
51
|
+
<dict>
|
|
52
|
+
<key>weight</key>
|
|
53
|
+
<real>11</real>
|
|
54
|
+
</dict>
|
|
55
|
+
<key>^(.*/)?\.DS_Store$</key>
|
|
56
|
+
<dict>
|
|
57
|
+
<key>omit</key>
|
|
58
|
+
<true/>
|
|
59
|
+
<key>weight</key>
|
|
60
|
+
<real>2000</real>
|
|
61
|
+
</dict>
|
|
62
|
+
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
|
63
|
+
<dict>
|
|
64
|
+
<key>nested</key>
|
|
65
|
+
<true/>
|
|
66
|
+
<key>weight</key>
|
|
67
|
+
<real>10</real>
|
|
68
|
+
</dict>
|
|
69
|
+
<key>^.*</key>
|
|
70
|
+
<true/>
|
|
71
|
+
<key>^Info\.plist$</key>
|
|
72
|
+
<dict>
|
|
73
|
+
<key>omit</key>
|
|
74
|
+
<true/>
|
|
75
|
+
<key>weight</key>
|
|
76
|
+
<real>20</real>
|
|
77
|
+
</dict>
|
|
78
|
+
<key>^PkgInfo$</key>
|
|
79
|
+
<dict>
|
|
80
|
+
<key>omit</key>
|
|
81
|
+
<true/>
|
|
82
|
+
<key>weight</key>
|
|
83
|
+
<real>20</real>
|
|
84
|
+
</dict>
|
|
85
|
+
<key>^Resources/</key>
|
|
86
|
+
<dict>
|
|
87
|
+
<key>weight</key>
|
|
88
|
+
<real>20</real>
|
|
89
|
+
</dict>
|
|
90
|
+
<key>^Resources/.*\.lproj/</key>
|
|
91
|
+
<dict>
|
|
92
|
+
<key>optional</key>
|
|
93
|
+
<true/>
|
|
94
|
+
<key>weight</key>
|
|
95
|
+
<real>1000</real>
|
|
96
|
+
</dict>
|
|
97
|
+
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
98
|
+
<dict>
|
|
99
|
+
<key>omit</key>
|
|
100
|
+
<true/>
|
|
101
|
+
<key>weight</key>
|
|
102
|
+
<real>1100</real>
|
|
103
|
+
</dict>
|
|
104
|
+
<key>^Resources/Base\.lproj/</key>
|
|
105
|
+
<dict>
|
|
106
|
+
<key>weight</key>
|
|
107
|
+
<real>1010</real>
|
|
108
|
+
</dict>
|
|
109
|
+
<key>^[^/]+$</key>
|
|
110
|
+
<dict>
|
|
111
|
+
<key>nested</key>
|
|
112
|
+
<true/>
|
|
113
|
+
<key>weight</key>
|
|
114
|
+
<real>10</real>
|
|
115
|
+
</dict>
|
|
116
|
+
<key>^embedded\.provisionprofile$</key>
|
|
117
|
+
<dict>
|
|
118
|
+
<key>weight</key>
|
|
119
|
+
<real>20</real>
|
|
120
|
+
</dict>
|
|
121
|
+
<key>^version\.plist$</key>
|
|
122
|
+
<dict>
|
|
123
|
+
<key>weight</key>
|
|
124
|
+
<real>20</real>
|
|
125
|
+
</dict>
|
|
126
|
+
</dict>
|
|
127
|
+
</dict>
|
|
128
|
+
</plist>
|
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 "
|
|
351
|
+
throw new CliError(`"notifyChannel" in ${settingsFile()} must be "auto", "terminal", "command", or "bell"`);
|
|
352
352
|
}
|
|
353
353
|
current = settings;
|
|
354
354
|
return current;
|
|
@@ -2070,7 +2070,7 @@ function buildPendingReviewView(raw, repo = null, grouped = false) {
|
|
|
2070
2070
|
empty: null,
|
|
2071
2071
|
sections: [
|
|
2072
2072
|
...awaiting.length === 0 ? [] : [sectionOf(`Awaiting your review (n=${awaiting.length})`, awaiting)],
|
|
2073
|
-
...reviewing.length === 0 ? [] : [sectionOf(`
|
|
2073
|
+
...reviewing.length === 0 ? [] : [sectionOf(`Reviewed (n=${reviewing.length})`, reviewing)]
|
|
2074
2074
|
]
|
|
2075
2075
|
};
|
|
2076
2076
|
}
|
|
@@ -4095,7 +4095,7 @@ var SETTINGS = [
|
|
|
4095
4095
|
key: "notifyChannel",
|
|
4096
4096
|
section: "Notifications",
|
|
4097
4097
|
label: "Notification channel",
|
|
4098
|
-
hint: "auto
|
|
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,28 +4169,104 @@ var CACHE_MESSAGES = {
|
|
|
4169
4169
|
|
|
4170
4170
|
// src/tui/utils/notify.ts
|
|
4171
4171
|
import { spawn } from "node:child_process";
|
|
4172
|
+
import { existsSync } from "node:fs";
|
|
4173
|
+
import { fileURLToPath } from "node:url";
|
|
4174
|
+
function notificationBoundary(renderer2) {
|
|
4175
|
+
const raw = renderer2;
|
|
4176
|
+
return {
|
|
4177
|
+
get capabilities() {
|
|
4178
|
+
return renderer2.capabilities;
|
|
4179
|
+
},
|
|
4180
|
+
triggerNotification: (message, title) => renderer2.triggerNotification(message, title),
|
|
4181
|
+
writeOut: (data) => {
|
|
4182
|
+
if (typeof raw.writeOut === "function") {
|
|
4183
|
+
raw.writeOut(data);
|
|
4184
|
+
} else {
|
|
4185
|
+
process.stdout.write(data);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
var BELL = "\x07";
|
|
4191
|
+
function terminalQuirk(renderer2) {
|
|
4192
|
+
const name = renderer2.capabilities?.terminal.name ?? "";
|
|
4193
|
+
if (/iterm/i.test(name)) {
|
|
4194
|
+
return "iterm";
|
|
4195
|
+
}
|
|
4196
|
+
if (/apple_terminal|terminal\.app/i.test(name)) {
|
|
4197
|
+
return "apple-terminal";
|
|
4198
|
+
}
|
|
4199
|
+
return null;
|
|
4200
|
+
}
|
|
4172
4201
|
function createNotifier(renderer2, channel) {
|
|
4173
4202
|
return (title, body, onError) => {
|
|
4174
|
-
if (channel
|
|
4203
|
+
if (channel === "bell") {
|
|
4204
|
+
renderer2.writeOut(BELL);
|
|
4205
|
+
return;
|
|
4206
|
+
}
|
|
4207
|
+
const tryTerminal = channel === "terminal" || channel === "auto" && terminalQuirk(renderer2) !== "apple-terminal";
|
|
4208
|
+
if (tryTerminal && renderer2.triggerNotification(body, title)) {
|
|
4175
4209
|
return;
|
|
4176
4210
|
}
|
|
4177
4211
|
if (channel === "terminal") {
|
|
4178
|
-
onError("the terminal has no notification support, switch the channel to auto
|
|
4212
|
+
onError("the terminal has no notification support, switch the channel to auto, the platform command, or bell");
|
|
4179
4213
|
return;
|
|
4180
4214
|
}
|
|
4181
4215
|
sendNotification(title, body, onError);
|
|
4182
4216
|
};
|
|
4183
4217
|
}
|
|
4184
4218
|
function notificationChannel(renderer2, channel) {
|
|
4185
|
-
if (channel === "
|
|
4219
|
+
if (channel === "bell" || channel === "terminal") {
|
|
4220
|
+
return channel;
|
|
4221
|
+
}
|
|
4222
|
+
if (channel === "auto" && renderer2.capabilities?.notifications === true && terminalQuirk(renderer2) !== "apple-terminal") {
|
|
4186
4223
|
return "terminal";
|
|
4187
4224
|
}
|
|
4188
4225
|
return notificationTool();
|
|
4189
4226
|
}
|
|
4190
|
-
function
|
|
4227
|
+
function notificationCaveat(renderer2, channel) {
|
|
4228
|
+
if (channel === "command" || channel === "bell") {
|
|
4229
|
+
return null;
|
|
4230
|
+
}
|
|
4231
|
+
switch (terminalQuirk(renderer2)) {
|
|
4232
|
+
case "iterm": {
|
|
4233
|
+
return "iTerm2 shows these only once Notification Center Alerts is on under Settings \u203A Profiles \u203A Terminal";
|
|
4234
|
+
}
|
|
4235
|
+
case "apple-terminal": {
|
|
4236
|
+
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";
|
|
4237
|
+
}
|
|
4238
|
+
default: {
|
|
4239
|
+
return null;
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
var HELPER_BINARY = "pr-stats.app/Contents/MacOS/pr-stats-notifier";
|
|
4244
|
+
var HELPER_NAME = "pr-stats.app";
|
|
4245
|
+
var HELPER_DENIED_EXIT = 3;
|
|
4246
|
+
var PROMPT_GRACE_MS = 5e3;
|
|
4247
|
+
var helperLookup;
|
|
4248
|
+
function notificationHelper() {
|
|
4249
|
+
if (helperLookup === void 0) {
|
|
4250
|
+
helperLookup = locateHelper();
|
|
4251
|
+
}
|
|
4252
|
+
return helperLookup;
|
|
4253
|
+
}
|
|
4254
|
+
function locateHelper() {
|
|
4255
|
+
if (process.platform !== "darwin") {
|
|
4256
|
+
return null;
|
|
4257
|
+
}
|
|
4258
|
+
for (const base of ["./", "../../../dist/"]) {
|
|
4259
|
+
const path = fileURLToPath(new URL(base + HELPER_BINARY, import.meta.url));
|
|
4260
|
+
if (existsSync(path)) {
|
|
4261
|
+
return path;
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
return null;
|
|
4265
|
+
}
|
|
4266
|
+
function notificationTool(helper = notificationHelper()) {
|
|
4191
4267
|
switch (process.platform) {
|
|
4192
4268
|
case "darwin": {
|
|
4193
|
-
return "osascript";
|
|
4269
|
+
return helper === null ? "osascript" : HELPER_NAME;
|
|
4194
4270
|
}
|
|
4195
4271
|
case "linux": {
|
|
4196
4272
|
return "notify-send";
|
|
@@ -4203,9 +4279,12 @@ function notificationTool() {
|
|
|
4203
4279
|
function escapeMarkup(text) {
|
|
4204
4280
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
4205
4281
|
}
|
|
4206
|
-
function notifyCommand(title, body) {
|
|
4282
|
+
function notifyCommand(title, body, helper = notificationHelper()) {
|
|
4207
4283
|
switch (process.platform) {
|
|
4208
4284
|
case "darwin": {
|
|
4285
|
+
if (helper !== null) {
|
|
4286
|
+
return { command: helper, args: [title, body], name: HELPER_NAME };
|
|
4287
|
+
}
|
|
4209
4288
|
return {
|
|
4210
4289
|
command: "osascript",
|
|
4211
4290
|
args: [
|
|
@@ -4217,31 +4296,54 @@ function notifyCommand(title, body) {
|
|
|
4217
4296
|
"end run",
|
|
4218
4297
|
title,
|
|
4219
4298
|
body
|
|
4220
|
-
]
|
|
4299
|
+
],
|
|
4300
|
+
name: "osascript"
|
|
4221
4301
|
};
|
|
4222
4302
|
}
|
|
4223
4303
|
case "linux": {
|
|
4224
|
-
return {
|
|
4304
|
+
return {
|
|
4305
|
+
command: "notify-send",
|
|
4306
|
+
args: ["--app-name=pr-stats", "--", title, escapeMarkup(body)],
|
|
4307
|
+
name: "notify-send"
|
|
4308
|
+
};
|
|
4225
4309
|
}
|
|
4226
4310
|
default: {
|
|
4227
4311
|
return null;
|
|
4228
4312
|
}
|
|
4229
4313
|
}
|
|
4230
4314
|
}
|
|
4231
|
-
|
|
4232
|
-
|
|
4315
|
+
var spawnDetached = (command, args) => spawn(command, args, { stdio: "ignore", detached: true });
|
|
4316
|
+
var runningHelpers = /* @__PURE__ */ new Set();
|
|
4317
|
+
function sendNotification(title, body, onError, spawnProcess = spawnDetached, helper = notificationHelper()) {
|
|
4318
|
+
const spec = notifyCommand(title, body, helper);
|
|
4233
4319
|
if (spec === null) {
|
|
4234
4320
|
onError("desktop notifications are not supported on this platform");
|
|
4235
4321
|
return;
|
|
4236
4322
|
}
|
|
4237
|
-
const
|
|
4323
|
+
const isHelper = spec.name === HELPER_NAME;
|
|
4324
|
+
const run = { started: Date.now() };
|
|
4325
|
+
if (isHelper && [...runningHelpers].some(({ started }) => run.started - started > PROMPT_GRACE_MS)) {
|
|
4326
|
+
onError("the notification permission prompt for pr-stats is still waiting for an answer");
|
|
4327
|
+
return;
|
|
4328
|
+
}
|
|
4329
|
+
const child = spawnProcess(spec.command, spec.args);
|
|
4330
|
+
if (isHelper) {
|
|
4331
|
+
runningHelpers.add(run);
|
|
4332
|
+
}
|
|
4238
4333
|
child.on("error", (error) => {
|
|
4334
|
+
runningHelpers.delete(run);
|
|
4239
4335
|
onError(`could not send the notification (${error.message})`);
|
|
4240
4336
|
});
|
|
4241
4337
|
child.on("exit", (code) => {
|
|
4242
|
-
|
|
4243
|
-
|
|
4338
|
+
runningHelpers.delete(run);
|
|
4339
|
+
if (code === null || code === 0) {
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
if (isHelper && code === HELPER_DENIED_EXIT) {
|
|
4343
|
+
onError("notifications for pr-stats are turned off, allow them under System Settings \u203A Notifications \u203A pr-stats");
|
|
4344
|
+
return;
|
|
4244
4345
|
}
|
|
4346
|
+
onError(`could not send the notification (${spec.name} exited with ${code})`);
|
|
4245
4347
|
});
|
|
4246
4348
|
child.unref();
|
|
4247
4349
|
}
|
|
@@ -4273,9 +4375,11 @@ function SettingsModal({
|
|
|
4273
4375
|
onSubmit
|
|
4274
4376
|
}) {
|
|
4275
4377
|
const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
|
|
4276
|
-
const renderer2 = useRenderer2();
|
|
4378
|
+
const renderer2 = notificationBoundary(useRenderer2());
|
|
4277
4379
|
const channelValue = notifyChannel2 === "command" ? notificationTool() ?? "unsupported" : notifyChannel2;
|
|
4278
4380
|
const deliveryValue = notificationChannel(renderer2, notifyChannel2) ?? "unsupported";
|
|
4381
|
+
const caveat = SETTINGS[selected].key === "testNotification" ? notificationCaveat(renderer2, notifyChannel2) : null;
|
|
4382
|
+
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
4383
|
return /* @__PURE__ */ jsxs11(ModalFrame, { title: "Settings", children: [
|
|
4280
4384
|
SECTIONS2.map((section) => /* @__PURE__ */ jsxs11("box", { flexDirection: "column", marginBottom: 1, children: [
|
|
4281
4385
|
/* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: theme.accent, marginLeft: 2, children: section.title }),
|
|
@@ -4302,17 +4406,7 @@ function SettingsModal({
|
|
|
4302
4406
|
) }, setting.key);
|
|
4303
4407
|
})
|
|
4304
4408
|
] }, 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
|
-
)
|
|
4409
|
+
/* @__PURE__ */ jsx12("text", { wrapMode: "word", height: 2, fg: bottomLine.fg, marginLeft: 2, marginRight: 2, children: bottomLine.text })
|
|
4316
4410
|
] });
|
|
4317
4411
|
}
|
|
4318
4412
|
function SettingValue({
|
|
@@ -4782,7 +4876,7 @@ function buildSizeRepoOptions(raw) {
|
|
|
4782
4876
|
}
|
|
4783
4877
|
function pendingDetail(counts) {
|
|
4784
4878
|
const awaiting = `${counts.awaiting} ${counts.awaiting === 1 ? "PR" : "PRs"} awaiting your review`;
|
|
4785
|
-
return awaiting + (counts.reviewing > 0 ? `, ${counts.reviewing}
|
|
4879
|
+
return awaiting + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewed` : "");
|
|
4786
4880
|
}
|
|
4787
4881
|
function buildPendingRepoOptions(raw) {
|
|
4788
4882
|
const countsByRepo = /* @__PURE__ */ new Map();
|
|
@@ -6905,7 +6999,10 @@ function App({
|
|
|
6905
6999
|
const [notifyChannel2, setNotifyChannel] = useState4(initialNotifyChannel);
|
|
6906
7000
|
const [copyLinks2, setCopyLinks] = useState4(initialCopyLinks);
|
|
6907
7001
|
const [themeState, setThemeState] = useState4(initialTheme);
|
|
6908
|
-
const defaultNotify = useMemo2(
|
|
7002
|
+
const defaultNotify = useMemo2(
|
|
7003
|
+
() => createNotifier(notificationBoundary(renderer2), notifyChannel2),
|
|
7004
|
+
[renderer2, notifyChannel2]
|
|
7005
|
+
);
|
|
6909
7006
|
const notifier = notify ?? defaultNotify;
|
|
6910
7007
|
const copyRow = (row) => {
|
|
6911
7008
|
copyLink(row.url, (message) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3lm/pr-stats",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
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",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"build": "pnpm build:app && pnpm build:bin",
|
|
34
34
|
"build:app": "esbuild src/tui/main.tsx --bundle --platform=node --format=esm --target=esnext --jsx=automatic --jsx-import-source=@opentui/react --external:@opentui/core --external:@opentui/react --external:react --outfile=dist/tui-app.mjs",
|
|
35
35
|
"build:bin": "esbuild src/tui/launch.ts --platform=node --format=esm --target=node20 --outfile=dist/tui.mjs",
|
|
36
|
+
"build:notifier": "bash scripts/build-notifier.sh",
|
|
36
37
|
"tui": "bun src/tui/main.tsx",
|
|
37
38
|
"tui:node": "pnpm build && node dist/tui.mjs",
|
|
38
39
|
"check": "tsc --noEmit",
|