Is it possible to listen/observe for new notifications macOS receives?
I mean like when a new iMessage or a Slack message is received (so basically everything that causes NotificationCenter to display a Notification)
Is it possible to listen/observe for new notifications macOS receives?
I mean like when a new iMessage or a Slack message is received (so basically everything that causes NotificationCenter to display a Notification)
Short answer: It is not possible.
You can't observe user notifications sent by applications unless an application provides a specific API. For example the AppleScript dictionary of iMessage and Mail contains events scripts can respond to. However user notifications are encapsulated in the target application.
There is a global notification class named DistributedNotificationCenter, a notification dispatch mechanism that enables the broadcast of notifications across task boundaries. Some processes are sending distributed notifications but it's a completely different functionality as UserNotification
. For example the TimeMachine engine process backupd
sends distributed notifications while running a backup.
You can subscribe for all distributed notifications with
DistributedNotificationCenter.default().addObserver(self, selector: #selector(handleNotifications(_:)), name: nil, object: nil)
but I doubt that iMessage sends a distributed notification when a message is received.
NSDistributedNotificationCenter
no longer supports nil
names. I was unable to get it working as a privileged operation either, but you can discover notification names by running strings
against system application binaries. –
Java After searching extensively online I was able to find a workaround using AXUIElement
. Here's a working example with Hammerspoon (which has an excellent module available for it), but it is possible to get the same thing working with Swift or Objective-C, although a bit cumbersome since it's a low-level C API. It may be possible to use AppleScript or JXA as well.
local log = hs.logger.new("notificationcenter")
local notificationCenterBundleID = "com.apple.notificationcenterui"
local notificationCenter = hs.axuielement.applicationElement(notificationCenterBundleID)
assert(notificationCenter, "Unable to find Notification Center AX element")
local processedNotificationIDs = {}
local notificationSubroles = {
AXNotificationCenterAlert = true,
AXNotificationCenterBanner = true,
}
notificationObserver = hs.axuielement.observer
.new(notificationCenter:pid())
:callback(function(_, element)
-- Ignore events when system drawer is open to avoid callbacks for
-- previous notifications.
if notificationCenter:asHSApplication():focusedWindow() then return end
-- Process each notification only once.
if
not notificationSubroles[element.AXSubrole]
or processedNotificationIDs[element.AXIdentifier]
then
return
end
-- Only match Messages and Slack notifications.
local stackingID = element.AXStackingIdentifier
if
stackingID ~= "com.tinyspeck.slackmacgap"
and stackingID:find("com.apple.MobileSMSiMessage;", 1, true) ~= 1
and stackingID:find("com.apple.MobileSMSSMS;", 1, true) ~= 1
then
log.df("Skipping notification with stacking ID %s", stackingID)
return
end
processedNotificationIDs[element.AXIdentifier] = true
local staticTexts = hs.fnutils.imap(
hs.fnutils.ifilter(element, function(value)
return value.AXRole == "AXStaticText"
end),
function(value)
return value.AXValue
end
)
local title = nil
local subtitle = nil
local message = nil
if #staticTexts == 2 then
title, message = table.unpack(staticTexts)
elseif #staticTexts == 3 then
title, subtitle, message = table.unpack(staticTexts)
else
error(string.format("Unexpected static text count: %d", #staticTexts))
end
log.f(
"Got notification: title = %s, subtitle = %s, message = %s",
title,
subtitle,
message
)
end)
:addWatcher(
notificationCenter,
-- Equivalent to kAXLayoutChangedNotification ("AXLayoutChanged")
-- https://github.com/Hammerspoon/hammerspoon/blob/8ea7d105ab27c917703a6c30e5980b82a23c6a0c/extensions/axuielement/observer.m#L402
hs.axuielement.observer.notifications["layoutChanged"]
)
:start()
The notification observed is AXLayoutChanged
, which is less than ideal since it's invoked whenever the notification is moused over or the drawer moves, and may include events for other elements. I tried several others, the closest fit was AXCreated
but that doesn't seem to get reliably invoked for new notifications. As a workaround the above code only looks at notifications received when the Notification Center drawer is closed, and uses the AXIdentifier
attribute to process each notification only once.
This is the only working method I could find. I tried using NSDistributedNotificationCenter
mentioned in @vadian's answer with names generated from strings
run against system binaries such as /System/Applications/Messages.app
, /System/Library/CoreServices/NotificationCenter.app
and /System/Library/CoreServices/UserNotificationCenter.app
, but was unable to find any notifications used for Messages or Notification Center. It's possible this was due to not finding the right value, since NSDistributedNotificationCenter
no longer supports nil
names. Here's the Swift script I used to check:
import Foundation
func readNames() throws -> [String] {
let fileURL = URL(fileURLWithPath: "/path/to/notification-names.txt")
return try String(contentsOf: fileURL, encoding: .utf8).components(separatedBy: "\n")
}
let nc = DistributedNotificationCenter.default()
let names = try readNames()
print(names)
for name in names {
nc.addObserver(forName: NSNotification.Name(name), object: nil, queue: nil) { notification in
print(notification)
}
}
print("Running")
RunLoop.main.run()
notification-names.txt
can be generated by e.g. strings /System/Library/CoreServices/NotificationCenter.app/Contents/MacOS/NotificationCenter | grep -F com.apple
.
There is also a database file which can be queried and watched for changes (see previous threads here and here), but in the limited testing I did it was not written to in real-time, unlike the above method with AXUIElement
.
@user12638282's provided an excellent solution.
Unfortunately, it seems to be unreliable, in my testing (though I would be happy to be proved wrong). Sometimes the callback is called for new Notifications reliable and consistently, yet sometimes it stops working, without explanation.
He also mentioned that the hammerspoon event for "created" was less reliable. However, in my experience, this event is MORE reliable. However, I have yet to figure out how to get the Notification title and message from this callback.
Hopefully this helps someone along on their search. These methods both warrant further investigation.
© 2022 - 2024 — McMap. All rights reserved.