Parsing Notifications in macOS Sequoia

I have several scripts that rely upon parsing Notification Center alerts/banners (like the kind generated by AppleScript’s display notification… not the Cocoa NSNotificationCenter):

  • A script that grabs an OTP code from the current banner notification (either Mail.app or Messages.app)
  • A script that re-authenticates my corporate Exchange email account (which requires re-authentication in System Settings every 9 days :roll_eyes:)

I’ve been using these scripts for the past ~10 years. The parsing is done with GUI scripting (through the macOS Accessibility framework), and typically needs minor updates with each major macOS release.

Unfortunately, I’ve hit a dead-end with macOS Sequoia. It doesn’t appear like the notification text (“Testing 123” in the example above) is exposed anywhere in AppleScript. I’m even trying to write a CLI utility to access the Accessibility framework through Swift (which unfortunately uses a C API, and is more painful that I’d like).

Note that the notifications now seem to be generated by the System with SwiftUI, and the AppleScript hierarchy now depends on whether the mouse is hovering over the notification (but regardless of the hover state, the text is not accessible in AppleScript).


I can see the element in Accessibility Inspector:

And UI Browser suggests the text can be pulled from the attribute “AXAttributedString” (note that the type is “unknown”):




However, this attribute’s value does not appear to be accessible from AppleScript (perhaps because of the “unknown” type above?):

tell application "System Events"
	tell process "NotificationCenter"
		if exists window 1 then
			return attributes of button 1 of scroll area 1 of group 1 of group 1 of window 1
		end if
	end tell
end tell

And you can’t even ask for the attribute "AXAttributedDescription" directly:
Screenshot 2024-11-01 at 5.00.18 PM


My questions, are therefore:

  1. Does anyone know of any other ways to access the contents of notifications? UNUserNotificationCenter appears to be restricted to notifications generated by the current application.

  2. Is there is any was to use the Accessiblity framework with ASObjC?

My testing suggests the AppleScript interface deficiencies is a bug, a not a deliberate removal by Apple in macOS Sequoia. Any other thoughts?

@tree_frog

So, I am still on Sonoma and not Sequoia, but I have spent more time than I am proud to admit dealing with GUI scripting the Notification Center. My initial goal was to create a way to clear push notifications and also clear out the Notification Center. What I have works, but I cannot guarantee it will work for Sequoia. Hopefully things haven’t changed too much between these two versions. I plan to upgrade soon, and I can try to be of more help if this doesn’t work. Anyway, I made some modifications and removed some logic for the sake of brevity. I hope this works for you:

delay 0.1
-- Test Notification
display notification "Test 123"
delay 0.1
tell application "System Events"	
    return the value of static text 2 of group 1 of UI element 1 of scroll area 1 of group 1 of window "Notification Center" of application process "NotificationCenter"
end tell

Obviously, I am not checking to see if a notification exists nor am I handling the errors gracefully, but those are simple additions if you so desire.

If you have more than one notification push to you in a short time, you may need to change the numerical values associated with the groups, UI elements, etc…

Thanks Moose!

Yes, unfortunately that no longer works in Sequoia. There aren’t any accessible static texts anywhere in the hierarchy. I’ve manually inspected the entire thing in Script Debugger.

This is the hierarchy according to UI browser:

I have made some progress on a Swift-based command line utility that accesses the C Accessibility APIs. It does look as if the AppleScript interface is broken in Sequoia.

With the C Accessibility APIs, I can access a property that returns the title, optional subtitle, and message as a single string separated with ", ", but I don’t see any way to fully disambiguate the fields (i.e. if the message has commas in it).

If you’re reliant on the GUI scripting, my advice would be to delay the update to Sequoia until we’ve made some more progress! I wish I had. :neutral_face:

Hello,

here is a script-bundle that uses the PFAssistive Framework to create an observer for the Notification Center. So you can at least use the string within AppleScript. Maybe you can do something with it.

However, the problem remains that Apple (possibly deliberately) uses an NSConcreteAttributedString (private subclass of the NSAttributedString class) at this point. Therefore, you cannot query the value directly with pure AppleScript. With ASObj-C you could possibly retrieve the individual values for Title, Subtitle and Message if you knew the corresponding attributes or handler of the private class. This leaves only the query of the String property from the NSAttributedString class, which outputs a comma-separated summary.

