@d3lm/pr-stats 0.2.15 → 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 +4 -2
- 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 +63 -12
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -125,13 +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 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, `
|
|
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
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
133
|
|
|
134
|
-
|
|
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.
|
|
135
135
|
|
|
136
136
|
## Theming
|
|
137
137
|
|
|
@@ -154,6 +154,8 @@ The `preset` key names the active theme, one of `default`, `green`, `blue`, `pur
|
|
|
154
154
|
|
|
155
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.
|
|
156
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
|
+
|
|
157
159
|
## License
|
|
158
160
|
|
|
159
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
|
@@ -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
|
}
|
|
@@ -4169,6 +4169,8 @@ 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";
|
|
4172
4174
|
function notificationBoundary(renderer2) {
|
|
4173
4175
|
const raw = renderer2;
|
|
4174
4176
|
return {
|
|
@@ -4238,10 +4240,33 @@ function notificationCaveat(renderer2, channel) {
|
|
|
4238
4240
|
}
|
|
4239
4241
|
}
|
|
4240
4242
|
}
|
|
4241
|
-
|
|
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()) {
|
|
4242
4267
|
switch (process.platform) {
|
|
4243
4268
|
case "darwin": {
|
|
4244
|
-
return "osascript";
|
|
4269
|
+
return helper === null ? "osascript" : HELPER_NAME;
|
|
4245
4270
|
}
|
|
4246
4271
|
case "linux": {
|
|
4247
4272
|
return "notify-send";
|
|
@@ -4254,9 +4279,12 @@ function notificationTool() {
|
|
|
4254
4279
|
function escapeMarkup(text) {
|
|
4255
4280
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
4256
4281
|
}
|
|
4257
|
-
function notifyCommand(title, body) {
|
|
4282
|
+
function notifyCommand(title, body, helper = notificationHelper()) {
|
|
4258
4283
|
switch (process.platform) {
|
|
4259
4284
|
case "darwin": {
|
|
4285
|
+
if (helper !== null) {
|
|
4286
|
+
return { command: helper, args: [title, body], name: HELPER_NAME };
|
|
4287
|
+
}
|
|
4260
4288
|
return {
|
|
4261
4289
|
command: "osascript",
|
|
4262
4290
|
args: [
|
|
@@ -4268,31 +4296,54 @@ function notifyCommand(title, body) {
|
|
|
4268
4296
|
"end run",
|
|
4269
4297
|
title,
|
|
4270
4298
|
body
|
|
4271
|
-
]
|
|
4299
|
+
],
|
|
4300
|
+
name: "osascript"
|
|
4272
4301
|
};
|
|
4273
4302
|
}
|
|
4274
4303
|
case "linux": {
|
|
4275
|
-
return {
|
|
4304
|
+
return {
|
|
4305
|
+
command: "notify-send",
|
|
4306
|
+
args: ["--app-name=pr-stats", "--", title, escapeMarkup(body)],
|
|
4307
|
+
name: "notify-send"
|
|
4308
|
+
};
|
|
4276
4309
|
}
|
|
4277
4310
|
default: {
|
|
4278
4311
|
return null;
|
|
4279
4312
|
}
|
|
4280
4313
|
}
|
|
4281
4314
|
}
|
|
4282
|
-
|
|
4283
|
-
|
|
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);
|
|
4284
4319
|
if (spec === null) {
|
|
4285
4320
|
onError("desktop notifications are not supported on this platform");
|
|
4286
4321
|
return;
|
|
4287
4322
|
}
|
|
4288
|
-
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
|
+
}
|
|
4289
4333
|
child.on("error", (error) => {
|
|
4334
|
+
runningHelpers.delete(run);
|
|
4290
4335
|
onError(`could not send the notification (${error.message})`);
|
|
4291
4336
|
});
|
|
4292
4337
|
child.on("exit", (code) => {
|
|
4293
|
-
|
|
4294
|
-
|
|
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;
|
|
4295
4345
|
}
|
|
4346
|
+
onError(`could not send the notification (${spec.name} exited with ${code})`);
|
|
4296
4347
|
});
|
|
4297
4348
|
child.unref();
|
|
4298
4349
|
}
|
|
@@ -4825,7 +4876,7 @@ function buildSizeRepoOptions(raw) {
|
|
|
4825
4876
|
}
|
|
4826
4877
|
function pendingDetail(counts) {
|
|
4827
4878
|
const awaiting = `${counts.awaiting} ${counts.awaiting === 1 ? "PR" : "PRs"} awaiting your review`;
|
|
4828
|
-
return awaiting + (counts.reviewing > 0 ? `, ${counts.reviewing}
|
|
4879
|
+
return awaiting + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewed` : "");
|
|
4829
4880
|
}
|
|
4830
4881
|
function buildPendingRepoOptions(raw) {
|
|
4831
4882
|
const countsByRepo = /* @__PURE__ */ new Map();
|
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",
|