Skip to content

Instantly share code, notes, and snippets.

@lancethomps
Last active April 6, 2025 15:55
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, Ventura, Sonoma, and Sequoia
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();
}
@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!

@viewfrom13
Copy link

Thanks @lancethomps - working perfectly in my keyboard maestro macro on Sequoia 15.2

@kennguyen107
Copy link

@lancethomps For some reason, your javascript code doesn't work for me on Macos Sequoia 15.2. The "result" tab in script editor keeps outputing "[object Object]" and if I run the script right after opening the notification center, then the script takes a bit longer to run and then an alert pops up saying "Error: Error: Invalid index."

@christianmagill
Copy link

@lancethomps Works great for me! Any chance of a version that just dismisses one notification at a time? In case you actually want to read through them.

@kennguyen107
Copy link

@lancethomps Works great for me! Any chance of a version that just dismisses one notification at a time? In case you actually want to read through them.

Could you tell me how you got this to work? It kept giving me errors.

@christianmagill
Copy link

@kennguyen107 I used Keyboard Maestro for triggering the Javascript for Automation script, but the missing piece for me was that the file defines a function but it doesn't run it. So you have to put a "run();" after the last line.

@kennguyen107
Copy link

@christianmagill For me, I've been trying to run it on Apple Script Editor, but to little success. What do you pass in as input and parameters to the run() function?

@christianmagill
Copy link

@kennguyen107 Looking through the source code, it didn't look like it was actually using the parameters, so I just left them blank. Make sure you've selected JavaScript in the script editor and not applescript.

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