├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── github.5m.js
├── package.json
├── screenshot.png
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Koki Sato
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install:
2 | mkdir -p $(HOME)/Library/Application\ Support/xbar/plugins
3 | ln -sf $(CURDIR)/github.5m.js $(HOME)/Library/Application\ Support/xbar/plugins/github.5m.js
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xbar Plugin for GitHub
2 |
3 |
4 |
5 |
6 |
7 | ## Prerequisites
8 |
9 | `node` is required to be installed with Homebrew.
10 |
11 | ```console
12 | $ brew install node
13 | ```
14 |
15 | ## Installation
16 |
17 | ### Clone repository
18 |
19 | ```console
20 | $ git clone git@github.com:koki-develop/xbar-plugin-github
21 | $ cd xbar-plugin-github
22 | $ make
23 | ```
24 |
25 | A symbolic link is created in `~/Library/Application Support/xbar/plugins/github.5m.js`.
26 |
27 | ### Install directly
28 |
29 | ```console
30 | $ wget https://raw.githubusercontent.com/koki-develop/xbar-plugin-github/main/github.5m.js -P ~/Library/Application\ Support/xbar/plugins/
31 | $ chmod +x ~/Library/Application\ Support/xbar/plugins/github.5m.js
32 | ```
33 |
34 | ## Configuration
35 |
36 | - `GITHUB_TOKEN` : Your GitHub Personal Access Token.
37 | - `SHOW_REVIEW_REQUESTED` : Show Pull Requests that are requested to review.
38 | - `SHOW_MY_PULL_REQUESTS` : Show your Pull Requests.
39 | - `SHOW_NOTIFICATIONS` : Show your notifications.
40 | - `SHOW_PULL_REQUEST_STATUS` : Show Pull Request's status.
41 | - `SHOW_PULL_REQUEST_BRANCHES` : Show Pull Request's base/head branches.
42 | - `SHOW_NOTIFICATION_REASON` : Show notification's reason.
43 | - `INCLUDE_BOT_PULL_REQUESTS` : Include Pull Requests created by bots.
44 | - `GITHUB_HOST` : Your GitHub Enterprise Host. Leave blank for GitHub.com.
45 |
46 | ## LICENSE
47 |
48 | [MIT](./LICENSE)
49 |
--------------------------------------------------------------------------------
/github.5m.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S PATH="${PATH}:/opt/homebrew/bin:/usr/local/bin" node
2 |
3 | // meta
4 | // GitHub
5 | // koki
6 | // koki-develop
7 | // node
8 |
9 | // variables
10 | // string(GITHUB_TOKEN=""): Your GitHub Personal Access Token.
11 | // boolean(SHOW_REVIEW_REQUESTED=true): Show Pull Requests that are requested to review.
12 | // boolean(SHOW_MY_PULL_REQUESTS=true): Show your Pull Requests.
13 | // boolean(SHOW_ISSUES_ASSIGNED=false): Show your Issues.
14 | // boolean(SHOW_NOTIFICATIONS=true): Show your notifications.
15 | // boolean(SHOW_PULL_REQUEST_STATUS=true): Show Pull Request's status.
16 | // boolean(SHOW_PULL_REQUEST_BRANCHES=true): Show Pull Request's base/head branches.
17 | // boolean(SHOW_NOTIFICATION_REASON=true): Show notification's reason.
18 | // boolean(INCLUDE_BOT_PULL_REQUESTS=false): Include Pull Requests created by bots.
19 | // string(GITHUB_HOST=""): Your GitHub Enterprise Host. Leave blank for GitHub.com.
20 |
21 | const config = {
22 | image:
23 | "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAAN1wAADdcAcvHpLkAAAAHdElNRQfoAQ4FJiFiADxdAAAUu0lEQVR42u2da3RcV3XHf/vcmZEt27KshyXHdixsTTyyHWt4OSQhUEhDSBZh0ULDoy0htKEUWKUuLY920S+lLV2lEJqS8u7ivQiPQggk0ARISghNChk7sS1bsuXEdiJbL1u2Zc3MvWf3w7kjydZzZu5Ilpr/WrOkGWnOvXfv89hn7//eB57DRQWZ7xsoFqlUGoDAc+9VJz6RAF7g3nZ0ZOb7lovCRamQ1q1pBFCrKIIQSl1BgWoxMmK0CkioEgdM+FWLkBfILbGSPYe9QF2uLTWCKhzcm5nvR52Ai0YhrW3bQQQJha5WMUbiitSDrgc2IGxAWQesBlYCy4AqIBwvBMAIcBY4BZwAjgKH3E95RtA+tZrDiFO6AKp07ds93yIA5lEhm1JpTALUAjbUgiEmlmaFy4EXhK/LgDVALWOCLxY+TkE9QCfwa+AxgT1q6MHiC2AQ4sA5Cwf3Z+ZFLnOukNZtaTQmSM66D2LG4Nt1wFUorwJeAlyK6/2VxBngCPAIcL/Aw1V4R7ISKApBlcH4SteTmTmVz5wqJJnaDuH8jbAM5cXA7wCvAjYB8Tl9+jHkgYPAj4HvCTyKMowBrNLZMXfTWcUVkkqlGYkrsQBQEGGlKr8F3Aq8HDcVXUw4CTwEfF2EH6tyUgR8A1W+VNxqq6hCWlPt7hKihRFxHfAu4GqguqJPVj5GgF8IfA7hh6qcFRFQpXPfropdtCIKaW1rxxrBWEWteGL0KmAnbmqq9NoQNc4B9wB3iMgvVTUoGONdFVBMpAppaUkTr7bOcjKCKOsU3gPcAjTPhfQqiOPAF4B/RzgKIAbyZ4TDhzORXaRUM3JSNFzShADiW4MxNwD/BrwJWD5XUqsglgPXAFcJPKMeh7CoicFA3/HILhKJQjan0qRr1nLKs4jIKjXmfcBHcXuIxYZ1wKtEiQvyhMDI8xvWkWhoor+vp+zGy1ZIsm07iDIYVwRagY/jFu7FMCqmwjKchbgRyAwaOyAIdY3NDJSplLIU0prajgBWDCJcAXwWeDVjvqXFDANsA3YI7BWCo4hQ19jEQG/pU1jJCkluaUcQ8DxQvQGnjPb5ltI8YB3wMjBdoqZLBOobm0tWSkkKKYyMXE4xhtfhFu+N8y2ZeUQ9cDWi3Xnf7vc8Sh4pRSskmdoOgHgexvBa4FPA+vmWyEWAWuBlnpFDqNeBKPUNzUVbYEUpxAWHFOt5YO2NwB04R+BzcKgBXoJop4p0GqCxYQ19RSz0s1ZIa2o7B9acpPbsEoxyJfA5/n9PU1OhFrjCwGMWjnauOklrzYZZT1+z2qmnUml8D8QqCkngyzg3+XOYGo8Atwh0WhHidnbh5FmZpz7WBZGEWuDveU4Zs8GVwN+rsEpQFwGbBWacsjYm2xEDqBpE/gr4E4rbZ4wAe4FjwDAQw4VdFwLOhPd9EBfMWgEsKeL7KSAL5r9RtLauicH+6aeuGfV26frLqaoxqOUG4KtAXZEPdZ8gbweGVXUlwkbgCpzn94W42PjFhHNABrgP+BVIlwgDqljQvwY+UGR7/cCtIvwgN6KINdM6I6dVyGVtaaxjfFwK3BUKshgosBPhkyGbYPRjEbNCVV8I/D7wWhxxYT4xiFPCN0F+gbH9WCceEUEd3+hqnCu+tsi2H0W4WeApz0DHk1O77aedehTFWDXAuyleGQADwCMo+CMgRvBHFE89gNPAz43Ku8SFcb8YCmW2UMDimCbjX5Yxzc8GI8DdwM1q5e3A94H+3ClDkIUgJ3TuzeD6rhzATV/FYgfKe8QaL/CFlpb0lP8Ym+oPybZ2R8fx5BqUW0u4CYDDihwCONydmeoaeYVfispjKvpN4C+BV4y7txFcWPVY+DoCPIubCgZxU0w2/F8P59Rcjhtxq3GujTW4zWsj50cqM8AnQL6n6JAImDjkhuDpo+ffr/UAod/47MVNtcXibVbsPQgPJqaZpCdVyOa2NFYVgRUKfx4+SCk4IuJi0pOhc98ukiETUR3R4Cc4is4twMuA3SI8rrAfpE+Uk4EGOYOAzLD8qYKAqvHEUINSB7oJSIcCfQa4QwyH1LpesVRg/+7JpxPjAxJYMAdKlEUDsFOt/MbmOL25Lc3+fZnZKcQAvoCB1+AW31LxTGLZSj935tSU/9AZ2uab2tLEUAKkv7Fmzcd7Tx+/UwhGVOW8lS7neSwPoGOShxmPlpY0Sxogf4YAGEQYRDkI/ARMwiqBERucO+kRS1ie7p4hHOsB1oAj3pWK60T0eoVvT2XeTlBIcks7vioG6hTeQXlkhDO5gRNQNbOVezAUcOuWNH1DPSCMqDWIB517MjN+/0IcPpyBwxc829Y0AGo1J4C3RIids5NOpxdi+Sk4uwKAIXXEu9iMX5qIauBPDTyQh8HWtjRdF3SsCY3m4hDLgYEbcJubOUVXBfm2pSi2gKGV4DluX7FGw4W4UuFahG8bb2IzE6ysqrzgiSxXeAvlb+CqzOGOMpu4eKDutZTSRkcBS4FbUVlhJ9m+n6eQTeFiblWvwNnc5aLZJtuNzZfToS4OGAvhwFhD+Wyda0B3gI5Oo6PXufCNp8YAv0s0O+gNGGpM/KIh2ZcM8QHxBOcOKRcrgNdjjdHg/D+MKqQ1lUZRArHPA66L6Dk2obppYlbNwoN6gGod0YWpr8OzLYjSui09+uHYCBnrxFcDz4voorXi3PULH6IguoboAnItKC+98MNRhYgzdWO4fUc5i9Z43C3w04U/YYGogJVOHHsxKLc9nIyvV5EYduzDUYWogIW1wIsjeoa9wIdVOLHwJ6wQolngX4AfRNTiDkHXjf/AALSk04X3lwMbIrjQOeCfLOwzgaDBwh8jnR0ZN205/9k/AE9H0Ow6lG2ojjocDcDAWheexUUCowge/VRU/tNTUKMcPJCZcwFWBNYAQvLYrseA/4igxSXAS0SU4V73gQFY3Q3qmaU4x1u5GBb4vIqeRuBABXMp5hpdHRkU6FzbDvB1SnPFX4h2xSypCU2FcA1RQFcDmyO4QAbkQVGpaGLLfMH53ASjXicuBa5ctKI0FN44hTifwBpKd7OPx/3qBYOLZiGfBFfvO4mVQIF7cTyBcrDGpX07iZlkMl34w0bKTzM7A/yCwJBYxNz3B9O1Ln4Iu3ABs3KwjHCvlkymMVgKm8LnUX4W7LNAB8Bw3zxKrMI4nMkQhnR7ceZ9OYgB6xGwARjiEMtVCc5pVi66FRlUok3zuhgRuNofI7gqEeXiEkNCpAqMGvAT+QTRsD56RWRkpujqYoAoqHvQYxE012jxEwgYVEE1RjT54kN4gU9uPkU1Nzg4Fuk7RfmulFWgMRRirjOrp465XS58fJmRf7CoIIygZUUQAWrEDQpiYUse0STyCwLZbPkNLSDEKD9gtVTDLUjBuShE4+Gt6tqziyVL51E8cw1lCeUrZFSpUSdnLk9ue37cRpr9ftFjBRHKsdCQ4qgt5aJW0cR8SGWusfHydOHXhjKaKcAn3KqbcMdpcZTNcrFaLEvLXuIWAEwOJOsZoCmC5oYFR+02zo0lATAUQcPNiq4CnZZQvChgFKpsNY47XC5OK+IrgkEERPIUxzyfCisJ4/Hx2vmT1VzBdT5aImjqJCK+2xgCYkweiML7tBy4XFDsIjZ9N7WlC79eRjRVjnq9ai8nAgYD6vsWxwaPAldYTHzRryMunPsinJVVLp4Nzjo2oTFj3t4jRGNpvUDQ9SKLdx0RFNQsw+WxlAufcbRwc2CM3HwQR04oFy3AK0SgbkMU+r24kAyLPAu6ldISdy7EMGEouHNP5rwNzdO4wsPlwgPepCorh07E2LQ5PcciqzxiCU8U3kA0e5ATjPMYhwoZDbZ0RnTPV4FeJ6L4+cWzbU+2tYNV/HywFXhjRM12IdJb8MgagJEaMIEdxoUko0A18G6UxniVT3JLeo5FFz2SbWHRHYunym1ERyndZcQOjwy4NwYglgXrCcD/QGTRjGsU3mnEM6C0bt0+h+KLFqlU2u2gEazhJuCtETWdA35lrRALyxEYKMSIAdhNNBEwcGvJe60GryNkLrZuWXhK2dyW5o+euZRABES3Ax8huuLPT4PsYlzIe9yiLihyBPjfCJ+nHvioevpyAoNR4bK2hVN0rjW1HavKZ9Y+hYFWVT4ObI3wEo8bw7HxAb0xsrWCiOZwqclRsLsLSKJ8BtGbLhleggKb2tpHD2a5WJFs244IqJvK260rYXhthJcIgJ/YQHM6bliMmkD1DaMegDPATcCqCC/eAFw7FPctsEcgq0aob2iivmkNA73ll1eNAqlUmlWNTTQ0NgOCCHHV0RKGOyK+3GER/hHoxwgDJ5wMRhUy0NdDXeMaQE7iWPDPj/gGlgGvxA35Y8bIM6qulEpN4yXUNzYzGEHd21LQ0pKmaeNqAh8Q2GGqOEawVVU+DHyYypQw/E7MypdBtHNc5vF5ocfk5jTqzIkbgW8xfZw9wO1bCoTjtTgG3mxqu/cC3xf4CvC4uroniLgiAVbhklfDke9SEX7XZVvSiED+HEiVOiEIgCZAtqHcDNxMdJlkF+IM8HvAfQocHMeBPk8hLek0sSyuUJnqd3A9eirsBt6MMd2oCsoKQV+k8Mc4hc4mcjgIPArch/CQIIe8uDnlZwMtcDULucgY6Npb2jZpU1s6PKRBx5465FWJ6jKcu+cK4HpcOfEoSIPT4b9E5Q3AkMf5lebOIzYczmS4LNmOepxU4UvhzU1FL10LbDeB2avGR5FhhR+CPAR6G/AhZnYtrAqFcD1Kn6IH/FywG2EXjhF4DDeaBnG1UMqBCa9XF977BlFtx6VgFNzoc+FW8AW+qaJD8SplX+b8w2ImME38hOBZELhP0ceAq6ZouB643Yq/XuCLKtp/dFWW9QNLTq/2Y5/ojfl96sqO18/yRhvCV+F6Z1GGgAeBnaKUt8A4dsgHgTeirMS5zeeDQfYoyN0CZIcnXn5Cjxjs7aGxoRlf9Kw41/CNTE0RWk64UBt4bOVIbEAMnBNL3JjdgZsiXk5pPS+B28n+nW/4TVyl5GL3DfXNYDSPG22vAy5hfpSRFfiIhYc9hK5JDh6blL6i4k4sQ+RuXA+dDh5wk8IdqqzRwE35OVVV5E5cQbBS8RWj3o/iWp7s1BWoBzSDK/wc5T6rGPwM5Ltm7GTGCZi05/b39dC8ohEbkxFcaPc1zFz8sRVIIDyAEChS2GgeAn6b4vc1JwT5kIo9BjJaxqkUDPT1UNfQXBgTR3HrVhRskWIwCLwfeFJgyuyyKQlewyudrkTlAVw+3WxwC8JNWEXjBqyg2F8Df4sr91cMHkfZY1QmlDAqBV0dGVQ9EkH2GPBA2Q0Wj68J5n5h+lS/KRVyOJPBC0DRPHA7zsydCTUoOxFpNvnAuWPUg8D7Bq53FLMI/BoJhoMI7Z4gbsl7VeC82nM5be0CblesLzNwDaalQI5UC37cYKx24nKzZ8PduhJ4i/GU0fJDxloNvC8K/CGu8ud0eXkj4QPcr24liwyugwkg3bg6jnOB08BHjKcH/bzhwAxT77T972RPD3WNzWGZB9mPoRZnlk4nJwHq1Mr3jadniTlepLij8w4Z5AcKv8KNlkFcCPMpnJf5HlxRyo8RxJ4IgxCR+boGe3uoa2wCpBp4M5WvGazAvyLmU2rFIjDYP/2zzKoDtm5OhxxHVuMS5m+c4StZ4DaUr1AlEICE1pfbJCvxlXnypxLVIuGOXjgHNqs2LHDpCxilK+KDHJNt7aCsU+FnOEOkkrgXlVsRPY7IrKrlzSoFISaQ8yxeYE6o8CGUtUxfpqgKeAfC/ZLTZ+F8q6I1lSY3kEA8hilMXwo2b8DMwcHAMiessd3ABwU9bgMlbmY3+c56im5NpYmrkPUsnvJKhS8xPa9VgTsN8n5Fh2PiEWA5MM9nmCddgGytws+p3Ag5JnBLIDxQZYW8mX0tyVnbMAN9PTQ0NiMqBDXD3ZKLH8P5uqbKSBcgrdAowhO+2lMC1K9eQ21TE011kx90kkqlaWhoHn0VcxjKbFDf2Awufe9tFF/HfjboAXZab/iemE0gwIEizPaijMq+cIMl2QSxhuN77Lnlz+JcI1O56T1cuaerjWPYDxhjzmBVrUBdY/OElxUYigVUB0YE6O1fUArpA97n9Q5+g2pnL8xkVV2IotPYOjsytKbSBP1NxCX7jbxWGeCfmZ50vENdwKszsPZR3PzajSsXnsOZ38txjsh1ywOvNu/xWaIpgTRXOAG8P6Hxr+UbnZ47S9jQlpRX2NWRIdm2HV+r1Cw9/VV7bkUOV9hrujUlDmwJX+AUER7qPRr9iIWvE7jTGBaKQo4Bf7GyaeNdQ8e7nYxKXCtLzo3r3LcbVcEO1xDf1n8Xbgp4oogmErgattXhzyWMdZDRFK8FgCeB22g+dNep492olDYyCigrWbGrIwMK/u4GrPMP/QHwo/mW0Bzix8BbrXKv9GxyMinTiiw7e7RrfwYjUGUFEXYLcivwMcpLkfN0LIe+EihMkaXiLPBJkLeJ8Hgs5AJE4QSNJJ13f0eGvIThb9UTivyNwG3A4yU26VF+ZaJJEYboqyg9L/9J4J0gHwB6VCFQ6IxofxVZfnVXRwY/4TqeQM6id4ljVtzJ3DnyZotSRsdp4Asgr1fRrwJZMeCfEw5G6N6Jqj4vMMYRTral8dTDij1oVHZa0XuA9+IyjhZaHrsPPAzcDnIf6IhnPaxoWactTIVIFVJAwcoIjzTKKdxrVB5R0TfgprIXVOras4IztGcaJRZnNX4a5FuI9hfsvgMdpc7EM6Oi55537tuFBzSQANGTKnxe3Ils7wTuxy2OkwtDyVfM8BWyTE0rGgEeAv4M5SYx+mnQfqwBha4KF/aseC8tkMCSbe0YsaDmOPAFQb4D+lJ1JzFci9tUFjrIwxJNpbapcBz4KbApfK+4LOSHgO+C/AzRfoBsfwyv2tJ9qHKjYjzmnArTmkqjKhijKIqoianYVpxPLAk8C/JtVJ9KZAP2dj8Z6fU3prdhsp6rwiq8CWQDaCfwEGr2Y2wuDMiBRB+PmQnzVmosmUoTWIPn2XH1vwo52iHXU0zkRyC1ptJhfT7Cnxqe+CagQpB1R+ctmmrcz+E5LCr8H8r5els1RDRkAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI0LTAxLTE0VDA1OjM3OjE1KzAwOjAwLMsTPAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNC0wMS0xNFQwNTozNzoxNSswMDowMF2Wq4AAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjQtMDEtMTRUMDU6Mzg6MzMrMDA6MDDafeOVAAAAAElFTkSuQmCC",
24 | githubHost: process.env["GITHUB_HOST"] || "github.com",
25 | token: process.env["GITHUB_TOKEN"],
26 | showReviewRequested: process.env["SHOW_REVIEW_REQUESTED"] === "true",
27 | showMyPullRequests: process.env["SHOW_MY_PULL_REQUESTS"] === "true",
28 | showIssuesAssigned: process.env["SHOW_ISSUES_ASSIGNED"] === "true",
29 | showNotifications: process.env["SHOW_NOTIFICATIONS"] === "true",
30 | showPullRequestStatus: process.env["SHOW_PULL_REQUEST_STATUS"] === "true",
31 | showBranches: process.env["SHOW_PULL_REQUEST_BRANCHES"] === "true",
32 | showNotificationReason: process.env["SHOW_NOTIFICATION_REASON"] === "true",
33 | includeBotPullRequests: process.env["INCLUDE_BOT_PULL_REQUESTS"] === "true",
34 | };
35 |
36 | const botNames = ["renovate", "dependabot"];
37 | const restApiEndpoint =
38 | config.githubHost === "github.com"
39 | ? "api.github.com"
40 | : `${config.githubHost}/api/v3`;
41 | const graphqlApiEndpoint =
42 | config.githubHost === "github.com"
43 | ? "api.github.com"
44 | : `${config.githubHost}/api`;
45 |
46 | /*
47 | * type definitions
48 | */
49 |
50 | /**
51 | * @typedef {Object} GitHubUser
52 | * @property {string} login
53 | */
54 |
55 | /**
56 | * @typedef {Object} GitHubRepository
57 | * @property {string} name
58 | * @property {GitHubUser} owner
59 | * @property {string} url
60 | */
61 |
62 | /**
63 | * @typedef {Object} GitHubPullRequest
64 | * @property {string} title
65 | * @property {string} url
66 | * @property {number} number
67 | * @property {string} headRefName
68 | * @property {string} baseRefName
69 | * @property {boolean} isDraft
70 | * @property {GitHubRepository} repository
71 | */
72 |
73 | /**
74 | * @typedef {Object} GitHubIssue
75 | * @property {string} title
76 | * @property {string} url
77 | * @property {GitHubRepository} repository
78 | * @property {string} number
79 | */
80 |
81 | /**
82 | * @typedef {Object} GitHubNotification
83 | * @property {string} id
84 | * @property {string} reason
85 | * @property {string} title
86 | * @property {string} html_url
87 | * @property {GitHubRepository} repository
88 | * @property {GitHubNotificationSubject} subject
89 | */
90 |
91 | /**
92 | * @typedef {Object} GitHubNotificationSubject
93 | * @property {string} title
94 | * @property {string} url
95 | * @property {string | null} latest_comment_url
96 | */
97 |
98 | /**
99 | * @returns {string}
100 | */
101 | const buildQueryPullRequestsReviewRequested = () => {
102 | const filters = ["is:pr", "is:open", "review-requested:@me"];
103 | if (!config.includeBotPullRequests)
104 | filters.push(...botNames.map((botName) => `-author:app/${botName}`));
105 | return filters.join(" ");
106 | };
107 |
108 | /**
109 | * @returns {Promise}
110 | */
111 | const fetchPullRequestsReviewRequested = async () => {
112 | return await searchPullRequests(buildQueryPullRequestsReviewRequested());
113 | };
114 |
115 | /**
116 | * @returns {string}
117 | */
118 | const buildQueryPullRequestsMine = () => {
119 | const filters = ["is:pr", "is:open", "author:@me"];
120 | if (!config.includeBotPullRequests)
121 | filters.push(...botNames.map((botName) => `-author:app/${botName}`));
122 | return filters.join(" ");
123 | };
124 |
125 | /**
126 | * @returns {Promise}
127 | */
128 | const fetchPullRequestsMine = async () => {
129 | return await searchPullRequests(buildQueryPullRequestsMine());
130 | };
131 |
132 | /**
133 | * @param {string} q
134 | * @returns {Promise}
135 | */
136 | const searchPullRequests = async (q) => {
137 | const query = `
138 | query {
139 | search(query: "${q}", type: ISSUE, first: 100) {
140 | edges {
141 | node {
142 | ... on PullRequest {
143 | title
144 | url
145 | number
146 | headRefName
147 | baseRefName
148 | isDraft
149 | repository {
150 | name
151 | owner {
152 | login
153 | }
154 | }
155 | commits(last: 1) {
156 | nodes {
157 | commit {
158 | checkSuites(last: 1) {
159 | nodes {
160 | conclusion
161 | }
162 | }
163 | }
164 | }
165 | }
166 | }
167 | }
168 | }
169 | }
170 | }
171 | `;
172 |
173 | data = await fetch(`https://${graphqlApiEndpoint}/graphql`, {
174 | method: "POST",
175 | headers: {
176 | Authorization: `Bearer ${config.token}`,
177 | },
178 | body: JSON.stringify({ query }),
179 | }).then(async (resp) => {
180 | const data = await resp.json();
181 | if (!resp.ok) {
182 | throw new Error(JSON.stringify(data));
183 | }
184 | return data;
185 | });
186 |
187 | return data.data.search.edges.map((edge) => edge.node);
188 | };
189 |
190 | /**
191 | * @returns {string}
192 | */
193 | const buildQueryIssuesAssigned = () => {
194 | const filters = ["is:issue", "is:open", "assignee:@me"];
195 | return filters.join(" ");
196 | };
197 |
198 | /**
199 | * @returns {Promise}
200 | */
201 | const fetchIssuesAssigned = async () => {
202 | return await searchIssues(buildQueryIssuesAssigned());
203 | };
204 |
205 | /**
206 | * @param {string} q
207 | * @returns {Promise}
208 | */
209 | const searchIssues = async (q) => {
210 | const query = `
211 | query {
212 | search(query: "${q}", type: ISSUE, first: 100) {
213 | edges {
214 | node {
215 | ... on Issue {
216 | title
217 | url
218 | number
219 | repository {
220 | name
221 | owner {
222 | login
223 | }
224 | }
225 | }
226 | }
227 | }
228 | }
229 | }
230 | `;
231 |
232 | data = await fetch(`https://${graphqlApiEndpoint}/graphql`, {
233 | method: "POST",
234 | headers: {
235 | Authorization: `Bearer ${config.token}`,
236 | },
237 | body: JSON.stringify({ query }),
238 | }).then(async (resp) => {
239 | const data = await resp.json();
240 | if (!resp.ok) {
241 | throw new Error(JSON.stringify(data));
242 | }
243 | return data;
244 | });
245 |
246 | return data.data.search.edges.map((edge) => edge.node);
247 | };
248 |
249 | /**
250 | * @param {number} max
251 | * @returns {Promise<[GitHubNotification[], boolean]>}
252 | */
253 | const fetchNotifications = async (max) => {
254 | const notifications = await fetch(
255 | `https://${restApiEndpoint}/notifications?per_page=${max + 1}`,
256 | {
257 | headers: {
258 | Authorization: `Bearer ${config.token}`,
259 | },
260 | },
261 | ).then(async (resp) => {
262 | const data = await resp.json();
263 | if (!resp.ok) {
264 | throw new Error(JSON.stringify(data));
265 | }
266 | return data;
267 | });
268 |
269 | return [
270 | await Promise.all(
271 | notifications.slice(0, max).map(async (notification) => {
272 | const resourceUrl =
273 | notification.subject.latest_comment_url ?? notification.subject.url;
274 | if (!resourceUrl) {
275 | return notification;
276 | }
277 | const resource = await fetch(resourceUrl, {
278 | headers: {
279 | Authorization: `Bearer ${config.token}`,
280 | },
281 | }).then((resp) => resp.json());
282 | return {
283 | ...notification,
284 | html_url: resource.html_url,
285 | };
286 | }),
287 | ),
288 | notifications.length > max,
289 | ];
290 | };
291 |
292 | const readNotification = async (id) => {
293 | await fetch(`https://${restApiEndpoint}/notifications/threads/${id}`, {
294 | method: "PATCH",
295 | headers: {
296 | Authorization: `Bearer ${config.token}`,
297 | },
298 | });
299 | };
300 |
301 | const readAllNotifications = async () => {
302 | await fetch(`https://${restApiEndpoint}/notifications`, {
303 | method: "PUT",
304 | headers: {
305 | Authorization: `Bearer ${config.token}`,
306 | },
307 | body: JSON.stringify({
308 | last_read_at: new Date().toISOString(),
309 | read: true,
310 | }),
311 | });
312 | };
313 |
314 | /**
315 | * @param {any} resources
316 | * @returns {Record}
317 | */
318 | const groupResourcesByRepo = (resources) => {
319 | const repositories = resources.reduce((acc, pr) => {
320 | const key = `${pr.repository.owner.login}/${pr.repository.name}`;
321 | if (!acc[key]) acc[key] = [];
322 | acc[key].push(pr);
323 | return acc;
324 | }, {});
325 |
326 | return Object.entries(repositories).sort(([a], [b]) => (a > b ? 1 : -1));
327 | };
328 |
329 | /**
330 | * @param {string} str
331 | * @returns string
332 | */
333 | const escapePipe = (str) => str.replaceAll(/\|/g, "ǀ");
334 |
335 | /**
336 | * @param {GitHubPullRequest[]} pullRequests
337 | * @returns {string[]}
338 | */
339 | const pullRequestsToLines = (pullRequests) => {
340 | const lines = [];
341 | for (const pullRequest of pullRequests) {
342 | const prefix = (() => {
343 | /** @type {string[]} */
344 | const cols = [];
345 |
346 | if (config.showPullRequestStatus) {
347 | const conclusion =
348 | pullRequest.commits.nodes[0].commit.checkSuites.nodes[0]?.conclusion;
349 | const emoji = conclustionToEmoji(conclusion);
350 | if (emoji) cols.push(emoji);
351 | }
352 |
353 | if (pullRequest.isDraft) cols.push("(Draft)");
354 |
355 | if (cols.length === 0) return "";
356 | return `${cols.join(" ")} `;
357 | })();
358 | lines.push(
359 | `${prefix}${escapePipe(pullRequest.title)} #${pullRequest.number} | href=${pullRequest.url}`,
360 | );
361 | if (config.showBranches) {
362 | lines.push(
363 | `${escapePipe(`${pullRequest.baseRefName} ← ${pullRequest.headRefName}`)} | size=10`,
364 | );
365 | }
366 | }
367 | return lines;
368 | };
369 |
370 | /**
371 | * @param {GitHubIssue[]} issues
372 | * @returns {string[]}
373 | * */
374 | const issuesToLines = (issues) => {
375 | const lines = [];
376 | for (const issue of issues) {
377 | lines.push(
378 | `${escapePipe(issue.title)} #${issue.number} | href=${issue.url}`,
379 | );
380 | }
381 | return lines;
382 | };
383 |
384 | /**
385 | * @param {string} conclusion
386 | * @returns {string | null}
387 | */
388 | const conclustionToEmoji = (conclusion) => {
389 | switch (conclusion) {
390 | case "SUCCESS":
391 | return ":white_check_mark:";
392 | case "FAILURE":
393 | case "TIMED_OUT":
394 | case "STARTUP_FAILURE":
395 | return ":x:";
396 | case "CANCELLED":
397 | return ":no_entry:";
398 | case "ACTION_REQUIRED":
399 | return ":clock12:";
400 | default:
401 | return null;
402 | }
403 | };
404 |
405 | (async () => {
406 | const [executable, script, ...args] = process.argv;
407 |
408 | if (args.length > 0) {
409 | // NOTE: When executed from the menu, xbar.var is not set, so get the token from the arguments.
410 | config.token = args[0];
411 | const command = args[1];
412 |
413 | switch (command) {
414 | case "read-notification":
415 | const id = args[2];
416 | await readNotification(id);
417 | break;
418 | case "read-all-notifications":
419 | await readAllNotifications();
420 | break;
421 | default:
422 | throw new Error(`Unknown command: ${command}`);
423 | }
424 |
425 | return;
426 | }
427 |
428 | if (!config.token) {
429 | console.log(`:warning: | image=${config.image}`);
430 | console.log("---");
431 | console.log("GITHUB_TOKEN not set. Please set it in the plugin settings.");
432 | return;
433 | }
434 |
435 | /** @type {Promise} */
436 | const promises = [];
437 | /** @type {Record} */
438 | const countsMap = {};
439 | /** @type {string[]} */
440 | const reviewRequestedLines = [];
441 | /** @type {string[]} */
442 | const mineLines = [];
443 | /** @type {string[]} */
444 | const issuesAssignedLines = [];
445 | /** @type {string[]} */
446 | const notificationsLines = [];
447 |
448 | /*
449 | * Review Requested
450 | */
451 | if (config.showReviewRequested) {
452 | const promise = fetchPullRequestsReviewRequested().then((pullRequests) => {
453 | reviewRequestedLines.push(
454 | `:eyes: Review Requested (${pullRequests.length}) | color=red href=https://${config.githubHost}/search?q=${encodeURIComponent(buildQueryPullRequestsReviewRequested())}`,
455 | );
456 |
457 | countsMap.reviewRequested = pullRequests.length;
458 | if (pullRequests.length === 0) {
459 | reviewRequestedLines.push("No pull requests");
460 | reviewRequestedLines.push("---");
461 | return;
462 | }
463 |
464 | const byRepo = groupResourcesByRepo(pullRequests);
465 | for (const [repo, pullRequests] of byRepo) {
466 | reviewRequestedLines.push(
467 | `${repo} | size=12 color=red`,
468 | ...pullRequestsToLines(pullRequests),
469 | );
470 | }
471 | reviewRequestedLines.push("---");
472 | });
473 | promises.push(promise);
474 | }
475 |
476 | /*
477 | * My Pull Requests
478 | */
479 | if (config.showMyPullRequests) {
480 | const promise = fetchPullRequestsMine().then((pullRequests) => {
481 | mineLines.push(
482 | `:seedling: My Pull Requests (${pullRequests.length}) | color=green href=https://${config.githubHost}/search?q=${encodeURIComponent(buildQueryPullRequestsMine())}`,
483 | );
484 |
485 | countsMap.mine = pullRequests.length;
486 | if (pullRequests.length === 0) {
487 | mineLines.push("No pull requests");
488 | mineLines.push("---");
489 | return;
490 | }
491 |
492 | const byRepo = groupResourcesByRepo(pullRequests);
493 | for (const [repo, pullRequests] of byRepo) {
494 | mineLines.push(
495 | `${repo} | size=12 color=green`,
496 | ...pullRequestsToLines(pullRequests),
497 | );
498 | }
499 | mineLines.push("---");
500 | });
501 | promises.push(promise);
502 | }
503 |
504 | /*
505 | * Issues
506 | */
507 | if (config.showIssuesAssigned) {
508 | const promise = fetchIssuesAssigned().then((issues) => {
509 | issuesAssignedLines.push(
510 | `:pushpin: Issues Assigned (${issues.length}) | color=pink href=https://${config.githubHost}/search?q=${encodeURIComponent(buildQueryIssuesAssigned())}`,
511 | );
512 |
513 | countsMap.issuesAssigned = issues.length;
514 | if (issues.length === 0) {
515 | issuesAssignedLines.push("No issue assigned");
516 | issuesAssignedLines.push("---");
517 | return;
518 | }
519 |
520 | const byRepo = groupResourcesByRepo(issues);
521 | for (const [repo, issues] of byRepo) {
522 | issuesAssignedLines.push(
523 | `${repo} | size=12 color=red`,
524 | ...issuesToLines(issues),
525 | );
526 | }
527 | issuesAssignedLines.push("---");
528 | });
529 | promises.push(promise);
530 | }
531 |
532 | /*
533 | * Notifications
534 | */
535 | if (config.showNotifications) {
536 | const max = 20;
537 | const promise = fetchNotifications(max).then(([notifications, hasMore]) => {
538 | const count = hasMore ? `${max}+` : notifications.length.toString();
539 |
540 | notificationsLines.push(
541 | `:bell: Notifications (${count}) | color=yellow href=https://${config.githubHost}/notifications`,
542 | );
543 |
544 | countsMap.notifications = count;
545 | if (notifications.length === 0) {
546 | notificationsLines.push("No notifications");
547 | notificationsLines.push("---");
548 | return;
549 | }
550 |
551 | notificationsLines.push(
552 | `--Mark all as read | shell="${executable}" param1="${script}" param2=${config.token} param3=read-all-notifications refresh=true`,
553 | );
554 |
555 | const byRepo = groupResourcesByRepo(notifications);
556 | for (const [repo, notifications] of byRepo) {
557 | notificationsLines.push(`${repo} | size=12 color=yellow`);
558 |
559 | for (const notification of notifications) {
560 | const prefix = config.showNotificationReason
561 | ? `(${notification.reason}) `
562 | : "";
563 | notificationsLines.push(
564 | `${prefix}${escapePipe(notification.subject.title)} | href=${notification.html_url}`,
565 | `--Mark as read | shell="${executable}" param1="${script}" param2=${config.token} param3=read-notification param4=${notification.id} refresh=true`,
566 | );
567 | }
568 | }
569 | });
570 | promises.push(promise);
571 | }
572 |
573 | // Wait for all promises to complete
574 | await Promise.all(promises);
575 |
576 | /*
577 | * Menu bar
578 | */
579 |
580 | /** @type {number[]} */
581 | const counts = [];
582 | if (config.showReviewRequested) counts.push(countsMap.reviewRequested);
583 | if (config.showMyPullRequests) counts.push(countsMap.mine);
584 | if (config.showIssuesAssigned) counts.push(countsMap.issuesAssigned);
585 | if (config.showNotifications) counts.push(countsMap.notifications);
586 |
587 | /** @type {string[]} */
588 | const menubarLines = [];
589 | menubarLines.push(
590 | `(${counts.map((i) => i.toString()).join("/")}) | templateImage=${config.image}`,
591 | "---",
592 | `Last updated at ${new Date().toLocaleString()} | size=12`,
593 | "---",
594 | );
595 |
596 | /*
597 | * Output
598 | */
599 |
600 | /** @type {string[]} */
601 | const lines = [];
602 | lines.push(...menubarLines);
603 | if (config.showReviewRequested) lines.push(...reviewRequestedLines);
604 | if (config.showMyPullRequests) lines.push(...mineLines);
605 | if (config.showIssuesAssigned) lines.push(...issuesAssignedLines);
606 | if (config.showNotifications) lines.push(...notificationsLines);
607 |
608 | console.log(lines.join("\n"));
609 | })();
610 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xbar-plugin-github",
3 | "main": "github.5m.js",
4 | "author": "koki-develop ",
5 | "license": "MIT",
6 | "devDependencies": {
7 | "prettier": "^3.2.2"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koki-develop/xbar-plugin-github/1811aacdce4f3ec17f220c9c765c15172df5dd09/screenshot.png
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | prettier@^3.2.2:
6 | version "3.2.2"
7 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.2.tgz#96e580f7ca9c96090ad054616c0c4597e2844b65"
8 | integrity sha512-HTByuKZzw7utPiDO523Tt2pLtEyK7OibUD9suEJQrPUCYQqrHr74GGX6VidMrovbf/I50mPqr8j/II6oBAuc5A==
9 |
--------------------------------------------------------------------------------