Created
May 14, 2025 17:55
-
-
Save nimatrueway/534de1312e20e53e7935935e41bd25ef to your computer and use it in GitHub Desktop.
Periodically look for a specific notification and close it in macOS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env osascript -l JavaScript | |
// verify this logic using "Accessibility Inspector" of macOS if it's not working | |
TARGET_NOTIFICATION_TITLE_AND_BODY = ["<Set title here>", "<Set description here>"]; | |
// generic utility functions | |
// ------------------------------------------------- | |
// This function takes an element and returns a string array of all its immediate text children | |
function findAllTextChildren(element) { | |
const textChildren = []; | |
const children = element.uiElements(); | |
for (const child of children) { | |
if (child.roleDescription() == "text") { | |
textChildren.push(child.value()); | |
} | |
} | |
return textChildren; | |
} | |
// This function returns the first action of the specified element that has the specified action description | |
function findFirstActionChild(element, action_description) { | |
const actions = element.actions(); | |
for (const action of actions) { | |
if (action.description() == action_description) { | |
return action; | |
} | |
} | |
return null; | |
} | |
// notification specific functions | |
// ------------------------------------------------- | |
// find scrollarea of the notification-center process | |
function findNotificationScrollingArea(process) { | |
const windows = process.windows(); | |
if (windows.length == 0) { | |
return; | |
} | |
const rootWindow = windows[0]; | |
// traverse into the first group | |
const groups = rootWindow.groups(); | |
if (groups.length == 0) { | |
console.log("No groups found in the root window."); | |
return; | |
} | |
const firstGroup = groups[0]; | |
// traverse into the first group again | |
const firstGroupChildren = firstGroup.groups(); | |
if (firstGroupChildren.length == 0) { | |
console.log("No children found in the first group."); | |
return; | |
} | |
const firstFirstGroup = firstGroupChildren[0]; | |
// traverse into the scroll area | |
const scrollAreas = firstFirstGroup.scrollAreas(); | |
if (scrollAreas.length == 0) { | |
console.log("No scroll areas found in the first-first group."); | |
return; | |
} | |
const scrollArea = scrollAreas[0]; | |
return scrollArea; | |
} | |
// traverses through all active notification items found in the scrollArea, | |
// and closes those that its all texts (title, description) matches "target_texts" | |
function closeNotification(scrollArea, target_texts) { | |
const scrollAreaGroups = scrollArea.groups(); | |
for (const notification of scrollAreaGroups) { | |
if (notification.subrole() == "AXNotificationCenterAlert") { | |
// fetch all text children of the group | |
const texts = findAllTextChildren(notification); | |
if (texts.toString() == target_texts.toString()) { | |
console.log("Found a notification alert that matches."); | |
const action = findFirstActionChild(notification, "Close"); | |
if (action) { | |
action.perform(); | |
return true; | |
} else { | |
console.log("Target notification alert does not have a close action."); | |
} | |
} | |
} else if (notification.role() == "AXGroup") { | |
if (closeNotification(notification, target_texts) == true) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
// main! | |
// ------------------------------------------------- | |
function run() { | |
const systemEvents = Application("System Events"); | |
const process = systemEvents.applicationProcesses.byName("NotificationCenter"); | |
for (;;) { | |
try { | |
const scrollArea = findNotificationScrollingArea(process); | |
if (scrollArea) { | |
closeNotification(scrollArea, TARGET_NOTIFICATION_TITLE_AND_BODY); | |
} | |
} catch (e) { | |
console.log('err: ', e) | |
} | |
delay(1); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
test scenario