#!/usr/bin/env osascript -l JavaScript // A script to close ephemeral Safari tabs. // // This is a script I run at the end of each working day, to close // Safari tabs I've opened that can be safely closed. // // See https://alexwlchan.net/2022/safari-tabs/ safari = Application("Safari"); // Generates all the window/tab/URLs in Safari. // // This runs in reverse window/tab index order: that is, windows are returned // bottom to top, and tabs from right to left. function* tabGenerator() { window_count = safari.windows.length; for (window_index = window_count - 1; window_index >= 0; window_index--) { window = safari.windows[window_index]; tab_count = window.tabs.length; for (tab_index = tab_count - 1; tab_index >= 0; tab_index--) { tab = window.tabs[tab_index]; yield [window_index, tab_index, tab.url()]; } } } function isSafeToClose(url) { // Sometimes we get a `null` as the URL of a tab; I'm not sure why, // so leave this tab open. if (url === null) { return false; } const urlPrefixes = [ "https://chat.openai.com/c/", "http://localhost:3000/", "http://localhost:5000/", "http://localhost:5757/", "http://localhost:5959/", "https://analytics.alexwlchan.net", "https://app.fastmail.com/mail/Inbox/?u=", "https://chat.openai.com/c/", "https://discord.com/", "https://github.com/Flickr-Foundation/", "https://zoom.us/", ]; const exactUrls = new Set([ "https://alexwlchan.net/", "https://arstechnica.com/", "https://bsky.app/", "https://calendar.google.com/calendar/u/0/r/week", "https://chat.openai.com/", "https://commons.flickr.org/", "https://daringfireball.net/", "https://docs.google.com/document/u/0/", "https://github.com/", "https://github.com/alexwlchan/books.alexwlchan.net", "https://github.com/alexwlchan/scripts", "https://lexies-library-lookup.netlify.app/", "https://www.linkedin.com/feed/", "https://mail.google.com/mail/u/0/#inbox", "https://mail.google.com/mail/u/1/#inbox", "https://mobile.twitter.com/home", "https://news.ycombinator.com/", "https://old.reddit.com/", "https://pinboard.in/u:alexwlchan", "https://remote.com", "https://social.alexwlchan.net/home", "https://social.alexwlchan.net/notifications", "https://twitter.com/home", "https://twitter.com/i/timeline", "https://twitter.com/notifications", "https://www.amazon.co.uk/", "https://www.facebook.com/", "https://www.macrumors.com", "https://www.macrumors.com/", "https://www.operationmincemeat.com", "https://www.theguardian.com/uk", "https://www.youtube.com/", "https://www.youtube.com/?app=desktop", "https://x.com/home", "history://", ]); return exactUrls.has(url) || urlPrefixes.find(p => url.startsWith(p)); } let closedCount = 0; var alreadySeenUrls = new Set(); // We can close a tab if: // // - it's safe to close, or // - the URL in this tab is open in another tab, so it's a dupe // for (const [window_index, tab_index, url] of tabGenerator()) { if (isSafeToClose(url)) { console.log(url); safari.windows[window_index].tabs[tab_index].close(); closedCount += 1; } else if (alreadySeenUrls.has(url)) { console.log(`${url} (open in another tab)`); safari.windows[window_index].tabs[tab_index].close(); closedCount += 1; } else { alreadySeenUrls.add(url); } } const remainingCount = alreadySeenUrls.size; console.log(`Closed ${closedCount} tab${closedCount !== 1 ? 's' : ''}; ${remainingCount} tab${remainingCount !== 1 ? 's' : ''} left open`)