I do not believe that this is a specific AppleScript error. Private types or classes are never supported in interfaces and in this specific case the information should perhaps not be publicly accessible.

Note:
The framework must be enabled for execution in the system settings - data protection and security after the first execution.
The observer only runs as long as the script (or the calling process) is running (or you compile it into a Stay Open Application).

HookNotificationCenter.scptd.zip (249.3 KB)

Edit: Script Bundle to get only the Message-String:
NotificationCenter.scptd.zip (247.1 KB)

2 Likes

Thanks for the explanation, Dirk. Sounds like helper utilities may be needed in the future.

Tree_frog, I’ve also spent way too much time scripting notification management, and just got it working again under Sequoia (except for text retrieval). The resultant scripts might save you some time and hair-pulling if you’re holding off on upgrading from Sonoma:

Close most recent notification group
tell application "System Events"
tell process "NotificationCenter"
	
	set SA to scroll area 1 of group 1 of group 1 of window "Notification Center"
	set collapsedNotificationGroup to false
	set expandedNotificationGroup to false
	
	
	-- Check if the top element is a collapsed notification group
	if (count (actions of button 1 of SA whose name starts with "Name:Clear All")) is 1 then
		set collapsedNotificationGroup to true
		log ("Top element is collapsed group")
		
		-- Check if the top element is an expanded notification group
	else if description of UI element 1 of SA is "heading" then
		set expandedNotificationGroup to true
		log ("Top element is expanded group")
	end if
	
	
	-- If the top element is a collapsed notification group, expand it and click the top notification
	if collapsedNotificationGroup then
		-- Close group
		perform first action of (actions of button 1 of SA whose name starts with "Name:Clear All")
		log ("Closed collapsed notification group (Clear All button)")
		
	else if expandedNotificationGroup then
		-- If the top element is part of an expanded notification group, click the Clear All button
		click button 2 of SA
		log ("Clicked Clear All (button 2)")
	else
		-- Do nothing if the top element is a single notification			
	end if
	
end tell
end tell
Expand group then open most recent notification
tell application "System Events"
tell process "NotificationCenter"
	
	set SA to scroll area 1 of group 1 of group 1 of window "Notification Center"
	set collapsedNotificationGroup to false
	set expandedNotificationGroup to false
	
	
	-- Check if the top element is a collapsed notification group
	if (count (actions of button 1 of SA whose name starts with "Name:Clear All")) is 1 then
		set collapsedNotificationGroup to true
		log ("Top element is collapsed group")
		
		-- Check if the top element is an expanded notification group
	else if description of UI element 1 of SA is "heading" then
		set expandedNotificationGroup to true
		log ("Top element is expanded group")
	end if
	
	
	-- If the top element is a collapsed notification group, expand it and click the top notification
	if collapsedNotificationGroup then
		-- Expand group
		click button 1 of SA
		log ("Expanded notification group (button 1)")
		-- For some reason this is necessary, otherwise the next call to notificationElements doesn't get a refreshed view
		delay 0.5
		-- Click the first notification
		click button 3 of SA
		log ("Clicked top notification of group (button 3)")
		
	else if expandedNotificationGroup then
		-- If the top element is part of an expanded notification group, click the top notification
		click button 3 of SA
		log ("Clicked top notification of group (button 3)")
		
	else
		-- If the top element is a non-grouped notification, click it
		click button 1 of SA
		log ("Clicked top single notification (button 1)")
	end if
	
end tell
end tell
Expand then close the most recent notification
tell application "System Events"
tell process "NotificationCenter"
	
	set SA to scroll area 1 of group 1 of group 1 of window "Notification Center"
	set collapsedNotificationGroup to false
	set expandedNotificationGroup to false
	
	
	-- Check if the top element is a collapsed notification group
	if (count (actions of button 1 of SA whose name starts with "Name:Clear All")) is 1 then
		set collapsedNotificationGroup to true
		log ("Top element is collapsed group")
		
		-- Check if the top element is an expanded notification group
	else if description of UI element 1 of SA is "heading" then
		set expandedNotificationGroup to true
		log ("Top element is expanded group")
	end if
	
	
	-- If the top element is a collapsed notification group, expand it and close the top notification
	if collapsedNotificationGroup then
		-- Expand group
		click button 1 of SA
		log ("Expanded notification group (button 1)")
		-- For some reason this is necessary, otherwise the next call to notificationElements doesn't get a refreshed view
		delay 0.5
		-- Close the first notification
		perform first action of (actions of button 3 of SA whose name starts with "Name:Close")
		log ("Closed top notification of group (button 3)")
		
	else if expandedNotificationGroup then
		-- If the top element is part of an expanded notification group, close the top notification
		perform first action of (actions of button 3 of SA whose name starts with "Name:Close")
		log ("Closed top notification of group (button 3)")
		
	else
		-- If the top element is a non-grouped notification, close it
		perform first action of (actions of button 1 of SA whose name starts with "Name:Close")
		log ("Closed top single notification (button 1)")
	end if
	
end tell
end tell
2 Likes

Thanks Dirk, that’s very helpful! Will have a look through this. :smiley:

Appreciate the detail on the NSConcreteAttributedString.

Any idea how I would go about finding out the name of the private class? I’m willing to put in the work.

Yes, sadly, I think so.

Appreciate it!

ChatGPT says:

To inspect the members (i.e., properties and methods) of an NSConcreteAttributedString, which is a private subclass of NSAttributedString, you can use a few techniques. Although NSConcreteAttributedString itself is not directly exposed in public APIs, you can still inspect the methods and properties available through its superclass, NSAttributedString, using the Objective-C runtime or debugging tools. Here are ways to approach this:

#import <objc/runtime.h>

void printMethodsOfClass(Class cls) {
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList(cls, &methodCount);
    for (unsigned int i = 0; i < methodCount; i++) {
        Method method = methodList[i];
        SEL selector = method_getName(method);
        NSLog(@"Method: %@", NSStringFromSelector(selector));
    }
    free(methodList);
}

// Call this function for NSAttributedString
printMethodsOfClass([NSAttributedString class]);
void printPropertiesOfClass(Class cls) {
    unsigned int propertyCount = 0;
    objc_property_t *propertyList = class_copyPropertyList(cls, &propertyCount);
    for (unsigned int i = 0; i < propertyCount; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"Property: %s", propertyName);
    }
    free(propertyList);
}

printPropertiesOfClass([NSAttributedString class]);

or use LLDB in Xcode:

po [attributedString description]
po [attributedString methodDescription]

Important Note

NSConcreteAttributedString is a private subclass, and it is not recommended to rely on its internals for production code, as it can change without notice between different versions of macOS or iOS. Always try to use the public API provided by NSAttributedString and NSMutableAttributedString for stable, forward-compatible code.

1 Like

With AppleScript you have no chance (unsigned pointer etc.).
Maybe you could use the PFAssistive framework in Xcode to get the NSConcreteAttributedString class (like in the second script bundle example) and then look at the members in the Xcode-Debugger.

PS: Without the PFAssistive Framework you have no chance anyway, because the Apple Accessibility Framework uses CFTypes and therefore ASObjC cannot be used. JSObjC may work.

Assistive Application Programming Guide 2.5.pdf (286.5 KB)
PFAssistive Framework Reference.zip (162.3 KB)

1 Like

Here’s the promised Swift-based command line tool. It only works in macOS Sequoia (and is likely to require updating with each major macOS release, as with GUI AppleScripts). The application running the binary requires accessibility permissions.

I’d welcome any style advice for handling the C-to-Swift interface.

It’s invoked from the shell as:

NotificationParser <performActionName> <performActionFilter>

If invoked without any arguments, it will print the notifications in the following format:

index, bundle_id, title, subtitle, message

(If subtitle does not exist, it will be omitted along with its preceding , .)

For my example notification in the first post, this would output:

1, com.apple.ScriptEditor2, Script Editor, Testing 123

Note that as with the above solution by @Dirk, it’s not possible to fully disambiguate the title, subtitle, and message, as they’re pulled from one comma-separated field. This executable additionally converts any linefeeds in that field (which should be rare) to vertical tabs to prevent output from spilling into multiple lines.

The tool can additionally be called with an action (same actions as vanilla GUI AppleScript, but also accepts more natural command names), which will be performed on all notifications…

NotificationParser close

…or only on notifications that match a specified string:

NotificationParser close "Script Editor, Testing 123"

Swift Source Code
// Version 0.5

import AppKit
import ApplicationServices

// Look for an action name & action filter supplied on the command line.
let arguments = CommandLine.arguments.dropFirst()
var actionName = arguments.first?.lowercased()
switch actionName { // Handle defective action specifiers. The first string specified is Apple-defined.
	case "axshowmenu", "showmenu", "menu":
		actionName = "AXShowMenu"
	case "axpress", "press":
		actionName = "AXPress"
	case "name:show details\ntarget:0x0\nselector:(null)", "show details", "showdetails", "details":
		actionName = "Name:Show Details\nTarget:0x0\nSelector:(null)"
	case "name:show\ntarget:0x0\nselector:(null)", "show":
		actionName = "Name:Show\nTarget:0x0\nSelector:(null)"
	case "name:close\ntarget:0x0\nselector:(null)", "close":
		actionName = "Name:Close\nTarget:0x0\nSelector:(null)"
	case "":
		actionName = nil
	case nil:
		break
	case "-h", "--help":
		exit(0, withErrorMessage:"usage: NotificationParser <performActionName> <performActionFilter>\n\nReturn Format:\nIndex (1-Based), Bundle ID, Title[, Subtitle], Message")
	default:
		exit(4, withErrorMessage:"Unrecognized action specified")
}
var actionFilter = arguments.dropFirst().first
if actionFilter == "" { actionFilter = nil }

// Get the accessibility object for the Notification Center.
let notificationCenter = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.notificationcenterui")
guard let pid = notificationCenter.first?.processIdentifier else { exit(2, withErrorMessage:"Unable to get process ID of Notification Center") }
let appRef = AXUIElementCreateApplication(pid)
let app = UIElement(appRef)
guard let app else { exit(5, withErrorMessage:"Unable to access the Accessibility hierarchy (check the permission for the parent application in System Settings > Privacy & Security > Accessibility)") }

// Get the main Notification Center window.
let windowRefs = getAttributeValue(element: appRef, attributeName: "AXWindows") as? [AXUIElement] // kAXWindowsAttribute
if windowRefs?.count == 0 {
	exit(0)
}
guard windowRefs?.count == 1 else { exit(3, withErrorMessage:"Unable to identify main Notification Center window") }
let windowRef = windowRefs![0]
let window = UIElement(windowRef)!

// Get the scroll area.
let scrollArea = window.child(1)?.child(1)?.child(1)
guard let scrollArea else { exit(3, withErrorMessage:"Unable to get Notification Center scroll area") }

// Get the button objects, containing the notifications' descriptions & actions.
var notifications = scrollArea.children
guard notifications.count > 1 else { exit(3, withErrorMessage:"Unable to get notifications (button elements)") }
notifications.removeLast() // Menu Button

// Print the descriptions.
for (i, var notification) in notifications.enumerated() {
	guard let notificationText = notification.accessibilityDescription else { exit(3, withErrorMessage:"Unable to get AXDescription attribute of notification (button element)") }
	// Replace any linefeeds in the title/subtitle/message with a vertical tab.
	print("\(i + 1), \(notification.bundleID ?? "unknown"), \(notificationText.replacingOccurrences(of: "\n", with:"\u{0B}"))")
}

// Perform the action specified on the command line.
let filteredNotifications = notifications.filter { actionFilter == nil || $0.accessibilityDescription == actionFilter }
for notification in filteredNotifications.reversed() {
	notification.performAction(actionName)
}

// Exit.
exit(0)



// WRAPPER TYPE.
struct UIElement : CustomStringConvertible {
	let reference: AXUIElement
	let role: String
	let title: String?
	let accessibilityDescription: String?

	lazy var bundleID: String? = { // Use only for notification (button element)
		let bid = stringValueForAttribute("AXStackingIdentifier")
		guard let bid else { return nil }
		return String(bid[bid.index(bid.startIndex, offsetBy: 17)...])
	}()
	
	init?(_ reference: CFTypeRef?) {
		let element = reference as! AXUIElement
		self.reference = element
		let role = getAttributeValue(element: element, attributeName: "AXRole") as? String // kAXRoleAttribute
		guard let role else { return nil } // AXRole is required for all Accessibility objects
		self.role = role
		self.title = getAttributeValue(element: element, attributeName: "AXTitle") as? String // kAXTitleAttribute
		self.accessibilityDescription = getAttributeValue(element: element, attributeName: "AXDescription") as? String // kAXDescriptionAttribute
	}
	
	var description: String { // For CustomStringConvertible, not AXDescription
		let name = title == nil ? "(Unnamed)" : "\"\(title!)\""
		return "\(role) \(name)"
	}
	
