Skip to main content

macos/close_tabs

1#!/usr/bin/env osascript -l JavaScript
2// A script to close ephemeral Safari tabs.
3//
4// This is a script I run at the end of each working day, to close
5// Safari tabs I've opened that can be safely closed.
6//
7// See https://alexwlchan.net/2022/safari-tabs/
9safari = Application("Safari");
11// Generates all the window/tab/URLs in Safari.
12//
13// This runs in reverse window/tab index order: that is, windows are returned
14// bottom to top, and tabs from right to left.
15function* tabGenerator() {
16 window_count = safari.windows.length;
18 for (window_index = window_count - 1; window_index >= 0; window_index--) {
19 window = safari.windows[window_index];
21 tab_count = window.tabs.length;
23 for (tab_index = tab_count - 1; tab_index >= 0; tab_index--) {
24 tab = window.tabs[tab_index];
26 yield [window_index, tab_index, tab.url()];
27 }
28 }
31function isSafeToClose(url) {
33 // Sometimes we get a `null` as the URL of a tab; I'm not sure why,
34 // so leave this tab open.
35 if (url === null) { return false; }
37 const urlPrefixes = [
38 "https://chat.openai.com/c/",
39 "http://localhost:3000/",
40 "http://localhost:5000/",
41 "http://localhost:5757/",
42 "http://localhost:5959/",
43 "https://analytics.alexwlchan.net",
44 "https://app.fastmail.com/mail/Inbox/?u=",
45 "https://chat.openai.com/c/",
46 "https://discord.com/",
47 "https://github.com/Flickr-Foundation/",
48 "https://zoom.us/",
49 ];
51 const exactUrls = new Set([
52 "https://alexwlchan.net/",
53 "https://arstechnica.com/",
54 "https://bsky.app/",
55 "https://calendar.google.com/calendar/u/0/r/week",
56 "https://chat.openai.com/",
57 "https://commons.flickr.org/",
58 "https://daringfireball.net/",
59 "https://docs.google.com/document/u/0/",
60 "https://github.com/",
61 "https://github.com/alexwlchan/books.alexwlchan.net",
62 "https://github.com/alexwlchan/scripts",
63 "https://lexies-library-lookup.netlify.app/",
64 "https://www.linkedin.com/feed/",
65 "https://mail.google.com/mail/u/0/#inbox",
66 "https://mail.google.com/mail/u/1/#inbox",
67 "https://mobile.twitter.com/home",
68 "https://news.ycombinator.com/",
69 "https://old.reddit.com/",
70 "https://pinboard.in/u:alexwlchan",
71 "https://remote.com",
72 "https://social.alexwlchan.net/home",
73 "https://social.alexwlchan.net/notifications",
74 "https://twitter.com/home",
75 "https://twitter.com/i/timeline",
76 "https://twitter.com/notifications",
77 "https://www.amazon.co.uk/",
78 "https://www.facebook.com/",
79 "https://www.macrumors.com",
80 "https://www.macrumors.com/",
81 "https://www.operationmincemeat.com",
82 "https://www.theguardian.com/uk",
83 "https://www.youtube.com/",
84 "https://www.youtube.com/?app=desktop",
85 "https://x.com/home",
86 "history://",
87 ]);
89 return exactUrls.has(url) || urlPrefixes.find(p => url.startsWith(p));
92let closedCount = 0;
94var alreadySeenUrls = new Set();
96// We can close a tab if:
97//
98// - it's safe to close, or
99// - the URL in this tab is open in another tab, so it's a dupe
100//
101for (const [window_index, tab_index, url] of tabGenerator()) {
102 if (isSafeToClose(url)) {
103 console.log(url);
104 safari.windows[window_index].tabs[tab_index].close();
105 closedCount += 1;
106 } else if (alreadySeenUrls.has(url)) {
107 console.log(`${url} (open in another tab)`);
108 safari.windows[window_index].tabs[tab_index].close();
109 closedCount += 1;
110 } else {
111 alreadySeenUrls.add(url);
112 }
115const remainingCount = alreadySeenUrls.size;
117console.log(`Closed ${closedCount} tab${closedCount !== 1 ? 's' : ''}; ${remainingCount} tab${remainingCount !== 1 ? 's' : ''} left open`)