Skip to content

Instantly share code, notes, and snippets.

@lancethomps
Last active October 31, 2024 15:09
Show Gist options
  • Save lancethomps/a5ac103f334b171f70ce2ff983220b4f to your computer and use it in GitHub Desktop.
Save lancethomps/a5ac103f334b171f70ce2ff983220b4f to your computer and use it in GitHub Desktop.
AppleScript to close all notifications on macOS Big Sur, Monterey, and Ventura
function run(input, parameters) {
const appNames = [];
const skipAppNames = [];
const verbose = true;
const scriptName = "close_notifications_applescript";
const CLEAR_ALL_ACTION = "Clear All";
const CLEAR_ALL_ACTION_TOP = "Clear";
const CLOSE_ACTION = "Close";
const notNull = (val) => {
return val !== null && val !== undefined;
};
const isNull = (val) => {
return !notNull(val);
};
const notNullOrEmpty = (val) => {
return notNull(val) && val.length > 0;
};
const isNullOrEmpty = (val) => {
return !notNullOrEmpty(val);
};
const isError = (maybeErr) => {
return notNull(maybeErr) && (maybeErr instanceof Error || maybeErr.message);
};
const systemVersion = () => {
return Application("Finder").version().split(".").map(val => parseInt(val));
};
const systemVersionGreaterThanOrEqualTo = (vers) => {
return systemVersion()[0] >= vers;
};
const isBigSurOrGreater = () => {
return systemVersionGreaterThanOrEqualTo(11);
};
const V11_OR_GREATER = isBigSurOrGreater();
const V12 = systemVersion()[0] === 12;
const APP_NAME_MATCHER_ROLE = V11_OR_GREATER ? "AXStaticText" : "AXImage";
const hasAppNames = notNullOrEmpty(appNames);
const hasSkipAppNames = notNullOrEmpty(skipAppNames);
const hasAppNameFilters = hasAppNames || hasSkipAppNames;
const appNameForLog = hasAppNames ? ` [${appNames.join(",")}]` : "";
const logs = [];
const log = (message, ...optionalParams) => {
let message_with_prefix = `${new Date().toISOString().replace("Z", "").replace("T", " ")} [${scriptName}]${appNameForLog} ${message}`;
console.log(message_with_prefix, optionalParams);
logs.push(message_with_prefix);
};
const logError = (message, ...optionalParams) => {
if (isError(message)) {
let err = message;
message = `${err}${err.stack ? (" " + err.stack) : ""}`;
}
log(`ERROR ${message}`, optionalParams);
};
const logErrorVerbose = (message, ...optionalParams) => {
if (verbose) {
logError(message, optionalParams);
}
};
const logVerbose = (message) => {
if (verbose) {
log(message);
}
};
const getLogLines = () => {
return logs.join("\n");
};
const getSystemEvents = () => {
let systemEvents = Application("System Events");
systemEvents.includeStandardAdditions = true;
return systemEvents;
};
const getNotificationCenter = () => {
try {
return getSystemEvents().processes.byName("NotificationCenter");
} catch (err) {
logError("Could not get NotificationCenter");
throw err;
}
};
const getNotificationCenterGroups = (retryOnError = false) => {
try {
let notificationCenter = getNotificationCenter();
if (notificationCenter.windows.length <= 0) {
return [];
}
if (!V11_OR_GREATER) {
return notificationCenter.windows();
}
if (V12) {
return notificationCenter.windows[0].uiElements[0].uiElements[0].uiElements();
}
return notificationCenter.windows[0].uiElements[0].uiElements[0].uiElements[0].uiElements();
} catch (err) {
logError("Could not get NotificationCenter groups");
if (retryOnError) {
logError(err);
log("Retrying getNotificationCenterGroups...");
return getNotificationCenterGroups(false);
} else {
throw err;
}
}
};
const isClearButton = (description, name) => {
return description === "button" && name === CLEAR_ALL_ACTION_TOP;
};
const matchesAnyAppNames = (value, checkValues) => {
if (isNullOrEmpty(checkValues)) {
return false;
}
let lowerAppName = value.toLowerCase();
for (let checkValue of checkValues) {
if (lowerAppName === checkValue.toLowerCase()) {
return true;
}
}
return false;
};
const matchesAppName = (role, value) => {
if (role !== APP_NAME_MATCHER_ROLE) {
return false;
}
if (hasAppNames) {
return matchesAnyAppNames(value, appNames);
}
return !matchesAnyAppNames(value, skipAppNames);
};
const notificationGroupMatches = (group) => {
try {
let description = group.description();
if (V11_OR_GREATER && isClearButton(description, group.name())) {
return true;
}
if (V11_OR_GREATER && description !== "group") {
return false;
}
if (!V11_OR_GREATER) {
let matchedAppName = !hasAppNameFilters;
if (!matchedAppName) {
for (let elem of group.uiElements()) {
if (matchesAppName(elem.role(), elem.description())) {
matchedAppName = true;
break;
}
}
}
if (matchedAppName) {
return notNull(findCloseActionV10(group, -1));
}
return false;
}
if (!hasAppNameFilters) {
return true;
}
let checkElem = group.uiElements[0];
if (checkElem.value().toLowerCase() === "time sensitive") {
checkElem = group.uiElements[1];
}
return matchesAppName(checkElem.role(), checkElem.value());
} catch (err) {
logErrorVerbose(`Caught error while checking window, window is probably closed: ${err}`);
logErrorVerbose(err);
}
return false;
};
const findCloseActionV10 = (group, closedCount) => {
try {
for (let elem of group.uiElements()) {
if (elem.role() === "AXButton" && elem.title() === CLOSE_ACTION) {
return elem.actions["AXPress"];
}
}
} catch (err) {
logErrorVerbose(`(group_${closedCount}) Caught error while searching for close action, window is probably closed: ${err}`);
logErrorVerbose(err);
return null;
}
log("No close action found for notification");
return null;
};
const findCloseAction = (group, closedCount) => {
try {
if (!V11_OR_GREATER) {
return findCloseActionV10(group, closedCount);
}
let checkForPress = isClearButton(group.description(), group.name());
let clearAllAction;
let closeAction;
for (let action of group.actions()) {
let description = action.description();
if (description === CLEAR_ALL_ACTION) {
clearAllAction = action;
break;
} else if (description === CLOSE_ACTION) {
closeAction = action;
} else if (checkForPress && description === "press") {
clearAllAction = action;
break;
}
}
if (notNull(clearAllAction)) {
return clearAllAction;
} else if (notNull(closeAction)) {
return closeAction;
}
} catch (err) {
logErrorVerbose(`(group_${closedCount}) Caught error while searching for close action, window is probably closed: ${err}`);
logErrorVerbose(err);
return null;
}
log("No close action found for notification");
return null;
};
const closeNextGroup = (groups, closedCount) => {
try {
for (let group of groups) {
if (notificationGroupMatches(group)) {
let closeAction = findCloseAction(group, closedCount);
if (notNull(closeAction)) {
try {
closeAction.perform();
return [true, 1];
} catch (err) {
logErrorVerbose(`(group_${closedCount}) Caught error while performing close action, window is probably closed: ${err}`);
logErrorVerbose(err);
}
}
return [true, 0];
}
}
return false;
} catch (err) {
logError("Could not run closeNextGroup");
throw err;
}
};
try {
let groupsCount = getNotificationCenterGroups(true).filter(group => notificationGroupMatches(group)).length;
if (groupsCount > 0) {
logVerbose(`Closing ${groupsCount}${appNameForLog} notification group${(groupsCount > 1 ? "s" : "")}`);
let startTime = new Date().getTime();
let closedCount = 0;
let maybeMore = true;
let maxAttempts = 2;
let attempts = 1;
while (maybeMore && ((new Date().getTime() - startTime) <= (1000 * 30))) {
try {
let closeResult = closeNextGroup(getNotificationCenterGroups(), closedCount);
maybeMore = closeResult[0];
if (maybeMore) {
closedCount = closedCount + closeResult[1];
}
} catch (innerErr) {
if (maybeMore && closedCount === 0 && attempts < maxAttempts) {
log(`Caught an error before anything closed, trying ${maxAttempts - attempts} more time(s).`)
attempts++;
} else {
throw innerErr;
}
}
}
} else {
throw Error(`No${appNameForLog} notifications found...`);
}
} catch (err) {
logError(err);
logError(err.message);
getLogLines();
throw err;
}
return getLogLines();
}
@nicksergeant
Copy link

