-
Star
(131)
You must be signed in to star a gist -
Fork
(12)
You must be signed in to fork a gist
-
-
Save lancethomps/a5ac103f334b171f70ce2ff983220b4f to your computer and use it in GitHub Desktop.
| 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] >= 16 || (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); | |
| } | |
| }; | |
| logVerbose(`SYS_VERSION: ${SYS_VERSION}`); | |
| 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) { | |
| logVerbose('getNotificationCenterGroups: V10_OR_LESS'); | |
| return notificationCenter.windows(); | |
| } | |
| if (V12) { | |
| logVerbose('getNotificationCenterGroups: V12'); | |
| return notificationCenter.windows[0].uiElements[0].uiElements[0].uiElements(); | |
| } | |
| if (V15_2_OR_GREATER) { | |
| logVerbose('getNotificationCenterGroups: V15_2_OR_GREATER'); | |
| return findNotificationCenterAlerts([], notificationCenter.windows[0].uiElements[0].uiElements[0].uiElements()); | |
| } | |
| logVerbose('getNotificationCenterGroups: no version specific logic'); | |
| 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) => { | |
| logVerbose(`Finding alerts for ${elements.length} 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(true), 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(); | |
| } |
@benlind
Thanks for suggestion. Looks like Aliento uses similar logic APIs to this gist and BTT use - it closes notification one by one. Similarly, it doesn't collapse expanded groups.
Supercharge probably uses different APIs - it clears everything in one shot. If it's not a secret, we ask Sindre on how he implemented it and try to code it in AppleScript.
Hmm, trying to run this using script editor. I've selected JavaScript and added run(); to the end, but I get "Error -2700: Script error." and "Error: No notifications found...". I've tried it both with Notification Center open and closed. Any ideas? My goal is to use Raycast to run this.
@brycedriesenga I received some help from a Raycast Ambassador on this. Not sure if it's the exact same script, but it works for me.
https://gist.github.com/christianmagill/92f89e6823b5f8b08876f39947411466
@christianmagill Oh sweet, I'll check it out, thank you!
Another alternative I just discovered is Aliento, which is a pay-what-you-want app whose sole purpose is dismissing notifications with a keyboard shortcut.