swiftbar-github 1.0.1 → 1.1.1
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 +3 -1
- package/dist/swiftbar-github.js +100 -34
- package/package.json +1 -2
- package/swiftbar-github.3m.sh +5 -0
package/README.md
CHANGED
|
@@ -16,4 +16,6 @@ Download [swiftbar-github.3m.sh](https://cdn.jsdelivr.net/npm/swiftbar-github@la
|
|
|
16
16
|
|
|
17
17
|
Run `chmod +x swiftbar-github.3m.sh`. Updates resolve through `@latest` on each refresh; downloaded versions are cached. For authentication without `gh`, set `GH_TOKEN` or `GITHUB_TOKEN` in SwiftBar's environment.
|
|
18
18
|
|
|
19
|
-
Allow notifications for
|
|
19
|
+
Allow notifications for terminal-notifier in macOS System Settings. Click entries or desktop notifications to open them on GitHub; desktop notification clicks are available for five minutes.
|
|
20
|
+
|
|
21
|
+
Use **Mark as read** to clear unread threads by type or reason. This does not complete pending review requests. Option-click the menu to access SwiftBar's hidden controls.
|
package/dist/swiftbar-github.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { basename, join } from "node:path";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
5
6
|
import { promisify } from "node:util";
|
|
6
7
|
import { Octokit } from "@octokit/rest";
|
|
7
|
-
import nodeNotifier from "node-notifier";
|
|
8
8
|
const exec = promisify(execFile);
|
|
9
|
-
const notifier =
|
|
9
|
+
const notifier = join(dirname(createRequire(import.meta.url).resolve("node-notifier")), "vendor/mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier");
|
|
10
10
|
const inbox = "https://github.com/notifications";
|
|
11
11
|
const launcher = process.env.SWIFTBAR_PLUGIN_PATH;
|
|
12
12
|
const cache = process.env.SWIFTBAR_PLUGIN_CACHE_PATH ||
|
|
@@ -71,25 +71,38 @@ async function save(file, values) {
|
|
|
71
71
|
}
|
|
72
72
|
function notify(item) {
|
|
73
73
|
return new Promise((resolve) => {
|
|
74
|
-
const
|
|
75
|
-
title
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
const child = spawn(notifier, [
|
|
75
|
+
"-title",
|
|
76
|
+
item.alertTitle,
|
|
77
|
+
"-subtitle",
|
|
78
|
+
item.repository,
|
|
79
|
+
"-message",
|
|
80
|
+
item.title,
|
|
81
|
+
"-open",
|
|
82
|
+
item.url,
|
|
83
|
+
"-group",
|
|
84
|
+
`github-${item.url}`,
|
|
85
|
+
"-sound",
|
|
86
|
+
"default",
|
|
87
|
+
"-timeout",
|
|
88
|
+
"300",
|
|
89
|
+
], { detached: true, stdio: "ignore" });
|
|
90
|
+
child.once("error", () => resolve(false));
|
|
91
|
+
child.once("spawn", () => {
|
|
92
|
+
// Record the handoff, not the eventual click or timeout.
|
|
93
|
+
child.unref();
|
|
94
|
+
resolve(true);
|
|
95
|
+
});
|
|
84
96
|
});
|
|
85
97
|
}
|
|
86
|
-
function action(label,
|
|
98
|
+
function action(label, args, refresh = false) {
|
|
87
99
|
if (!launcher)
|
|
88
100
|
return `${label} | href=${inbox}`;
|
|
89
|
-
|
|
101
|
+
const params = args.map((arg, index) => `param${index + 1}=${arg}`).join(" ");
|
|
102
|
+
return `${label} | bash="${launcher.replaceAll('"', '\\"')}" ${params} terminal=false${refresh ? " refresh=true" : ""}`;
|
|
90
103
|
}
|
|
91
|
-
async function
|
|
92
|
-
|
|
104
|
+
async function client() {
|
|
105
|
+
return new Octokit({
|
|
93
106
|
auth: await token(),
|
|
94
107
|
userAgent: "swiftbar-github",
|
|
95
108
|
request: {
|
|
@@ -103,6 +116,9 @@ async function fetchActivity(argument) {
|
|
|
103
116
|
},
|
|
104
117
|
log: { debug() { }, info() { }, warn() { }, error() { } },
|
|
105
118
|
});
|
|
119
|
+
}
|
|
120
|
+
async function fetchActivity(argument) {
|
|
121
|
+
const github = await client();
|
|
106
122
|
const { data: user } = await github.rest.users.getAuthenticated();
|
|
107
123
|
const requested = await github.paginate(github.rest.search.issuesAndPullRequests, {
|
|
108
124
|
q: `is:pr is:open archived:false review-requested:${user.login}`,
|
|
@@ -122,12 +138,11 @@ async function fetchActivity(argument) {
|
|
|
122
138
|
repository,
|
|
123
139
|
title,
|
|
124
140
|
key: item.html_url,
|
|
125
|
-
group: `github-review-${item.html_url}`,
|
|
126
141
|
alertTitle: "Review requested",
|
|
127
142
|
};
|
|
128
143
|
});
|
|
129
144
|
if (argument === "open-reviews")
|
|
130
|
-
return { reviews, notifications: [] };
|
|
145
|
+
return { reviews, notifications: [], unread: [] };
|
|
131
146
|
const reviewURLs = new Set(reviews.map((item) => item.url));
|
|
132
147
|
const unread = await github.paginate(github.rest.activity.listNotificationsForAuthenticatedUser, {
|
|
133
148
|
all: false,
|
|
@@ -146,26 +161,25 @@ async function fetchActivity(argument) {
|
|
|
146
161
|
repository,
|
|
147
162
|
title,
|
|
148
163
|
url: item.url,
|
|
149
|
-
group: `github-notification-${item.id}`,
|
|
150
164
|
alertTitle: `GitHub: ${reason}`,
|
|
151
165
|
reason,
|
|
152
166
|
};
|
|
153
167
|
});
|
|
154
|
-
return { reviews, notifications };
|
|
168
|
+
return { reviews, notifications, unread };
|
|
155
169
|
}
|
|
156
|
-
function renderMenu(reviews, notifications) {
|
|
170
|
+
function renderMenu(reviews, notifications, unread) {
|
|
157
171
|
const total = reviews.length + notifications.length;
|
|
158
172
|
if (!total)
|
|
159
173
|
return "";
|
|
160
174
|
const menu = [
|
|
161
|
-
`${total} | templateImage=${icon}`,
|
|
175
|
+
`${process.env.SWIFTBAR_GITHUB_DEV ? "Dev " : ""}${total} | templateImage=${icon}`,
|
|
162
176
|
"---",
|
|
163
177
|
`Review requests (${reviews.length}) | color=gray`,
|
|
164
178
|
];
|
|
165
179
|
if (reviews.length) {
|
|
166
180
|
for (const item of reviews)
|
|
167
181
|
menu.push(`${item.repository} ${item.title} | href=${item.url}`);
|
|
168
|
-
menu.push(action("Open all review requests", "open-reviews"));
|
|
182
|
+
menu.push(action("Open all review requests", ["open-reviews"]));
|
|
169
183
|
}
|
|
170
184
|
else
|
|
171
185
|
menu.push("No reviews requested | color=gray");
|
|
@@ -173,37 +187,89 @@ function renderMenu(reviews, notifications) {
|
|
|
173
187
|
if (notifications.length) {
|
|
174
188
|
for (const item of notifications)
|
|
175
189
|
menu.push(`${item.repository} [${item.reason}]: ${item.title} | href=${item.url}`);
|
|
176
|
-
menu.push(action("Open all notifications", "open-notifications"));
|
|
190
|
+
menu.push(action("Open all notifications", ["open-notifications"]));
|
|
177
191
|
}
|
|
178
192
|
else
|
|
179
193
|
menu.push("No unread notifications | color=gray");
|
|
194
|
+
if (unread.length) {
|
|
195
|
+
const types = {
|
|
196
|
+
PullRequest: "Pull requests",
|
|
197
|
+
Issue: "Issues",
|
|
198
|
+
Discussion: "Discussions",
|
|
199
|
+
Commit: "Commits",
|
|
200
|
+
Release: "Releases",
|
|
201
|
+
};
|
|
202
|
+
menu.push("---", "Mark as read");
|
|
203
|
+
menu.push(`--${action(`All notifications (${unread.length})`, ["mark-read", unread.map((item) => item.id).join(",")], true)}`);
|
|
204
|
+
for (const [heading, labelFor] of [
|
|
205
|
+
[
|
|
206
|
+
"By type",
|
|
207
|
+
(item) => text(types[item.subject.type] || item.subject.type),
|
|
208
|
+
],
|
|
209
|
+
[
|
|
210
|
+
"By reason",
|
|
211
|
+
(item) => text(item.reason).replaceAll("_", " "),
|
|
212
|
+
],
|
|
213
|
+
]) {
|
|
214
|
+
const groups = new Map();
|
|
215
|
+
for (const item of unread) {
|
|
216
|
+
const label = labelFor(item);
|
|
217
|
+
const ids = groups.get(label) || [];
|
|
218
|
+
ids.push(item.id);
|
|
219
|
+
groups.set(label, ids);
|
|
220
|
+
}
|
|
221
|
+
menu.push("-----", `--${heading} | color=gray`);
|
|
222
|
+
for (const [label, ids] of [...groups].sort(([a], [b]) => a.localeCompare(b)))
|
|
223
|
+
menu.push(`--${action(`${label} (${ids.length})`, ["mark-read", ids.join(",")], true)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
180
226
|
return menu.join("\n");
|
|
181
227
|
}
|
|
182
|
-
async function deliverAlerts(file, items) {
|
|
228
|
+
async function deliverAlerts(file, items, represented = []) {
|
|
183
229
|
await mkdir(cache, { recursive: true });
|
|
184
230
|
const path = join(cache, file);
|
|
185
231
|
const seen = await state(path);
|
|
186
|
-
const current =
|
|
232
|
+
const current = new Set(represented);
|
|
187
233
|
for (const item of items) {
|
|
234
|
+
if (current.has(item.key))
|
|
235
|
+
continue;
|
|
188
236
|
if (!seen || seen.has(item.key) || (await notify(item)))
|
|
189
|
-
current.
|
|
237
|
+
current.add(item.key);
|
|
190
238
|
}
|
|
191
|
-
await save(path, current);
|
|
239
|
+
await save(path, [...current]);
|
|
192
240
|
}
|
|
193
241
|
async function main() {
|
|
194
242
|
const argument = process.argv[2];
|
|
195
|
-
if (argument &&
|
|
243
|
+
if (argument &&
|
|
244
|
+
!["open-reviews", "open-notifications", "mark-read"].includes(argument))
|
|
196
245
|
throw new Error("Usage: swiftbar-github");
|
|
197
|
-
|
|
246
|
+
if (argument === "mark-read") {
|
|
247
|
+
const ids = (process.argv[3] || "").split(",").map(Number);
|
|
248
|
+
if (ids.some((id) => !Number.isSafeInteger(id) || id <= 0))
|
|
249
|
+
throw new Error("Usage: swiftbar-github mark-read <thread-ids>");
|
|
250
|
+
const github = await client();
|
|
251
|
+
for (const id of ids)
|
|
252
|
+
await github.rest.activity.markThreadAsRead({ thread_id: id });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const { reviews, notifications, unread } = await fetchActivity(argument);
|
|
198
256
|
if (argument) {
|
|
199
257
|
const items = argument === "open-reviews" ? reviews : notifications;
|
|
200
258
|
for (const item of items)
|
|
201
259
|
await exec("/usr/bin/open", ["-g", item.url]);
|
|
202
260
|
return;
|
|
203
261
|
}
|
|
204
|
-
const menu = renderMenu(reviews, notifications);
|
|
262
|
+
const menu = renderMenu(reviews, notifications, unread);
|
|
263
|
+
const previousReviews = await state(join(cache, "requested-prs"));
|
|
264
|
+
const reviewURLs = new Set([
|
|
265
|
+
...reviews.map((item) => item.url),
|
|
266
|
+
...(previousReviews || []),
|
|
267
|
+
]);
|
|
268
|
+
const represented = unread
|
|
269
|
+
.filter((item) => item.reason === "review_requested" && reviewURLs.has(webURL(item)))
|
|
270
|
+
.map((item) => `${item.id}:${item.updated_at}`);
|
|
205
271
|
await deliverAlerts("requested-prs", reviews);
|
|
206
|
-
await deliverAlerts("inbox-notifications", notifications);
|
|
272
|
+
await deliverAlerts("inbox-notifications", notifications, represented);
|
|
207
273
|
if (menu)
|
|
208
274
|
console.log(menu);
|
|
209
275
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swiftbar-github",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "GitHub notifications and review requests in the macOS menu bar",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:jryom/swiftbar-github",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@biomejs/biome": "^2.5.15",
|
|
29
29
|
"@types/node": "^26.6.5",
|
|
30
|
-
"@types/node-notifier": "^8.0.5",
|
|
31
30
|
"typescript": "^7.0.2"
|
|
32
31
|
}
|
|
33
32
|
}
|
package/swiftbar-github.3m.sh
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
# <swiftbar.hideAbout>true</swiftbar.hideAbout>
|
|
3
|
+
# <swiftbar.hideRunInTerminal>true</swiftbar.hideRunInTerminal>
|
|
4
|
+
# <swiftbar.hideLastUpdated>true</swiftbar.hideLastUpdated>
|
|
5
|
+
# <swiftbar.hideDisablePlugin>true</swiftbar.hideDisablePlugin>
|
|
6
|
+
# <swiftbar.hideSwiftBar>true</swiftbar.hideSwiftBar>
|
|
2
7
|
|
|
3
8
|
if output=$(npx --yes swiftbar-github@latest "$@" 2>/dev/null); then
|
|
4
9
|
[[ -z "$output" ]] || printf '%s\n' "$output"
|