Skip to content

Instantly share code, notes, and snippets.

@lancethomps
Last active December 13, 2024 19:20
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 SYS_VERSION = systemVersion();
const V11_OR_GREATER = isBigSurOrGreater();
const V10_OR_LESS = !V11_OR_GREATER;
const V12 = SYS_VERSION[0] === 12;
const V15_OR_GREATER = SYS_VERSION[0] >= 15;
const V15_2_OR_GREATER = SYS_VERSION[0] >= 15 && SYS_VERSION[1] >= 2;
const APP_NAME_MATCHER_ROLE = V11_OR_GREATER ? 'AXStaticText' : 'AXImage';
const NOTIFICATION_SUB_ROLES = ['AXNotificationCenterAlert', 'AXNotificationCenterAlertStack'];
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 (V10_OR_LESS) {
return notificationCenter.windows();
}
if (V12) {
return notificationCenter.windows[0].uiElements[0].uiElements[0].uiElements();
}
if (V15_2_OR_GREATER) {
return findNotificationCenterAlerts([], 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 findNotificationCenterAlerts = (alerts, elements) => {
for (let elem of elements) {
let subrole = elem.subrole();
if (NOTIFICATION_SUB_ROLES.indexOf(subrole) > -1) {
alerts.push(elem);
} else if (elem.uiElements.length > 0) {
findNotificationCenterAlerts(alerts, elem.uiElements());
}
}
return alerts;
};
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 = (value) => {
if (hasAppNames) {
return matchesAnyAppNames(value, appNames);
}
return !matchesAnyAppNames(value, skipAppNames);
};
const getAppName = (group) => {
if (V15_OR_GREATER) {
for (let action of group.actions()) {
if (action.description() === 'Remind Me Tomorrow') {
return 'reminders';
}
}
return '';
}
if (V10_OR_LESS) {
if (group.role() !== APP_NAME_MATCHER_ROLE) {
return '';
}
return group.description();
}
let checkElem = group.uiElements[0];
if (checkElem.value().toLowerCase() === 'time sensitive') {
checkElem = group.uiElements[1];
}
if (checkElem.role() !== APP_NAME_MATCHER_ROLE) {
return '';
}
return checkElem.value();
};
const notificationGroupMatches = (group) => {
try {
let description = group.description();
if (V11_OR_GREATER && isClearButton(description, group.name())) {
return true;
}
if (V15_OR_GREATER) {
let subrole = group.subrole();
if (NOTIFICATION_SUB_ROLES.indexOf(subrole) === -1) {
return false;
}
} else if (V11_OR_GREATER && description !== 'group') {
return false;
}
if (V10_OR_LESS) {
let matchedAppName = !hasAppNameFilters;
if (!matchedAppName) {
for (let elem of group.uiElements()) {
if (matchesAppName(getAppName(elem))) {
matchedAppName = true;
break;
}
}
}
if (matchedAppName) {
return notNull(findCloseActionV10(group, -1));
}
return false;
}
if (!hasAppNameFilters) {
return true;
}
return matchesAppName(getAppName(group));
} 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 (V10_OR_LESS) {
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

@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?

@Ptujec
Copy link

Ptujec commented Dec 12, 2024

Happy "This is broken again with macOS 15.2" Day! I think I fixed it though.

@lancethomps
Copy link
Author

sorry all - I know I haven't been active in responding to issues / helping people out on this, but I didn't really plan to find a way to make this support multiple macOS versions and languages. perhaps I should be a better citizen here and maybe even make this a repo instead of a gist.

I just upgraded to macOS 15.1.1 and fixed any issues (at least for my laptop using BTT via the custom application). let me know if you are still having issues. @Ptujec I will see if I can upgrade to 15.2 and check for any issues, but this is a company laptop so I might not be able to.

@lancethomps
Copy link
Author

Happy "This is broken again with macOS 15.2" Day! I think I fixed it though.

I just pushed a fix for 15.2

@Ptujec
Copy link

Ptujec commented Dec 12, 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?

I wonder what @fifafu (dev of BTT) is using for the feature. But I guess the reasons why things break after updates are similar.

@Ptujec
Copy link

Ptujec commented Dec 12, 2024

Happy "This is broken again with macOS 15.2" Day! I think I fixed it though.

I just pushed a fix for 15.2

Great! I am not complaining, by the way. I'm just trying to be helpful if someone is interested in my version. I just tried yours. It takes a while, but it works. Props for trying to make it work for all the different OS versions in the same script.

@lancethomps
Copy link
Author

I definitely appreciate the helpfulness @Ptujec!

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