swiftbar-github 1.0.0 → 1.1.0

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
@@ -17,3 +17,5 @@ Download [swiftbar-github.3m.sh](https://cdn.jsdelivr.net/npm/swiftbar-github@la
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
19
  Allow notifications for the bundled terminal-notifier in macOS System Settings. Click entries to open them on GitHub.
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/assets/github.svg CHANGED
@@ -1,3 +1,3 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
- <path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.56v-2.14c-3.2.7-3.88-1.36-3.88-1.36-.52-1.33-1.28-1.68-1.28-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.75 1.18 1.75 1.18 1.02 1.75 2.69 1.24 3.34.95.1-.74.4-1.24.73-1.53-2.55-.29-5.23-1.28-5.23-5.69 0-1.25.45-2.28 1.18-3.08-.12-.29-.51-1.46.11-3.04 0 0 .96-.31 3.16 1.18a11 11 0 0 1 5.75 0c2.2-1.49 3.16-1.18 3.16-1.18.62 1.58.23 2.75.11 3.04.73.8 1.18 1.83 1.18 3.08 0 4.42-2.69 5.4-5.25 5.68.41.36.78 1.06.78 2.14v3.17c0 .31.21.67.79.56A11.5 11.5 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5z"/>
2
+ <path transform="translate(4 4)" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
3
3
  </svg>
@@ -83,13 +83,14 @@ function notify(item) {
83
83
  notifier.notify(notification, (error) => resolve(!error));
84
84
  });
85
85
  }
86
- function action(label, argument) {
86
+ function action(label, args, refresh = false) {
87
87
  if (!launcher)
88
88
  return `${label} | href=${inbox}`;
89
- return `${label} | bash="${launcher.replaceAll('"', '\\"')}" param1=${argument} terminal=false`;
89
+ const params = args.map((arg, index) => `param${index + 1}=${arg}`).join(" ");
90
+ return `${label} | bash="${launcher.replaceAll('"', '\\"')}" ${params} terminal=false${refresh ? " refresh=true" : ""}`;
90
91
  }
91
- async function fetchActivity(argument) {
92
- const github = new Octokit({
92
+ async function client() {
93
+ return new Octokit({
93
94
  auth: await token(),
94
95
  userAgent: "swiftbar-github",
95
96
  request: {
@@ -103,6 +104,9 @@ async function fetchActivity(argument) {
103
104
  },
104
105
  log: { debug() { }, info() { }, warn() { }, error() { } },
105
106
  });
107
+ }
108
+ async function fetchActivity(argument) {
109
+ const github = await client();
106
110
  const { data: user } = await github.rest.users.getAuthenticated();
107
111
  const requested = await github.paginate(github.rest.search.issuesAndPullRequests, {
108
112
  q: `is:pr is:open archived:false review-requested:${user.login}`,
@@ -127,7 +131,7 @@ async function fetchActivity(argument) {
127
131
  };
128
132
  });
129
133
  if (argument === "open-reviews")
130
- return { reviews, notifications: [] };
134
+ return { reviews, notifications: [], unread: [] };
131
135
  const reviewURLs = new Set(reviews.map((item) => item.url));
132
136
  const unread = await github.paginate(github.rest.activity.listNotificationsForAuthenticatedUser, {
133
137
  all: false,
@@ -151,21 +155,21 @@ async function fetchActivity(argument) {
151
155
  reason,
152
156
  };
153
157
  });
154
- return { reviews, notifications };
158
+ return { reviews, notifications, unread };
155
159
  }
156
- function renderMenu(reviews, notifications) {
160
+ function renderMenu(reviews, notifications, unread) {
157
161
  const total = reviews.length + notifications.length;
158
162
  if (!total)
159
163
  return "";
160
164
  const menu = [
161
- `${total} | templateImage=${icon}`,
165
+ `${process.env.SWIFTBAR_GITHUB_DEV ? "Dev " : ""}${total} | templateImage=${icon}`,
162
166
  "---",
163
167
  `Review requests (${reviews.length}) | color=gray`,
164
168
  ];
165
169
  if (reviews.length) {
166
170
  for (const item of reviews)
167
171
  menu.push(`${item.repository} ${item.title} | href=${item.url}`);
168
- menu.push(action("Open all review requests", "open-reviews"));
172
+ menu.push(action("Open all review requests", ["open-reviews"]));
169
173
  }
170
174
  else
171
175
  menu.push("No reviews requested | color=gray");
@@ -173,37 +177,89 @@ function renderMenu(reviews, notifications) {
173
177
  if (notifications.length) {
174
178
  for (const item of notifications)
175
179
  menu.push(`${item.repository} [${item.reason}]: ${item.title} | href=${item.url}`);
176
- menu.push(action("Open all notifications", "open-notifications"));
180
+ menu.push(action("Open all notifications", ["open-notifications"]));
177
181
  }
178
182
  else
179
183
  menu.push("No unread notifications | color=gray");
184
+ if (unread.length) {
185
+ const types = {
186
+ PullRequest: "Pull requests",
187
+ Issue: "Issues",
188
+ Discussion: "Discussions",
189
+ Commit: "Commits",
190
+ Release: "Releases",
191
+ };
192
+ menu.push("---", "Mark as read");
193
+ menu.push(`--${action(`All notifications (${unread.length})`, ["mark-read", unread.map((item) => item.id).join(",")], true)}`);
194
+ for (const [heading, labelFor] of [
195
+ [
196
+ "By type",
197
+ (item) => text(types[item.subject.type] || item.subject.type),
198
+ ],
199
+ [
200
+ "By reason",
201
+ (item) => text(item.reason).replaceAll("_", " "),
202
+ ],
203
+ ]) {
204
+ const groups = new Map();
205
+ for (const item of unread) {
206
+ const label = labelFor(item);
207
+ const ids = groups.get(label) || [];
208
+ ids.push(item.id);
209
+ groups.set(label, ids);
210
+ }
211
+ menu.push("-----", `--${heading} | color=gray`);
212
+ for (const [label, ids] of [...groups].sort(([a], [b]) => a.localeCompare(b)))
213
+ menu.push(`--${action(`${label} (${ids.length})`, ["mark-read", ids.join(",")], true)}`);
214
+ }
215
+ }
180
216
  return menu.join("\n");
181
217
  }
182
- async function deliverAlerts(file, items) {
218
+ async function deliverAlerts(file, items, represented = []) {
183
219
  await mkdir(cache, { recursive: true });
184
220
  const path = join(cache, file);
185
221
  const seen = await state(path);
186
- const current = [];
222
+ const current = new Set(represented);
187
223
  for (const item of items) {
224
+ if (current.has(item.key))
225
+ continue;
188
226
  if (!seen || seen.has(item.key) || (await notify(item)))
189
- current.push(item.key);
227
+ current.add(item.key);
190
228
  }
191
- await save(path, current);
229
+ await save(path, [...current]);
192
230
  }
193
231
  async function main() {
194
232
  const argument = process.argv[2];
195
- if (argument && !["open-reviews", "open-notifications"].includes(argument))
233
+ if (argument &&
234
+ !["open-reviews", "open-notifications", "mark-read"].includes(argument))
196
235
  throw new Error("Usage: swiftbar-github");
197
- const { reviews, notifications } = await fetchActivity(argument);
236
+ if (argument === "mark-read") {
237
+ const ids = (process.argv[3] || "").split(",").map(Number);
238
+ if (ids.some((id) => !Number.isSafeInteger(id) || id <= 0))
239
+ throw new Error("Usage: swiftbar-github mark-read <thread-ids>");
240
+ const github = await client();
241
+ for (const id of ids)
242
+ await github.rest.activity.markThreadAsRead({ thread_id: id });
243
+ return;
244
+ }
245
+ const { reviews, notifications, unread } = await fetchActivity(argument);
198
246
  if (argument) {
199
247
  const items = argument === "open-reviews" ? reviews : notifications;
200
248
  for (const item of items)
201
249
  await exec("/usr/bin/open", ["-g", item.url]);
202
250
  return;
203
251
  }
204
- const menu = renderMenu(reviews, notifications);
252
+ const menu = renderMenu(reviews, notifications, unread);
253
+ const previousReviews = await state(join(cache, "requested-prs"));
254
+ const reviewURLs = new Set([
255
+ ...reviews.map((item) => item.url),
256
+ ...(previousReviews || []),
257
+ ]);
258
+ const represented = unread
259
+ .filter((item) => item.reason === "review_requested" && reviewURLs.has(webURL(item)))
260
+ .map((item) => `${item.id}:${item.updated_at}`);
205
261
  await deliverAlerts("requested-prs", reviews);
206
- await deliverAlerts("inbox-notifications", notifications);
262
+ await deliverAlerts("inbox-notifications", notifications, represented);
207
263
  if (menu)
208
264
  console.log(menu);
209
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swiftbar-github",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "GitHub notifications and review requests in the macOS menu bar",
5
5
  "license": "MIT",
6
6
  "repository": "github:jryom/swiftbar-github",
@@ -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"