Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ dist/
*.py[co]

.idea/

rumps\.egg-info/
59 changes: 50 additions & 9 deletions rumps/rumps.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

from Foundation import (NSDate, NSTimer, NSRunLoop, NSDefaultRunLoopMode, NSSearchPathForDirectoriesInDomains,
NSMakeRect, NSLog, NSObject)
from AppKit import NSApplication, NSStatusBar, NSMenu, NSMenuItem, NSAlert, NSTextField, NSImage
from AppKit import (NSApplication, NSStatusBar, NSMenu, NSMenuItem, NSAlert, NSTextField, NSSecureTextField, NSImage,
NSBeep, NSSpeechSynthesizer)
from PyObjCTools import AppHelper

import inspect
Expand All @@ -40,9 +41,31 @@ def _log(*args):
else:
def _log(*_):
pass


debug_mode(False)


def error():
"""Generate a simple system beep."""
NSBeep()


class SpeechSynthesizer:

def __init__(self, message):
self.synth = NSSpeechSynthesizer.alloc().initWithVoice_(None)
self.synth.startSpeakingString_(message)

def stop(self):
if self.synth.speaking:
self.synth.stopSpeaking()


def speak(message):
return SpeechSynthesizer(message)


def alert(title=None, message='', ok=None, cancel=None, other=None, icon_path=None):
"""Generate a simple alert window.

Expand All @@ -62,8 +85,8 @@ def alert(title=None, message='', ok=None, cancel=None, other=None, icon_path=No
:param cancel: the text for the "cancel" button. If a string, the button will have that text. If `cancel`
evaluates to ``True``, will create a button with text "Cancel". Otherwise, this button will not be
created.
:param other: the text for the "other" button. If a string, the button will have that text. Otherwise, this button will not be
created.
:param other: the text for the "other" button. If a string, the button will have that text. Otherwise, this button
will not be created.
:param icon_path: a path to an image. If ``None``, the applications icon is used.
:return: a number representing the button pressed. The "ok" button is ``1`` and "cancel" is ``0``.
"""
Expand Down Expand Up @@ -137,7 +160,8 @@ def _default_user_notification_center():
return notification_center


def notification(title, subtitle, message, data=None, sound=True):
def notification(title, subtitle, message, data=None, sound=True, action_button=None, other_button=None,
reply_button=None, icon=None):
"""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,

Expand All @@ -151,6 +175,9 @@ def notification(title, subtitle, message, data=None, sound=True):
: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 reply_button: A boolean representing whether or not the notification has a reply button.
"""
if not _NOTIFICATIONS:
raise RuntimeError('OS X 10.8+ is required to send notifications')
Expand All @@ -161,11 +188,21 @@ def notification(title, subtitle, message, data=None, sound=True):
notification.setTitle_(title)
notification.setSubtitle_(subtitle)
notification.setInformativeText_(message)
infoDict = NSMutableDictionary.alloc().init()
infoDict.setDictionary_({} if data is None else data)
notification.setUserInfo_(infoDict)
info_dict = NSMutableDictionary.alloc().init()
info_dict.setDictionary_({} if data is None else data)
notification.setUserInfo_(info_dict)
if icon:
notification.set_identityImage_(_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 reply_button:
notification.setHasReplyButton_(True)
notification.setDeliveryDate_(NSDate.dateWithTimeInterval_sinceDate_(0, NSDate.date()))
notification_center = _default_user_notification_center()
notification_center.scheduleNotification_(notification)
Expand Down Expand Up @@ -754,9 +791,10 @@ class Window(object):
evaluates to ``True``, will create a button with text "Cancel". Otherwise, this button will not be
created.
:param dimensions: the size of the editable textbox. Must be sequence with a length of 2.
:param secure: should the text field be secured or not. With True the window can be used for passwords.
"""

def __init__(self, message='', title='', default_text='', ok=None, cancel=None, dimensions=(320, 160)):
def __init__(self, message='', title='', default_text='', ok=None, cancel=None, dimensions=(320, 160), secure=None):
message = text_type(message)
message = message.replace('%', '%%')
title = text_type(title)
Expand All @@ -772,7 +810,8 @@ def __init__(self, message='', title='', default_text='', ok=None, cancel=None,
title, ok, cancel, None, message)
self._alert.setAlertStyle_(0) # informational style

self._textfield = NSTextField.alloc().initWithFrame_(NSMakeRect(0, 0, *dimensions))
text_field_type = NSSecureTextField if secure else NSTextField
self._textfield = text_field_type.alloc().initWithFrame_(NSMakeRect(0, 0, *dimensions))
self._textfield.setSelectable_(True)
self._alert.setAccessoryView_(self._textfield)

Expand Down Expand Up @@ -933,6 +972,8 @@ def userNotificationCenter_didActivateNotification_(self, notification_center, n
'decorator to register a function.')
else:
try:
data['activationType'] = notification.activationType()
data['actualDeliveryDate'] = notification.actualDeliveryDate()
_call_as_function_or_method(notification_function, data)
except Exception:
_log(traceback.format_exc())
Expand Down
14 changes: 9 additions & 5 deletions rumps/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
# License: BSD, see LICENSE for details.
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
try: # Python 2.7+
from collections import OrderedDict as _OrderedDict
from test.support import import_fresh_module
pyCollections = import_fresh_module('collections', blocked=['_collections'])
_OrderedDict, _Link = pyCollections.OrderedDict, pyCollections._Link
except ImportError:
from .packages.ordereddict import OrderedDict as _OrderedDict

Expand All @@ -16,15 +18,17 @@
class ListDict(_OrderedDict):
def __insertion(self, link_prev, key_value):
key, value = key_value
if link_prev[2] != key:
if link_prev.key != key:
if key in self:
del self[key]
link_next = link_prev[1]
self._OrderedDict__map[key] = link_prev[1] = link_next[0] = [link_prev, link_next, key]
link_next = link_prev.next
new_link = _Link()
new_link.prev, new_link.next, new_link.key = link_prev, link_next, key
self._OrderedDict__map[key] = link_prev.next = link_next.prev = new_link
dict.__setitem__(self, key, value)

def insert_after(self, existing_key, key_value):
self.__insertion(self._OrderedDict__map[existing_key], key_value)

def insert_before(self, existing_key, key_value):
self.__insertion(self._OrderedDict__map[existing_key][0], key_value)
self.__insertion(self._OrderedDict__map[existing_key].prev, key_value)