I am having some difficulty finding out how to send local notifications on Catalina using pyobjc.
The closes example I have seen is this: PyObjC "Notifications are not allowed for this application"
I am having some difficulty finding out how to send local notifications on Catalina using pyobjc.
The closes example I have seen is this: PyObjC "Notifications are not allowed for this application"
Edit (June 27, 2020): I've created a package which has functionality to display notifications on Mac OS here. It will use PyObjC to create and display notifications. If It does not work for whatever reason, it will fallback to AppleScript notifications with osascript
. I did some testing and found that the PyObjC notifications work on some devices but don't on some.
Answer:
I have also been searching for this answer, so I'd like to share what I've found:
The first thing you'll notice is that the function notify()
defines a class, then returns an instance of it. You might be wondering why you can't directly call Notification.send(params)
. I tried it, but I was getting an error with the PyObjC, which I am unfortunately unable to fix:
# Error
class Notification(NSObject):
objc.BadPrototypeError: Objective-C expects 1 arguments, Python argument has 2 arguments for <unbound selector send of Notification at 0x10e410180>
Now onto the code:
# vscode may show the error: "No name '...' in module 'Foundation'; you can ignore it"
from Foundation import NSUserNotification, NSUserNotificationCenter, NSObject, NSDate
from PyObjCTools import AppHelper
def notify(
title='Notification',
subtitle=None, text=None,
delay=0,
action_button_title=None,
action_button_callback=None,
other_button_title=None,
other_button_callback=None,
reply_placeholder=None,
reply_callback=None
):
class Notification(NSObject):
def send(self):
notif = NSUserNotification.alloc().init()
if title is not None:
notif.setTitle_(title)
if subtitle is not None:
notif.setSubtitle_(subtitle)
if text is not None:
notif.setInformativeText_(text)
# notification buttons (main action button and other button)
if action_button_title:
notif.setActionButtonTitle_(action_button_title)
notif.set_showsButtons_(True)
if other_button_title:
notif.setOtherButtonTitle_(other_button_title)
notif.set_showsButtons_(True)
# reply button
if reply_callback:
notif.setHasReplyButton_(True)
if reply_placeholder:
notif.setResponsePlaceholder_(reply_placeholder)
NSUserNotificationCenter.defaultUserNotificationCenter().setDelegate_(self)
# setting delivery date as current date + delay (in seconds)
notif.setDeliveryDate_(NSDate.dateWithTimeInterval_sinceDate_(delay, NSDate.date()))
# schedule the notification send
NSUserNotificationCenter.defaultUserNotificationCenter().scheduleNotification_(notif)
# on if any of the callbacks are provided, start the event loop (this will keep the program from stopping)
if action_button_callback or other_button_callback or reply_callback:
print('started')
AppHelper.runConsoleEventLoop()
def userNotificationCenter_didDeliverNotification_(self, center, notif):
print('delivered notification')
def userNotificationCenter_didActivateNotification_(self, center, notif):
print('did activate')
response = notif.response()
if notif.activationType() == 1:
# user clicked on the notification (not on a button)
# don't stop event loop because the other buttons can still be pressed
pass
elif notif.activationType() == 2:
# user clicked on the action button
action_button_callback()
AppHelper.stopEventLoop()
elif notif.activationType() == 3:
# user clicked on the reply button
reply_text = response.string()
reply_callback(reply_text)
AppHelper.stopEventLoop()
# create the new notification
new_notif = Notification.alloc().init()
# return notification
return new_notif
def main():
n = notify(
title='Notification',
delay=0,
action_button_title='Action',
action_button_callback=lambda: print('Action'),
# other_button_title='Other',
# other_button_callback=lambda: print('Other'),
reply_placeholder='Enter your reply please',
reply_callback=lambda reply: print('Replied: ', reply),
)
n.send()
if __name__ == '__main__':
main()
The notify()
function takes in quite a few parameters (they are self-explanatory). The delay
is how many seconds later the notification will appear. Note that if you set a delay that's longer than the execution of the program, the notification will be sent ever after the program is being executed.
You'll see the button parameters. There are three types of buttons:
All those if
statements are setting the buttons appropriately and self explanatory. For instance, if the parameters for the other button are not provided, a Other button will not be shown.
One thing you'll notice is that if there are buttons, we are starting the console event loop:
if action_button_callback or other_button_callback or reply_callback:
print('started')
AppHelper.runConsoleEventLoop()
This is a part of Python Objective-C. This is not a good explanation, but it basically keeps program "on" (I hope someone cane give a better explanation).
Basically, if you specify that you want a button, the program will continue to be "on" until AppHelper.stopEventLoop()
(more about this later).
Now there are some "hook" functions:
userNotificationCenter_didDeliverNotification_(self, notification_center, notification)
: called when the notification is delivereduserNotificationCenter_didActivateNotification_(self, notification_center, notification)
: called when the user interacts with the notification (clicks, clicks action button, or reply) (documentation)There surely are more, but I do not think there is a hook for the notification being dismissed or ignored, unfortunately.
With userNotificationCenter_didActivateNotification_
, we can define some callbacks:
def userNotificationCenter_didActivateNotification_(self, center, notif):
print('did activate')
response = notif.response()
if notif.activationType() == 1:
# user clicked on the notification (not on a button)
# don't stop event loop because the other buttons can still be pressed
pass
elif notif.activationType() == 2:
# user clicked on the action button
# action button callback
action_button_callback()
AppHelper.stopEventLoop()
elif notif.activationType() == 3:
# user clicked on the reply button
reply_text = response.string()
# reply button callback
reply_callback(reply_text)
AppHelper.stopEventLoop()
There are different activation types for the types of actions. The text from the reply action can also be retrieved as shown.
You'll also notice the AppHelper.stopEventLoop()
at the end. This means to "end" the program from executing, since the notification has been dealt with by the user.
Now let's address all the problems with this solution.
AppHelper.stopEventLoop()
at times like this.AppHelper.stopEventLoop()
is being run after interaction, it is not possible to send multiple notifications with callbacks, as the program will stop executing after the first notification is interacted with.If you want notifications with callbacks, you probably should not, because of the problems I addressed.
If you only want to show notifications to alert the user on something, yes.
PYNC is a wrapper around terminal-notifier. However, both received their last commit in 2018. Alerter seems to be a successor to terminal-notifier, but there is not Python wrapper.
You can also try running applescript to send notifications, but you cannot set callbacks, nor can you change the icon.
I hope this answer has helped you. I am also trying to find out how to reliably send notifications with callbacks on Mac OS. I've figured out how to send notifications, but callbacks is the issue.
© 2022 - 2024 — McMap. All rights reserved.