I also wonder if a better approach is attempting to click this bottom Clear All button instead of the groups of notifications. I tried figuring out what that button was labeled w/ Accessibility Inspector but no luck:

image

@evannjohnson
Copy link

I think it is unlikely that is a better approach, as since this UI scripting, to click that button programmatically requires the full Notification Center to be open, showing the button that we want to click. Meaning, you'll need to have either opened the full notification center, or have the script open it just so it can click the button (unless there is something I'm missing).

Clicking the dismiss buttons of just the notifications that are visible on screen is likely what most people will want.

@sealvessel
Copy link

Can you please make this a real repository as everyone keeps getting unnecessary notifications. We only need the bug fix updates to be tracked.

@sealvessel
Copy link

Also, how may we code sign this applescript to avoid tampering?

@nicksergeant
Copy link

@evannjohnson ahh, that explains why this isn't working for me 😅. The original gist here works when the Notification Center is open. It may also work when it's closed - I'm not sure - but I only ever use this after opening the Notification Center, seeing the notifications, and then clearing all of them.

@bpetrynski
Copy link

@joshyboyrules
Copy link

@bpetrynski thanks! worked for me.

@nicksergeant
Copy link

@bpetrynski awesome! That's definitely in the right direction. It only will clear one notification grouping at a time but I can probably add in some looping here. Thanks!

@Ptujec
Copy link

Ptujec commented Oct 15, 2024

I updated my version for macOS 15. If you have a group of notifications expanded it will collapse the group first which may speed things up a little in some cases.

@MrJarnould
Copy link

@Ptujec Which version of macOS 15 are you using? I tried your script and couldn't get it to work.

@Ptujec
Copy link

Ptujec commented Oct 16, 2024

@Ptujec Which version of macOS 15 are you using? I tried your script and couldn't get it to work.

15.0.1
I just updated the script so it should now should give you an error message.

@Ptujec
Copy link

Ptujec commented Oct 28, 2024

@Ptujec Which version of macOS 15 are you using? I tried your script and couldn't get it to work.

@MrJarnould I just updated to 15.1 and adjusted the script(s). Apple changed things again from 15.0.1 to 15.1.

@geigel
Copy link

geigel commented Oct 31, 2024

Has anyone been able to get this to work within BetterTouchTool (BTT) and if so, what did you do? I am having trouble getting it to fire and unsure if I'm tripping up on something due to BTT or something else... thanks in advance!

FYI: BetterTouchTool offers this feature built-in: Close All Notification Alerts, for those who would prefer a paid solution: https://community.folivora.ai/t/clear-notifications-with-a-keyboard-shortcut/30335

(I already use BetterTouchTool for dozens of other use cases, so I already had it.)

(and if you're wondering how I came across this thread: I didn't realize BTT already offered this feature until today...)

The BTT action was working for me until I updated to Sequoia (Version 15.1). Has anyone found a solution that works well in BTT?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment