Skip to content

Instantly share code, notes, and snippets.

@nimatrueway
Created May 14, 2025 17:55
Show Gist options
  • Save nimatrueway/534de1312e20e53e7935935e41bd25ef to your computer and use it in GitHub Desktop.
Save nimatrueway/534de1312e20e53e7935935e41bd25ef to your computer and use it in GitHub Desktop.
Periodically look for a specific notification and close it in macOS
#!/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);
}
}
@nimatrueway
Copy link
Author

nimatrueway commented May 14, 2025

test scenario

// produce a notification by sending this to hammerspoon console
hs.notify.new({title = "test", subTitle = "something", withdrawAfter = 10000, }):send()

// change this variable in the script, run it, it'll close the notification
TARGET_NOTIFICATION_TITLE_AND_BODY = ["test", "something"]

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