	func child(_ index: Int) -> UIElement? { // 1-based, negative indexing supported.
		let childrenRefs = getAttributeValue(element: reference, attributeName: "AXChildren") as? [AXUIElement] // kAXChildrenAttribute
		guard let childrenRefs else { return nil }
		guard index != 0 else { fatalError("UIElement.child is 1-based; not valid for an index of 0") }
		
		let i = index < 0 ? childrenRefs.count + index + 1 : index - 1 // Convert to 0-based, positive indexing.
		guard i < childrenRefs.count && i > -1 else { return nil }
		return UIElement(childrenRefs[i])
	}
	
	var children: [UIElement] {
		let childrenRefs = getAttributeValue(element: reference, attributeName: "AXChildren") as? [AXUIElement] // kAXChildrenAttribute
		guard let childrenRefs else { return [] }
		
		var children: [UIElement] = []
		for childRef in childrenRefs {
			children.append(UIElement(childRef)!)
		}
		return children
	}
	
	func performAction(_ actionName: String?) {
		if let actionName { AXUIElementPerformAction(reference, actionName as NSString) }
	}
	
	var attributeNames: [String] {
		var names: CFArray?
		let err = AXUIElementCopyAttributeNames(reference, &names)
		guard err == .success else { return []	}
		return names as! [String]
	}
	
	func stringValueForAttribute(_ attributeName: String) -> String? {
		return getAttributeValue(element: reference, attributeName: attributeName) as? String
	}
}

// HELPER FUNCTIONS.
// Exit with error message.
func exit(_ code: CInt, withErrorMessage message: String) -> Never {
	fputs(message, stderr)
	exit(code)
}

// Get specified attribute.
func getAttributeValue(element: AXUIElement, attributeName: String) -> CFTypeRef? {
	var value: CFTypeRef?
	let err = AXUIElementCopyAttributeValue(element, attributeName as NSString, &value)
	guard err == .success else { return nil	}
	return value
}

Compile with swiftc -o NotificationParser NotificationParser.swift

(Requires Xcode or the Xcode Command Line Tools.)

Execution takes about 25 ms (iMac Pro) or 13 ms (M1 Pro).

Pre-Compiled Binary (Universal, Unsigned) (46.0 KB)


Use from AppleScript:

-- Version 0.5.1

to getUserNotificationInfo()
	set notifications_s to do shell script quoted form of NOTIFICATION_PARSER_PATH without altering line endings
	set previous_TIDs to AppleScript's text item delimiters
	set AppleScript's text item delimiters to linefeed
	set notifications_L to text items of notifications_s -- Includes trailing empty string.
	set notifications to {}
	set AppleScript's text item delimiters to ", "
	repeat with i from 1 to ((length of notifications_L) - 1)
		set notification_s to item i of notifications_L
		set notification_s_components to text items of notification_s
		set notification_index to item 1 of notification_s_components as integer
		set application_bid to item 2 of notification_s_components
		try
			set application_name to name of application id application_bid as string
		on error
			set application_name to "Unknown"
		end try
		set notification_title to item 3 of notification_s_components
		set notification_subtitle to "" -- Unable to disambiguate from message.
		set notification_message to items 4 thru -1 of notification_s_components as string
		set AppleScript's text item delimiters to character id 11 -- Vertical tab (from substitution in binary)
		set notification_message to text items
		set AppleScript's text item delimiters to linefeed
		set notification_message to notification_message as string
		
		set the end of notifications to {notification_index:notification_index, application_bid:application_bid, application_name:application_name, notification_title:notification_title, notification_subtitle:notification_subtitle, notification_message:notification_message}
	end repeat
	set AppleScript's text item delimiters to AppleScript's text item delimiters as string
	
	return notifications
	
end getUserNotificationInfo

Which, in the above example, returns:

{{notification_index:1, application_bid:"com.apple.ScriptEditor2", application_name:"Script Editor", notification_title:"Script Editor", notification_subtitle:"", notification_message:"Testing 123"}}

Two notes:

  1. Since it’s not possible to disambiguate the subtitle without further work, this handler always returns "" as the subtitle. The subtitle, if present, will be prepended to the message field.
  2. This binary handles notifications from multiple applications, but does not yet handle stacked notifications (when a single application displays multiple applications). This will be possible with some more work.

Feedback is welcomed!

2 Likes

