Customizing the display time of banners in the Notification Center

The System Preferences Notification Center pane allows the user to customize certain notification behaviors, such as notification type (no notification vs banner vs alert) and a global do-not-disturb switch. Whereas notification alerts stay open until the user closes them, notification banners display for a fixed duration of time set by the system (approximately 5 seconds), and no option is offered to customize that value. The current handler, called displayNotification, is an attempt to add that missing feature for Notification Center banners (hereafter referred to simply as “notifications”.)

The handler uses ASObjC’s NSUserNotificationCenter and NSUserNotification classes to achieve finer control over notifications than that provided by Applescript’s display notification Standard Additions command. The handler delivers a new unique instance of the notification to the user notification center every second, then removes the previous instance after allowing sufficient time for the new instance to be displayed. This removal process prevents a large number of identical-appearing notifications from accumulating in Notification Center’s display menu. The handler offers the user control over whether the final notification instance is removed from or left in the Notification Center at the end of handler execution; in the former case, it will remain visible in the Notification Center’s display menu. The handler’s actions are effected through a background osascript so that control is returned to the calling program immediately after the handler is called. Please refer to the comments in the handler code for greater detail concerning the handler’s actions.

The handler input argument is used to configure the notification to be displayed. It is an AppleScript record of the following form:


set inputRecord to {theTitle:..., theSubtitle:..., informativeText..., displayTime:..., removeWhenDone:...}

where:

theTitle = text string containing the notification’s title
- largest font size and topmost location in the displayed notification
- corresponds to the display notification command’s with title parameter
- default value if omitted = “” (the empty string, in which case the system will display
“osascript” as the title, which is the name of the process delivering the notification)

theSubtitle = text string containing the notification’s subtitle
- middle font size and mid location in the displayed notification
- corresponds to the display notification command’s subtitle parameter
- default value if omitted = “” (the empty string, in which case no subtitle will be displayed)

informativeText = text string containing the notification’s informative text
- smallest font size and bottommost location in the displayed notification
- corresponds to the display notification command’s direct parameter
- default value if omitted = “” (the empty string, in which case no informative text will be displayed)

displayTime = integer specifying the time in seconds the notification is to be displayed
- any value < 5 will be set to 5, the minimum display time of the system
- default value if omitted = 5

removeWhenDone = true or false
- true will result in the final notification instance being removed from the Notification Center’s display
menu at the end of handler execution (all other notification instances will have already been removed)
- default value if omitted = true

A couple of caveats with the current handler:

  1. Because the handler uses repetitive firing of notifications to give the illusion of a single notification, it doesn’t handle multiple simultaneous notifications well, unless, that is, you like watching a “Dancing Notifications” show. :slight_smile:

  2. If an old unrelated notification is deleted from the Notification Center menu while a notification generated by the current handler is being displayed, the current notification may transiently disappear while the old notification is being deleted then reappear once the deletion is completed.

Example:


-- The following handler call will display a notification with title = "The current date and time is:", no subtitle, informative text = current date/time value, display time = 60 seconds, and removal of the notification from the Notification Center display menu at the end of handler execution:

set currDateTime to (current date) as text

displayNotification({theTitle:"The current date and time is:", theSubtitle:"", informativeText:currDateTime, displayTime:60, removeWhenDone:true})

-- or equivalently and taking advantage of default values for missing input record properties --

displayNotification({theTitle:"The current date and time is:", informativeText:currDateTime, displayTime:60})

Handler:


on displayNotification(inputRecord)
	-- Displays a Notification Center banner with the options to customize the banner's title, subtitle, informative text, display time, and persistence in the Notification Center menu after completion of handler execution
	
	-- Utility handler to convert the input record values to their text representations for incorporation into the background osascript
	script util
		on textValue(theObject)
			set tid to AppleScript's text item delimiters
			try
				|| of {theObject}
			on error m
				try
					set AppleScript's text item delimiters to "{"
					set m to m's text items 2 thru -1 as text
					set AppleScript's text item delimiters to "}"
					set theText to m's text items 1 thru -2 as text
				end try
			end try
			set AppleScript's text item delimiters to tid
			return theText
		end textValue
	end script
	
	-- Process and validate the input record
	try
		tell inputRecord
			if (its class ≠ record) and (it ≠ {}) then error "The input argument is not an Applescript record."
			tell (it & {theTitle:"", theSubtitle:"", informativeText:"", displayTime:5, removeWhenDone:true})
				if length ≠ 5 then error "The input argument record has an invalid property." & return & return & "Valid properties include:" & return & return & tab & "theTitle" & return & tab & "theSubtitle" & return & tab & "informativeText" & return & tab & "displayTime" & return & tab & "removeWhenDone"
				set {theTitle, theSubtitle, informativeText, displayTime, removeWhenDone} to {its theTitle, its theSubtitle, its informativeText, its displayTime, its removeWhenDone}
				if theTitle's class ≠ text then error "The input argument's theTitle property is not a text string."
				if theSubtitle's class ≠ text then error "The input argument's theSubtitle property is not a text string."
				if informativeText's class ≠ text then error "The input argument's informativeText property is not a text string."
				if displayTime's class is not in {integer, real} then error "The input argument's displayTime property is not a number (it should be an integer ≥ 5)."
				set displayTime to displayTime as integer
				if removeWhenDone's class ≠ boolean then error "The input argument's removeWhenDone property is not a boolean true or false value."
			end tell
		end tell
	on error m number n
		error ("Problem with handler displayNotification:" & return & return & m) number n
	end try
	
	-- Execute the notification-displaying script as a background osascript so that control is returned to the calling program immediately after the handler is called
	do shell script "osascript -e " & ("

		use framework \"Foundation\"
		use scripting additions
		property || : current application
		
		on run
			-- Get the osascript's notification configuration parameters from the handler input record values
			set {theTitle, theSubtitle, informativeText, displayTime, removeWhenDone} to " & util's textValue({theTitle, theSubtitle, informativeText, displayTime, removeWhenDone}) & "
			-- Create a reference to the default notification center, and set the current osascript to be its delegate
			set userNotificationCenter to (||'s NSUserNotificationCenter)'s defaultUserNotificationCenter()
			set userNotificationCenter's delegate to me
			-- Set the number of one-second repeat loop cycles to the input display time minus 5 (minimum value = 1) to adjust for the 5-second system display time of the final delivered notification
			set nReps to displayTime - 5
			if nReps < 1 then set nReps to 1
			-- Deliver a new instance of the user notification to the notification center every second for the predetermined number of times
			set theNotification to null
			repeat nReps times
				-- Save a reference to the most recently delivered user notification instance so that it can be removed after the current instance is displayed
				set oldNotification to theNotification
				-- Create a new user notification instance using the input record's title, subtitle, and informative text values, along with a unique identifying string for its identifier property
				set theNotification to (||'s NSUserNotification)'s alloc()'s init()
				tell theNotification to set {its title, its subtitle, its informativeText, its identifier} to {theTitle, theSubtitle, informativeText, (||'s NSUUID)'s UUID()'s UUIDString()}
				-- Deliver the new user notification instance to the notification center
				(userNotificationCenter's deliverNotification:theNotification)
				-- After a one second delay to allow sufficient time for the new notification to be displayed, remove the previously delivered notification instance
				delay 1
				if oldNotification ≠ null then (userNotificationCenter's removeDeliveredNotification:oldNotification)
			end repeat
			-- After the final user notification instance has been allowed sufficient time to complete its display, remove it if specified by the input record
			if removeWhenDone then
				delay 5
				(userNotificationCenter's removeDeliveredNotification:theNotification)
			end if
		end run
		
		on userNotificationCenter:userNotificationCenter shouldPresentNotification:theNotification
			-- Set the return value of this handler to true so that all notifications delivered to the notification center by the current osascript (the notification center's delegate) are displayed
			return true
		end userNotificationCenter:shouldPresentNotification:
		
	")'s quoted form & " >/dev/null 2>&1 &"
	
end displayNotification

Edit notes:
- Since the post was first submitted, it became evident that the osascript would best be coded as a single multi-line text string rather than lines of text to be concatenated. The current version of the osascript reflects that simplification.
- Also, minor cosmetic improvements were made to the line indentations and comments in the osascript.