# -*- coding: utf-8 -*- _ENABLED = True try: from Foundation import NSUserNotification, NSUserNotificationCenter except ImportError: _ENABLED = False import datetime import os import sys import traceback import Foundation from . import _internal from . import compat from . import events def on_notification(f): """Decorator for registering a function to serve as a "notification center" for the application. This function will receive the data associated with an incoming macOS notification sent using :func:`rumps.notification`. This occurs whenever the user clicks on a notification for this application in the macOS Notification Center. .. code-block:: python @rumps.notifications def notification_center(info): if 'unix' in info: print 'i know this' """ return events.on_notification.register(f) def _gather_info_issue_9(): # pragma: no cover missing_plist = False missing_bundle_ident = False info_plist_path = os.path.join(os.path.dirname(sys.executable), 'Info.plist') try: with open(info_plist_path) as f: import plistlib try: load_plist = plistlib.load except AttributeError: load_plist = plistlib.readPlist try: load_plist(f)['CFBundleIdentifier'] except Exception: missing_bundle_ident = True except IOError as e: import errno if e.errno == errno.ENOENT: # No such file or directory missing_plist = True info = '\n\n' if missing_plist: info += 'In this case there is no file at "%(info_plist_path)s"' info += '\n\n' confidence = 'should' elif missing_bundle_ident: info += 'In this case the file at "%(info_plist_path)s" does not contain a value for "CFBundleIdentifier"' info += '\n\n' confidence = 'should' else: confidence = 'may' info += 'Running the following command %(confidence)s fix the issue:\n' info += '/usr/libexec/PlistBuddy -c \'Add :CFBundleIdentifier string "rumps"\' %(info_plist_path)s\n' return info % {'info_plist_path': info_plist_path, 'confidence': confidence} def _default_user_notification_center(): notification_center = NSUserNotificationCenter.defaultUserNotificationCenter() if notification_center is None: # pragma: no cover info = ( 'Failed to setup the notification center. This issue occurs when the "Info.plist" file ' 'cannot be found or is missing "CFBundleIdentifier".' ) try: info += _gather_info_issue_9() except Exception: pass raise RuntimeError(info) else: return notification_center def _init_nsapp(nsapp): if _ENABLED: try: notification_center = _default_user_notification_center() except RuntimeError: pass else: notification_center.setDelegate_(nsapp) @_internal.guard_unexpected_errors def _clicked(ns_user_notification_center, ns_user_notification): from . import rumps ns_user_notification_center.removeDeliveredNotification_(ns_user_notification) ns_dict = ns_user_notification.userInfo() if ns_dict is None: data = None else: dumped = ns_dict['value'] app = getattr(rumps.App, '*app_instance', rumps.App) try: data = app.serializer.loads(dumped) except Exception: traceback.print_exc() return # notification center function not specified => no error but log warning if not events.on_notification.callbacks: rumps._log( 'WARNING: notification received but no function specified for ' 'answering it; use @notifications decorator to register a function.' ) else: notification = Notification(ns_user_notification, data) events.on_notification.emit(notification) def notify(title, subtitle, message, data=None, sound=True, action_button=None, other_button=None, has_reply_button=False, icon=None, ignoreDnD=False): """Send a notification to Notification Center (OS X 10.8+). If running on a version of macOS that does not support notifications, a ``RuntimeError`` will be raised. Apple says, "The userInfo content must be of reasonable serialized size (less than 1k) or an exception will be thrown." So don't do that! :param title: text in a larger font. :param subtitle: text in a smaller font below the `title`. :param message: text representing the body of the notification below the `subtitle`. :param data: will be passed to the application's "notification center" (see :func:`rumps.notifications`) when this notification is clicked. :param sound: whether the notification should make a noise when it arrives. :param action_button: title for the action button. :param other_button: title for the other button. :param has_reply_button: whether or not the notification has a reply button. :param icon: the filename of an image for the notification's icon, will replace the default. :param ignoreDnD: whether the notification should ignore do not disturb, e.g., appear also while screen sharing. """ from . import rumps if not _ENABLED: raise RuntimeError('OS X 10.8+ is required to send notifications') _internal.require_string_or_none(title, subtitle, message) notification = NSUserNotification.alloc().init() notification.setTitle_(title) notification.setSubtitle_(subtitle) notification.setInformativeText_(message) if data is not None: app = getattr(rumps.App, '*app_instance', rumps.App) dumped = app.serializer.dumps(data) objc_string = _internal.string_to_objc(dumped) ns_dict = Foundation.NSMutableDictionary.alloc().init() ns_dict.setDictionary_({'value': objc_string}) notification.setUserInfo_(ns_dict) if icon is not None: notification.set_identityImage_(rumps._nsimage_from_file(icon)) if sound: notification.setSoundName_("NSUserNotificationDefaultSoundName") if action_button: notification.setActionButtonTitle_(action_button) notification.set_showsButtons_(True) if other_button: notification.setOtherButtonTitle_(other_button) notification.set_showsButtons_(True) if has_reply_button: notification.setHasReplyButton_(True) if ignoreDnD: notification.set_ignoresDoNotDisturb_(True) notification.setDeliveryDate_(Foundation.NSDate.dateWithTimeInterval_sinceDate_(0, Foundation.NSDate.date())) notification_center = _default_user_notification_center() notification_center.scheduleNotification_(notification) class Notification(compat.collections_abc.Mapping): def __init__(self, ns_user_notification, data): self._ns = ns_user_notification self._data = data def __repr__(self): return '<{0}: [data: {1}]>'.format(type(self).__name__, repr(self._data)) @property def title(self): return compat.text_type(self._ns.title()) @property def subtitle(self): return compat.text_type(self._ns.subtitle()) @property def message(self): return compat.text_type(self._ns.informativeText()) @property def activation_type(self): activation_type = self._ns.activationType() if activation_type == 1: return 'contents_clicked' elif activation_type == 2: return 'action_button_clicked' elif activation_type == 3: return 'replied' elif activation_type == 4: return 'additional_action_clicked' @property def delivered_at(self): ns_date = self._ns.actualDeliveryDate() seconds = ns_date.timeIntervalSince1970() dt = datetime.datetime.fromtimestamp(seconds) return dt @property def response(self): ns_attributed_string = self._ns.response() if ns_attributed_string is None: return None ns_string = ns_attributed_string.string() return compat.text_type(ns_string) @property def data(self): return self._data def _check_if_mapping(self): if not isinstance(self._data, compat.collections_abc.Mapping): raise TypeError( 'notification cannot be used as a mapping when data is not a ' 'mapping' ) def __getitem__(self, key): self._check_if_mapping() return self._data[key] def __iter__(self): self._check_if_mapping() return iter(self._data) def __len__(self): self._check_if_mapping() return len(self._data)