Thanks again, Dirk. I’m still working through all the really useful resources you shared.

Using your Objective-C code, I’ve been able to get some info on the basic NSConcreteAttributedString class:

Combined Objective-C Code
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

void printMethodsOfClass(Class cls) {
	unsigned int methodCount = 0;
	Method *methodList = class_copyMethodList(cls, &methodCount);
	for (unsigned int i = 0; i < methodCount; i++) {
		Method method = methodList[i];
		SEL selector = method_getName(method);
		NSLog(@"Method: %@", NSStringFromSelector(selector));
	}
	free(methodList);
}

void printPropertiesOfClass(Class cls) {
	unsigned int propertyCount = 0;
	objc_property_t *propertyList = class_copyPropertyList(cls, &propertyCount);
	for (unsigned int i = 0; i < propertyCount; i++) {
		const char *propertyName = property_getName(propertyList[i]);
		NSLog(@"Property: %s", propertyName);
	}
	free(propertyList);
}

int main(int argc, char *argv[]) {
	@autoreleasepool {
		id NSConcreteAttributedString = [[NSClassFromString(@"NSConcreteAttributedString") alloc] init];
		printMethodsOfClass([NSConcreteAttributedString class]);
		printPropertiesOfClass([NSConcreteAttributedString class]);
	}
}
Output

2024-11-05 23:59:17.871 Untitled[37560:1456322] Method: dealloc
2024-11-05 23:59:17.871 Untitled[37560:1456322] Method: copyWithZone:
2024-11-05 23:59:17.871 Untitled[37560:1456322] Method: init
2024-11-05 23:59:17.871 Untitled[37560:1456322] Method: length
2024-11-05 23:59:17.871 Untitled[37560:1456322] Method: string
2024-11-05 23:59:17.871 Untitled[37560:1456322] Method: initWithString:
2024-11-05 23:59:17.871 Untitled[37560:1456322] Method: attribute:atIndex:effectiveRange:
2024-11-05 23:59:17.871 Untitled[37560:1456322] Method: attributesAtIndex:effectiveRange:
2024-11-05 23:59:17.871 Untitled[37560:1456322] Method: initWithString:attributes:
2024-11-05 23:59:17.871 Untitled[37560:1456322] Method: _runArrayHoldingAttributes
2024-11-05 23:59:17.871 Untitled[37560:1456322] Method: initWithAttributedString:

However, my issue is that this is an NSConcreteAttributedString is within an isolate binary… I’m not sure how to get an accessibility API reference from this class (since I believe the hierarchy always starts at the application level?). So, would I have to create a GUI application & display this NSConcreteAttributedString in order to be able to get the accessibility reference?

I agree; I’m hoping someone will come up with another solution. In this situation, though, notification parsing will likely break with major macOS releases – private API or no. :face_with_diagonal_mouth:


I’ve had a chance to work through your NotificationCenter script (thanks again!):

NotificationCenter.scptd
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"
use framework "PFAssistive"

property ca : a reference to current application

display notification "Test 123" with title "Title" subtitle "SubTitle"
delay 1

display alert getTopMessageString()

