Skip to content

Instantly share code, notes, and snippets.

@alisey
Last active December 8, 2021 13:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alisey/ef84a1f4292c97200bfd91f43f144dd7 to your computer and use it in GitHub Desktop.
Save alisey/ef84a1f4292c97200bfd91f43f144dd7 to your computer and use it in GitHub Desktop.
Show GitHub notification count in your macOS menu bar

GitHub Notifications

This little app shows the number of unread GitHub notifications in macOS menu bar.

There are other similar apps, but they either run a full fucking Electron, or have too many other features and look busy.

Installation

  1. Download and install xbar (beta version is fine). It's a small app that allows you to add output of any script (Bash, Python, JS) to the menu bar.
  2. Launch it, go to the menu, enable "Start at Login".
  3. cd ~/Library/Application\ Support/xbar/plugins/
  4. curl -O https://gist.githubusercontent.com/alisey/ef84a1f4292c97200bfd91f43f144dd7/raw/c4ad5e16c954e8b492f60b38307a9f30ef258e32/github-notifications.1m.js
  5. chmod +x github-notifications.1m.js
  6. xbar menu → Refresh All.
  7. Open https://github.com/settings/tokens
  8. Generate a new token:
    • Note: xbar
    • Expiration: any option you like
    • Select scopes: notifications
  9. Copy the access token somewhere.
  10. Enable SSO for the token (if required by organization).
  11. Click the GitHub icon in the menu bar → xbar → Open plugin.
  12. In the plugin settings paste your access token into the GITHUB_ACCESS_TOKEN field. WARNING: the token will be saved in plain text in the plugins folder, where other apps can access it. But you shouldn't be too worried, since the token has a very narrow scope.
  13. Click the refresh button on top of the settings.
  14. If you have unread notifications, you should see them in the menu bar.

It shouldn't be too difficult to edit github-notifications.1m.js to customize it to your needs. You will need to do so if your Node.js executable is not located in /usr/local/bin/node. Unfortunately #!/usr/bin/env node doesn't work in xbar.