on getTopMessageString()
	set currMessage to ""
	set appElement to ca's PFApplicationUIElement's applicationUIElementWithBundleIdentifier:"com.apple.notificationcenterui" delegate:(missing value)
	repeat with i from 0 to (appElement's childrenCount()) - 1
		set currElement to (appElement's AXChildren's objectAtIndex:i)
		set cuurDescription to (currElement's valueForAttribute:"AXRoleDescription")
		if cuurDescription as text is "Systemdialog" then
			repeat
				if appElement's childrenCount() > 0 then
					set currElement to (currElement's AXChildren's objectAtIndex:0) -- group 1 
					set cuurDescription to (currElement's valueForAttribute:"AXSubrole")
					if cuurDescription as text is "AXNotificationCenterBanner" then
						set currAttributedDescription to (currElement's valueForAttribute:"AXAttributedDescription")
						set currAttributeString to (ca's NSAttributedString's alloc()'s initWithAttributedString:currAttributedDescription)
						set currMessage to (currAttributeString's |string|) as text
						exit repeat
					end if
				else
					exit repeat
				end if
			end repeat
			exit repeat
		end if
	end repeat
	return currMessage
end getTopMessageString

But, it looks like notifications are no longer “AXNotificationCenterBanner”, but rather “AXNotificationCenterAlert”, which doesn’t seem to have a “AXAttributedDescription” any more. Were you still on Sonoma?

Sequoia seems to use SwiftUI for the Notification Center – I’m not sure if that’s new, or if it’s helpful in trying to access the private class details.

1 Like

Wow! :wave:

The solution is much better than using the PFAssistive framework, because it also works in the script editor!

Also, you should already have the code to access the NSConcreteAttributedString class (if you not convert them into a string)

self.accessibilityDescription = getAttributeValue(element: element, attributeName: “AXDescription”) as? String // kAXDescriptionAttribute

This is the same as what I do with the PFAssistive Framework:

set currAttributedDescription to (currElement's valueForAttribute: “AXAttributedDescription”)

and according to the script debugger, an NSCFAttributedString is returned at this point:

which, in Apple’s documentation, corresponds to an NS(Concrete)AttributedString:

The NSAttributedString class and its Core Foundation counterpart, CFAttributedString, are bridged royalty-free, meaning you can use the two types interchangeably in your code without losing any text or attribute information.

The ChatCPT says that you should be able to see the private members of the class in the Xcode debugger.

Try to analyze this return value. I did not succeed, because my Xcode knowledge is not mature enough to set it to the correct type. When I use NSAttributedString, it is always nil. But I do that in ASObjc:

set currAttributeString to (ca's NSAttributedString's alloc()'s initWithAttributedString:currAttributedDescription)

and the original type is obviously retained:


:thinking:

According to the UI browser, it is an “AXNotificationCenterBanner” (AXSubrole) and I am also looking for it in my sample.

No, Sequoia 15.1

Yes, it looks like it and I fear that using private classes and not taking AppleScript into account will probably become more and more common.

I don’t know whether the information for the title, subtitle and message is available separately in the private class. I only assume so, because otherwise the output of the font etc. would make no sense. but perhaps this is the case and the fact that you get something at all is a mistake.

Edit:
I also realized that the information (in my case) is in the attribute “AXAttributedDescription” and not “AXDescription” and the Subrole is definitely “AXNotificationCenterBanner”:



1 Like

@tree_frog, I think I found the most disgusting and vile way to get what you want.

It’s not pretty. It’s UGLY, but it works lol.

So, I came across an article that kind of explained this change a bit, and I started get one of those bad ideas that I occasionally get. That article eventually led me to finding this fellow’s script on Github, which I proceeded to modify.

(I tried to link both, but this site won’t let me for whatever reason).

As disgusting as it all may be, it does work… after some conditions have been met:

  1. The notification you are trying to read from must be “dismissed” on its own i.e., ‘unchecked.’ If you click the “Close” button or check the notification, then it’s gone for good. This makes sense considering how Notification Center functions.

  2. If notification “X” come through and dismisses on it’s own, and then the same happens for notification “Y”, then you won’t be getting “X” without a bit of modifying of the script.

Anyway, here is the crime against humanity lol:

#!/bin/zsh

# Path Notification Center’s Sqlite3 database
DB_PATH="$HOME/Library/Group Containers/group.com.apple.usernoted/db2"

#Whatever directory you want to copy $DB_PATH to
DEST=“/Add/Your/Own/Path“

# Make directory if it doesn't exist
mkdir -p "$DEST"

# Copy Notification database to hardcoded destination
cp -r "$DB_PATH" "$DEST"

COPIED_DB="$DEST/db2/db"

# Check if the file was copied successfully
if [[ ! -f "$COPIED_DB" ]]; then
    echo "Error: Database file not found at $COPIED_DB"
    exit 1
fi

# Query to select the data column as hex from the record table
SQL_QUERY="SELECT hex(data) FROM record ORDER BY ROWID DESC LIMIT 1;"

# Execute the query and process each row
sqlite3 "$COPIED_DB" "$SQL_QUERY" | while read -r HEXDATA; do
    # Convert hex to binary and then to plist format

    # Uncomment line to see all keys && values from ALL unchecked notifications
    #echo "$HEXDATA" | xxd -r -p - | plutil -p - 

    # This will only show you the notification's "body" text
    echo "$HEXDATA" | xxd -r -p - | plutil -p - | awk '/"body"/ {sub(/^[^"]*"body" => "/, ""); sub(/"$/, ""); print}'
done

# Remove tmp directory, if you want
# rm -R "$DEST"