#!/usr/local/bin/node
// <xbar.title>GitHub Notifications</xbar.title>
// <xbar.version>v1.0</xbar.version>
// <xbar.author>Alexey Lebedev</xbar.author>
// <xbar.author.github>alisey</xbar.author.github>
// <xbar.desc>Shows the number of unread GitHub notifications.</xbar.desc>
// <xbar.dependencies>node</xbar.dependencies>
// <xbar.var>string(GITHUB_ACCESS_TOKEN=""): GitHub personal access token with permission to access notifications.</xbar.var>
const https = require('https');
const { GITHUB_ACCESS_TOKEN } = process.env;
const gitHubLogo36Px =
'iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6' +
'JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEA' +
'AQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAJAA' +
'AAABAAAAkAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAJKADAAQAAAABAAAAJAAAAAA4NgJpAAAA' +
'CXBIWXMAABYlAAAWJQFJUiTwAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4' +
'bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJE' +
'RiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgog' +
'ICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJo' +
'dHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8' +
'L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+Cjwv' +
'eDp4bXBtZXRhPgoZXuEHAAAEgElEQVRYCb1YP1MbRxTftydsJ2SQRBrGmHCO7Jl0EV06RJcu+BNYfALz' +
'DYyrTKpA5w67TGVcuuLSxZWVLjPBYckgT9LEF3ucYWJpX37vTicvsCtOQpOdgX37/u3j/dn3DlJjroU4' +
'jitWf8OamszcIkU1qJAfWalSbFiR0cxJT9unfxhjMkrJX1SSTy0uNdpKq7vgb5WVyfm4o5i2u7+/fFxG' +
'7kKDFm7cakWR3VWK4jIKR/AYxWrrIsOikIJaHNc+nZv/Vmv1EMYUIQmxl8HXFKn1uWq9FtWrz0/S9MQn' +
'5PWQ5EnE+gkMafqEpoAzfeqv+fLrnEEDY/anEKKL7PYapV0pCRM8838YI9dmUZA7XRtO5ZDkjCL62mWA' +
'C/eI+CERScy/cGljwAl0bKPa/kQeOWlAC1eYrr39+/WzQtcwZIOyRjWdXtSntePjg0SwWThttAWld+XM' +
'eHPwDhkilTIXbxHjLx7mXoJc2ShyJU+H6FBk3eXe8cGg5c/BSLHLKPA76tdTY1IXL4rhrvQsvuCRMMyq' +
'mbhrfu0UuGJfXG5wATu76R69vCnnzKCQd4TBZ5DgJ10Bg8TdbXmj8qTWdC90wSe9ihPzEFc5vHg2yEmq' +
'LTSdM3Hw0l6lZ4RxGuta1uuk33lXS2zRkdLrXjKQxPSgSMgQzzh49NkUVfwgJENWr2tcuhpi6OneoxBt' +
'UnzXHGxD1uslrehLjVEhBoNvJdP0jnsBs33qnocwqRaSmuMhwgVYGfc4XZhCumtSZbXpXnYpbZlBl9Iw' +
'iTAev6ATxEPeBGMKPwWTGOHKoC8uu2cHTiWHvAahR8VnO7EjeCkQPbDpUyC9EbM4/+gjAlf7yOp2gDYx' +
'emmpse7rmZlCpiNNmjoh7Zro3rS9ZDV/H7oPHkp0X9m9EAPw8ayNggpGyHlJ1z9r7Aa9AwnWdg8JrxQ6' +
'8D62lsCB5R03A7zn0PmXC98HoXWOOEDAO51XR7+tVLIzqcdo/xkzK2VeYTZZjG/jQ7D/RJJbwVMRR4cw' +
'PCHmvZ7VP59Uep1R89DM+5n4atRfZSLkDLegY+RC5W0LQ+YhAa5jQBtcLsfMI9Kd/2X9wsELLV8YF7rG' +
'//F3I77VQrGI10utwgnCjLLPF7J7o4Cxi0d2pTsTRXdwTh2ajK6dkDHCd2yykTdxZUbBRGqroA+H/Dfp' +
'X6Zana/DZ18NiPFcdR6j5cGz2Xr1B80aNLRiop/Y0s7bN69/KZT4dnwQroC50OVjyXGsdjC+flcw5Dk0' +
'OFV0f+s961VEspmhiDdR9sU/DNqFUJkdYUiH+RAQkFD9gztd8jBkgpQQ9cneQUhMzkTNWY5eSJXk5/z3' +
'NN4mMcbi6/VsYZwySK6TGciSXftgFPIp4n0ZziXxZf/Y6k3XwPFh7ogxvnnrnEGFUVfIriBldtzLvNXm' +
'MpSBofMd/mCfMSJ+KodcfRI+nDcX40YHJXz/ssZIiNCKNo6P8o9O9y4X9nrIZUB5P8ILehPVJ89CIjT0' +
'v0PZRy3wmAE9gWxbHtvBczBKbHzaOAmNr5p43Bv+A9+MtJjLYYqGAAAAAElFTkSuQmCC';
async function httpsRequest(options, data = '') {
return new Promise((resolve, reject) => {
const request = https.request(options, (response) => {
let body = '';
response.setEncoding('utf8');
response.on('error', reject);
response.on('data', (chunk) => {
body += chunk;
});
response.on('end', () => resolve({response, body}));
});
request.on('error', reject);
request.write(data);
request.end();
});
}
async function gitHubGet(endpoint) {
const {response, body} = await httpsRequest({
host: 'api.github.com',
path: endpoint,
headers: {
'user-agent': 'xbar',
authorization: `token ${GITHUB_ACCESS_TOKEN}`,
},
timeout: 5000,
});
if (response.statusCode === 200 || response.statusCode === 304) {
return JSON.parse(body);
}
throw new Error(`HTTP Status ${response.statusCode}`);
}
function getNotificationUrl(notification) {
let url = notification.subject.url;
url = url.replace('api.github.com/repos', 'github.com');
url = url.replace('/pulls/', '/pull/');
return url;
}
function isOffTime() {
const date = new Date();
return date.getDay() === 0 || date.getDay() === 6 || date.getHours() < 6 || date.getHours() >= 19;
}
async function main() {
if (!GITHUB_ACCESS_TOKEN) {
console.log(`✕ | templateImage=${gitHubLogo36Px}`);
console.log('---');
console.log(`Configure GitHub access token in plugin settings`);
return;
}
try {
const notifications = await gitHubGet('/notifications');
const count = isOffTime() ? 0 : notifications.length;
console.log(`${count || ''} | templateImage=${gitHubLogo36Px}`);
if (notifications.length) {
console.log('---');
}
for (const notification of notifications) {
const title = notification.subject.title;
const url = getNotificationUrl(notification);
console.log(`☉ ${title} | href=${url} | refresh=true`);
}
console.log('---');
console.log('Open Notifications on GitHub | href=https://github.com/notifications');
} catch (error) {
console.log(`✕ | templateImage=${gitHubLogo36Px}`);
console.log('---');
console.log('Error: ' + error.message);
}
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment