mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-05 12:45:47 -07:00
Bump rumps from 0.3.0 to 0.4.0 (#1896)
* Bump rumps from 0.3.0 to 0.4.0 Bumps [rumps](https://github.com/jaredks/rumps) from 0.3.0 to 0.4.0. - [Release notes](https://github.com/jaredks/rumps/releases) - [Changelog](https://github.com/jaredks/rumps/blob/master/CHANGES.rst) - [Commits](https://github.com/jaredks/rumps/commits) --- updated-dependencies: - dependency-name: rumps dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * Update rumps==0.4.0 Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> [skip ci]
This commit is contained in:
parent
b31d75aeee
commit
79cf61c53e
11 changed files with 573 additions and 243 deletions
|
@ -2,7 +2,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# rumps: Ridiculously Uncomplicated macOS Python Statusbar apps.
|
||||
# Copyright: (c) 2017, Jared Suttles. All rights reserved.
|
||||
# Copyright: (c) 2020, Jared Suttles. All rights reserved.
|
||||
# License: BSD, see LICENSE for details.
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
|
@ -17,10 +17,14 @@ statusbar application.
|
|||
"""
|
||||
|
||||
__title__ = 'rumps'
|
||||
__version__ = '0.3.0'
|
||||
__version__ = '0.4.0'
|
||||
__author__ = 'Jared Suttles'
|
||||
__license__ = 'Modified BSD'
|
||||
__copyright__ = 'Copyright 2017 Jared Suttles'
|
||||
__copyright__ = 'Copyright 2020 Jared Suttles'
|
||||
|
||||
from .rumps import (separator, debug_mode, alert, notification, application_support, timers, quit_application, timer,
|
||||
clicked, notifications, MenuItem, SliderMenuItem, Timer, Window, App, slider)
|
||||
from . import notifications as _notifications
|
||||
from .rumps import (separator, debug_mode, alert, application_support, timers, quit_application, timer,
|
||||
clicked, MenuItem, SliderMenuItem, Timer, Window, App, slider)
|
||||
|
||||
notifications = _notifications.on_notification
|
||||
notification = _notifications.notify
|
||||
|
|
92
lib/rumps/_internal.py
Normal file
92
lib/rumps/_internal.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import inspect
|
||||
import traceback
|
||||
|
||||
import Foundation
|
||||
|
||||
from . import compat
|
||||
from . import exceptions
|
||||
|
||||
|
||||
def require_string(*objs):
|
||||
for obj in objs:
|
||||
if not isinstance(obj, compat.string_types):
|
||||
raise TypeError(
|
||||
'a string is required but given {0}, a {1}'.format(obj, type(obj).__name__)
|
||||
)
|
||||
|
||||
|
||||
def require_string_or_none(*objs):
|
||||
for obj in objs:
|
||||
if not(obj is None or isinstance(obj, compat.string_types)):
|
||||
raise TypeError(
|
||||
'a string or None is required but given {0}, a {1}'.format(obj, type(obj).__name__)
|
||||
)
|
||||
|
||||
|
||||
def call_as_function_or_method(func, *args, **kwargs):
|
||||
# The idea here is that when using decorators in a class, the functions passed are not bound so we have to
|
||||
# determine later if the functions we have (those saved as callbacks) for particular events need to be passed
|
||||
# 'self'.
|
||||
#
|
||||
# This works for an App subclass method or a standalone decorated function. Will attempt to find function as
|
||||
# a bound method of the App instance. If it is found, use it, otherwise simply call function.
|
||||
from . import rumps
|
||||
try:
|
||||
app = getattr(rumps.App, '*app_instance')
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
for name, method in inspect.getmembers(app, predicate=inspect.ismethod):
|
||||
if method.__func__ is func:
|
||||
return method(*args, **kwargs)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
def guard_unexpected_errors(func):
|
||||
"""Decorator to be used in PyObjC callbacks where an error bubbling up
|
||||
would cause a crash. Instead of crashing, print the error to stderr and
|
||||
prevent passing to PyObjC layer.
|
||||
|
||||
For Python 3, print the exception using chaining. Accomplished by setting
|
||||
the cause of :exc:`rumps.exceptions.InternalRumpsError` to the exception.
|
||||
|
||||
For Python 2, emulate exception chaining by printing the original exception
|
||||
followed by :exc:`rumps.exceptions.InternalRumpsError`.
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
except Exception as e:
|
||||
internal_error = exceptions.InternalRumpsError(
|
||||
'an unexpected error occurred within an internal callback'
|
||||
)
|
||||
if compat.PY2:
|
||||
import sys
|
||||
traceback.print_exc()
|
||||
print('\nThe above exception was the direct cause of the following exception:\n', file=sys.stderr)
|
||||
traceback.print_exception(exceptions.InternalRumpsError, internal_error, None)
|
||||
else:
|
||||
internal_error.__cause__ = e
|
||||
traceback.print_exception(exceptions.InternalRumpsError, internal_error, None)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def string_to_objc(x):
|
||||
if isinstance(x, compat.binary_type):
|
||||
return Foundation.NSData.alloc().initWithData_(x)
|
||||
elif isinstance(x, compat.string_types):
|
||||
return Foundation.NSString.alloc().initWithString_(x)
|
||||
else:
|
||||
raise TypeError(
|
||||
"expected a string or a bytes-like object but provided %s, "
|
||||
"having type '%s'" % (
|
||||
x,
|
||||
type(x).__name__
|
||||
)
|
||||
)
|
|
@ -1,17 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
rumps.compat
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Compatibility for Python 2 and Python 3 major versions.
|
||||
|
||||
:copyright: (c) 2020 by Jared Suttles
|
||||
:license: BSD-3-Clause, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
|
||||
if not PY2:
|
||||
binary_type = bytes
|
||||
text_type = str
|
||||
string_types = (str,)
|
||||
|
||||
iteritems = lambda d: iter(d.items())
|
||||
|
||||
import collections.abc as collections_abc
|
||||
|
||||
else:
|
||||
binary_type = ()
|
||||
text_type = unicode
|
||||
string_types = (str, unicode)
|
||||
|
||||
iteritems = lambda d: d.iteritems()
|
||||
|
||||
import collections as collections_abc
|
||||
|
|
40
lib/rumps/events.py
Normal file
40
lib/rumps/events.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import traceback
|
||||
|
||||
from . import _internal
|
||||
|
||||
|
||||
class EventEmitter(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.callbacks = set()
|
||||
self._executor = _internal.call_as_function_or_method
|
||||
|
||||
def register(self, func):
|
||||
self.callbacks.add(func)
|
||||
return func
|
||||
|
||||
def unregister(self, func):
|
||||
try:
|
||||
self.callbacks.remove(func)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def emit(self, *args, **kwargs):
|
||||
#print('EventEmitter("%s").emit called' % self.name)
|
||||
for callback in self.callbacks:
|
||||
try:
|
||||
self._executor(callback, *args, **kwargs)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
__call__ = register
|
||||
|
||||
|
||||
before_start = EventEmitter('before_start')
|
||||
on_notification = EventEmitter('on_notification')
|
||||
on_sleep = EventEmitter('on_sleep')
|
||||
on_wake = EventEmitter('on_wake')
|
||||
before_quit = EventEmitter('before_quit')
|
9
lib/rumps/exceptions.py
Normal file
9
lib/rumps/exceptions.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class RumpsError(Exception):
|
||||
"""A generic rumps error occurred."""
|
||||
|
||||
|
||||
class InternalRumpsError(RumpsError):
|
||||
"""Internal mechanism powering functionality of rumps failed."""
|
|
@ -1,10 +1,266 @@
|
|||
INFO_PLIST_TEMPLATE = '''\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>%(name)s</string>
|
||||
</dict>
|
||||
</plist>
|
||||
'''
|
||||
# -*- 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)
|
||||
|
|
|
@ -82,7 +82,7 @@ class OrderedDict(dict):
|
|||
def clear(self):
|
||||
'od.clear() -> None. Remove all items from od.'
|
||||
try:
|
||||
for node in self.__map.itervalues():
|
||||
for node in self.__map.values():
|
||||
del node[:]
|
||||
root = self.__root
|
||||
root[:] = [root, root, None]
|
||||
|
|
|
@ -1,38 +1,32 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# rumps: Ridiculously Uncomplicated macOS Python Statusbar apps.
|
||||
# Copyright: (c) 2017, Jared Suttles. All rights reserved.
|
||||
# License: BSD, see LICENSE for details.
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
_NOTIFICATIONS = True
|
||||
# rumps: Ridiculously Uncomplicated macOS Python Statusbar apps.
|
||||
# Copyright: (c) 2020, Jared Suttles. All rights reserved.
|
||||
# License: BSD, see LICENSE for details.
|
||||
|
||||
|
||||
# For compatibility with pyinstaller
|
||||
# See: http://stackoverflow.com/questions/21058889/pyinstaller-not-finding-pyobjc-library-macos-python
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
try:
|
||||
from Foundation import NSUserNotification, NSUserNotificationCenter
|
||||
except ImportError:
|
||||
_NOTIFICATIONS = False
|
||||
|
||||
from Foundation import (NSDate, NSTimer, NSRunLoop, NSDefaultRunLoopMode, NSSearchPathForDirectoriesInDomains,
|
||||
NSMakeRect, NSLog, NSObject, NSMutableDictionary, NSString)
|
||||
from AppKit import NSApplication, NSStatusBar, NSMenu, NSMenuItem, NSAlert, NSTextField, NSSecureTextField, NSImage, NSSlider, NSSize
|
||||
NSMakeRect, NSLog, NSObject, NSMutableDictionary, NSString, NSUserDefaults)
|
||||
from AppKit import NSApplication, NSStatusBar, NSMenu, NSMenuItem, NSAlert, NSTextField, NSSecureTextField, NSImage, NSSlider, NSSize, NSWorkspace, NSWorkspaceWillSleepNotification, NSWorkspaceDidWakeNotification
|
||||
from PyObjCTools import AppHelper
|
||||
|
||||
import inspect
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
import traceback
|
||||
import weakref
|
||||
|
||||
from collections import Mapping, Iterable
|
||||
from .compat import text_type, string_types, iteritems, collections_abc
|
||||
from .text_field import Editing, SecureEditing
|
||||
from .utils import ListDict
|
||||
from .compat import text_type, string_types, iteritems
|
||||
|
||||
from . import _internal
|
||||
from . import events
|
||||
from . import notifications
|
||||
|
||||
_TIMERS = weakref.WeakKeyDictionary()
|
||||
separator = object()
|
||||
|
@ -78,11 +72,13 @@ def alert(title=None, message='', ok=None, cancel=None, other=None, icon_path=No
|
|||
message = message.replace('%', '%%')
|
||||
if title is not None:
|
||||
title = text_type(title)
|
||||
_require_string_or_none(ok)
|
||||
_internal.require_string_or_none(ok)
|
||||
if not isinstance(cancel, string_types):
|
||||
cancel = 'Cancel' if cancel else None
|
||||
alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_(
|
||||
title, ok, cancel, other, message)
|
||||
if NSUserDefaults.standardUserDefaults().stringForKey_('AppleInterfaceStyle') == 'Dark':
|
||||
alert.window().setAppearance_(AppKit.NSAppearance.appearanceNamed_('NSAppearanceNameVibrantDark'))
|
||||
alert.setAlertStyle_(0) # informational style
|
||||
if icon_path is not None:
|
||||
icon = _nsimage_from_file(icon_path)
|
||||
|
@ -91,119 +87,6 @@ def alert(title=None, message='', ok=None, cancel=None, other=None, icon_path=No
|
|||
return alert.runModal()
|
||||
|
||||
|
||||
def _gather_info_issue_9():
|
||||
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:
|
||||
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 notification(title, subtitle, message, data=None, sound=True, action_button=None, other_button=None,
|
||||
has_reply_button=False, 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,
|
||||
|
||||
"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.
|
||||
"""
|
||||
if not _NOTIFICATIONS:
|
||||
raise RuntimeError('OS X 10.8+ is required to send notifications')
|
||||
|
||||
if data is not None and not isinstance(data, Mapping):
|
||||
raise TypeError('notification data must be a mapping')
|
||||
|
||||
_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(App, '*app_instance')
|
||||
dumped = app.serializer.dumps(data)
|
||||
ns_dict = NSMutableDictionary.alloc().init()
|
||||
ns_string = NSString.alloc().initWithString_(dumped)
|
||||
ns_dict.setDictionary_({'value': ns_string})
|
||||
notification.setUserInfo_(ns_dict)
|
||||
|
||||
if icon is not None:
|
||||
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 has_reply_button:
|
||||
notification.setHasReplyButton_(True)
|
||||
|
||||
notification.setDeliveryDate_(NSDate.dateWithTimeInterval_sinceDate_(0, NSDate.date()))
|
||||
notification_center = _default_user_notification_center()
|
||||
notification_center.scheduleNotification_(notification)
|
||||
|
||||
|
||||
def application_support(name):
|
||||
"""Return the application support folder path for the given `name`, creating it if it doesn't exist."""
|
||||
app_support_path = os.path.join(NSSearchPathForDirectoriesInDomains(14, 1, 1).objectAtIndex_(0), name)
|
||||
|
@ -248,18 +131,6 @@ def _nsimage_from_file(filename, dimensions=None, template=None):
|
|||
return image
|
||||
|
||||
|
||||
def _require_string(*objs):
|
||||
for obj in objs:
|
||||
if not isinstance(obj, string_types):
|
||||
raise TypeError('a string is required but given {0}, a {1}'.format(obj, type(obj).__name__))
|
||||
|
||||
|
||||
def _require_string_or_none(*objs):
|
||||
for obj in objs:
|
||||
if not(obj is None or isinstance(obj, string_types)):
|
||||
raise TypeError('a string or None is required but given {0}, a {1}'.format(obj, type(obj).__name__))
|
||||
|
||||
|
||||
# Decorators and helper function serving to register functions for dealing with interaction and events
|
||||
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
def timer(interval):
|
||||
|
@ -365,36 +236,6 @@ def slider(*args, **options):
|
|||
return f
|
||||
return decorator
|
||||
|
||||
|
||||
def notifications(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'
|
||||
|
||||
"""
|
||||
notifications.__dict__['*notification_center'] = f
|
||||
return f
|
||||
|
||||
|
||||
def _call_as_function_or_method(func, event):
|
||||
# The idea here is that when using decorators in a class, the functions passed are not bound so we have to
|
||||
# determine later if the functions we have (those saved as callbacks) for particular events need to be passed
|
||||
# 'self'.
|
||||
#
|
||||
# This works for an App subclass method or a standalone decorated function. Will attempt to find function as
|
||||
# a bound method of the App instance. If it is found, use it, otherwise simply call function.
|
||||
app = getattr(App, '*app_instance')
|
||||
for name, method in inspect.getmembers(app, predicate=inspect.ismethod):
|
||||
if method.__func__ is func:
|
||||
return method(event)
|
||||
return func(event)
|
||||
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
|
||||
|
@ -468,14 +309,14 @@ class Menu(ListDict):
|
|||
menu.add(iterable)
|
||||
return
|
||||
|
||||
for n, ele in enumerate(iteritems(iterable) if isinstance(iterable, Mapping) else iterable):
|
||||
for n, ele in enumerate(iteritems(iterable) if isinstance(iterable, collections_abc.Mapping) else iterable):
|
||||
|
||||
# for mappings we recurse but don't drop down a level in the menu
|
||||
if not isinstance(ele, MenuItem) and isinstance(ele, Mapping):
|
||||
if not isinstance(ele, MenuItem) and isinstance(ele, collections_abc.Mapping):
|
||||
parse_menu(ele, menu, depth)
|
||||
|
||||
# any iterables other than strings and MenuItems
|
||||
elif not isinstance(ele, (string_types, MenuItem)) and isinstance(ele, Iterable):
|
||||
elif not isinstance(ele, (string_types, MenuItem)) and isinstance(ele, collections_abc.Iterable):
|
||||
try:
|
||||
menuitem, submenu = ele
|
||||
except TypeError:
|
||||
|
@ -580,7 +421,7 @@ class MenuItem(Menu):
|
|||
# NOTE:
|
||||
# Because of the quirks of PyObjC, a class level dictionary **inside an NSObject subclass for 10.9.x** is required
|
||||
# in order to have callback_ be a @classmethod. And we need callback_ to be class level because we can't use
|
||||
# instances in setTarget_ method of NSMenuItem. Otherwise this would be much more straightfoward like Timer class.
|
||||
# instances in setTarget_ method of NSMenuItem. Otherwise this would be much more straightforward like Timer class.
|
||||
#
|
||||
# So the target is always the NSApp class and action is always the @classmethod callback_ -- for every function
|
||||
# decorated with @clicked(...). All we do is lookup the MenuItem instance and the user-provided callback function
|
||||
|
@ -690,6 +531,35 @@ class MenuItem(Menu):
|
|||
def state(self, new_state):
|
||||
self._menuitem.setState_(new_state)
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""Indicates whether the menu item is hidden.
|
||||
|
||||
.. versionadded:: 0.4.0
|
||||
|
||||
"""
|
||||
return self._menuitem.isHidden()
|
||||
|
||||
@hidden.setter
|
||||
def hidden(self, value):
|
||||
self._menuitem.setHidden_(value)
|
||||
|
||||
def hide(self):
|
||||
"""Hide the menu item.
|
||||
|
||||
.. versionadded:: 0.4.0
|
||||
|
||||
"""
|
||||
self.hidden = True
|
||||
|
||||
def show(self):
|
||||
"""Show the menu item.
|
||||
|
||||
.. versionadded:: 0.4.0
|
||||
|
||||
"""
|
||||
self.hidden = False
|
||||
|
||||
def set_callback(self, callback, key=None):
|
||||
"""Set the function serving as callback for when a click event occurs on this menu item. When `callback` is
|
||||
``None``, it will disable the callback function and grey out the menu item. If `key` is a string, set as the
|
||||
|
@ -703,7 +573,7 @@ class MenuItem(Menu):
|
|||
:param callback: the function to be called when the user clicks on this menu item.
|
||||
:param key: the key shortcut to click this menu item.
|
||||
"""
|
||||
_require_string_or_none(key)
|
||||
_internal.require_string_or_none(key)
|
||||
if key is not None:
|
||||
self._menuitem.setKeyEquivalent_(key)
|
||||
NSApp._ns_to_py_and_callback[self._menuitem] = self, callback
|
||||
|
@ -744,7 +614,7 @@ class SliderMenuItem(object):
|
|||
self._slider = NSSlider.alloc().init()
|
||||
self._slider.setMinValue_(min_value)
|
||||
self._slider.setMaxValue_(max_value)
|
||||
self._slider.setValue_(value)
|
||||
self._slider.setDoubleValue_(value)
|
||||
self._slider.setFrameSize_(NSSize(*dimensions))
|
||||
self._slider.setTarget_(NSApp)
|
||||
self._menuitem = NSMenuItem.alloc().init()
|
||||
|
@ -774,11 +644,11 @@ class SliderMenuItem(object):
|
|||
@property
|
||||
def value(self):
|
||||
"""The current position of the slider."""
|
||||
return self._slider.value()
|
||||
return self._slider.doubleValue()
|
||||
|
||||
@value.setter
|
||||
def value(self, new_value):
|
||||
self._slider.setValue_(new_value)
|
||||
self._slider.setDoubleValue_(new_value)
|
||||
|
||||
|
||||
class SeparatorMenuItem(object):
|
||||
|
@ -860,9 +730,9 @@ class Timer(object):
|
|||
def callback_(self, _):
|
||||
_log(self)
|
||||
try:
|
||||
return _call_as_function_or_method(getattr(self, '*callback'), self)
|
||||
return _internal.call_as_function_or_method(getattr(self, '*callback'), self)
|
||||
except Exception:
|
||||
_log(traceback.format_exc())
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class Window(object):
|
||||
|
@ -899,7 +769,7 @@ class Window(object):
|
|||
self._cancel = bool(cancel)
|
||||
self._icon = None
|
||||
|
||||
_require_string_or_none(ok)
|
||||
_internal.require_string_or_none(ok)
|
||||
if not isinstance(cancel, string_types):
|
||||
cancel = 'Cancel' if cancel else None
|
||||
|
||||
|
@ -908,9 +778,9 @@ class Window(object):
|
|||
self._alert.setAlertStyle_(0) # informational style
|
||||
|
||||
if secure:
|
||||
self._textfield = NSSecureTextField.alloc().initWithFrame_(NSMakeRect(0, 0, *dimensions))
|
||||
self._textfield = SecureEditing.alloc().initWithFrame_(NSMakeRect(0, 0, *dimensions))
|
||||
else:
|
||||
self._textfield = NSTextField.alloc().initWithFrame_(NSMakeRect(0, 0, *dimensions))
|
||||
self._textfield = Editing.alloc().initWithFrame_(NSMakeRect(0, 0, *dimensions))
|
||||
self._textfield.setSelectable_(True)
|
||||
self._alert.setAccessoryView_(self._textfield)
|
||||
|
||||
|
@ -982,7 +852,7 @@ class Window(object):
|
|||
|
||||
:param name: the text for a new button. Must be a string.
|
||||
"""
|
||||
_require_string(name)
|
||||
_internal.require_string(name)
|
||||
self._alert.addButtonWithTitle_(name)
|
||||
|
||||
def add_buttons(self, iterable=None, *args):
|
||||
|
@ -1009,6 +879,8 @@ class Window(object):
|
|||
:return: a :class:`rumps.rumps.Response` object that contains the text and the button clicked as an integer.
|
||||
"""
|
||||
_log(self)
|
||||
if NSUserDefaults.standardUserDefaults().stringForKey_('AppleInterfaceStyle') == 'Dark':
|
||||
self._alert.window().setAppearance_(AppKit.NSAppearance.appearanceNamed_('NSAppearanceNameVibrantDark'))
|
||||
clicked = self._alert.runModal() % 999
|
||||
if clicked > 2 and self._cancel:
|
||||
clicked -= 1
|
||||
|
@ -1062,27 +934,7 @@ class NSApp(NSObject):
|
|||
_ns_to_py_and_callback = {}
|
||||
|
||||
def userNotificationCenter_didActivateNotification_(self, notification_center, notification):
|
||||
notification_center.removeDeliveredNotification_(notification)
|
||||
ns_dict = notification.userInfo()
|
||||
if ns_dict is None:
|
||||
data = None
|
||||
else:
|
||||
dumped = ns_dict['value']
|
||||
app = getattr(App, '*app_instance')
|
||||
data = app.serializer.loads(dumped)
|
||||
|
||||
try:
|
||||
notification_function = getattr(notifications, '*notification_center')
|
||||
except AttributeError: # notification center function not specified -> no error but warning in log
|
||||
_log('WARNING: notification received but no function specified for answering it; use @notifications '
|
||||
'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())
|
||||
notifications._clicked(notification_center, notification)
|
||||
|
||||
def initializeStatusBar(self):
|
||||
self.nsstatusitem = NSStatusBar.systemStatusBar().statusItemWithLength_(-1) # variable dimensions
|
||||
|
@ -1113,14 +965,42 @@ class NSApp(NSObject):
|
|||
if not (self.nsstatusitem.title() or self.nsstatusitem.image()):
|
||||
self.nsstatusitem.setTitle_(self._app['_name'])
|
||||
|
||||
def applicationDidFinishLaunching_(self, notification):
|
||||
workspace = NSWorkspace.sharedWorkspace()
|
||||
notificationCenter = workspace.notificationCenter()
|
||||
notificationCenter.addObserver_selector_name_object_(
|
||||
self,
|
||||
self.receiveSleepNotification_,
|
||||
NSWorkspaceWillSleepNotification,
|
||||
None
|
||||
)
|
||||
notificationCenter.addObserver_selector_name_object_(
|
||||
self,
|
||||
self.receiveWakeNotification_,
|
||||
NSWorkspaceDidWakeNotification,
|
||||
None
|
||||
)
|
||||
|
||||
def receiveSleepNotification_(self, ns_notification):
|
||||
_log('receiveSleepNotification')
|
||||
events.on_sleep.emit()
|
||||
|
||||
def receiveWakeNotification_(self, ns_notification):
|
||||
_log('receiveWakeNotification')
|
||||
events.on_wake.emit()
|
||||
|
||||
def applicationWillTerminate_(self, ns_notification):
|
||||
_log('applicationWillTerminate')
|
||||
events.before_quit.emit()
|
||||
|
||||
@classmethod
|
||||
def callback_(cls, nsmenuitem):
|
||||
self, callback = cls._ns_to_py_and_callback[nsmenuitem]
|
||||
_log(self)
|
||||
try:
|
||||
return _call_as_function_or_method(callback, self)
|
||||
return _internal.call_as_function_or_method(callback, self)
|
||||
except Exception:
|
||||
_log(traceback.format_exc())
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class App(object):
|
||||
|
@ -1150,7 +1030,7 @@ class App(object):
|
|||
serializer = pickle
|
||||
|
||||
def __init__(self, name, title=None, icon=None, template=None, menu=None, quit_button='Quit'):
|
||||
_require_string(name)
|
||||
_internal.require_string(name)
|
||||
self._name = name
|
||||
self._icon = self._icon_nsimage = self._title = None
|
||||
self._template = template
|
||||
|
@ -1186,7 +1066,7 @@ class App(object):
|
|||
|
||||
@title.setter
|
||||
def title(self, title):
|
||||
_require_string_or_none(title)
|
||||
_internal.require_string_or_none(title)
|
||||
self._title = title
|
||||
try:
|
||||
self._nsapp.setStatusBarTitle()
|
||||
|
@ -1308,13 +1188,7 @@ class App(object):
|
|||
self._nsapp = NSApp.alloc().init()
|
||||
self._nsapp._app = self.__dict__ # allow for dynamic modification based on this App instance
|
||||
nsapplication.setDelegate_(self._nsapp)
|
||||
if _NOTIFICATIONS:
|
||||
try:
|
||||
notification_center = _default_user_notification_center()
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
notification_center.setDelegate_(self._nsapp)
|
||||
notifications._init_nsapp(self._nsapp)
|
||||
|
||||
setattr(App, '*app_instance', self) # class level ref to running instance (for passing self to App subclasses)
|
||||
t = b = None
|
||||
|
@ -1326,4 +1200,6 @@ class App(object):
|
|||
|
||||
self._nsapp.initializeStatusBar()
|
||||
|
||||
AppHelper.installMachInterrupt()
|
||||
events.before_start.emit()
|
||||
AppHelper.runEventLoop()
|
||||
|
|
32
lib/rumps/text_field.py
Normal file
32
lib/rumps/text_field.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from AppKit import NSApplication, NSTextField, NSSecureTextField, NSKeyDown, NSCommandKeyMask
|
||||
|
||||
|
||||
class Editing(NSTextField):
|
||||
"""NSTextField with cut, copy, paste, undo and selectAll"""
|
||||
def performKeyEquivalent_(self, event):
|
||||
return _perform_key_equivalent(self, event)
|
||||
|
||||
|
||||
class SecureEditing(NSSecureTextField):
|
||||
"""NSSecureTextField with cut, copy, paste, undo and selectAll"""
|
||||
def performKeyEquivalent_(self, event):
|
||||
return _perform_key_equivalent(self, event)
|
||||
|
||||
|
||||
def _perform_key_equivalent(self, event):
|
||||
if event.type() == NSKeyDown and event.modifierFlags() & NSCommandKeyMask:
|
||||
if event.charactersIgnoringModifiers() == "x":
|
||||
NSApplication.sharedApplication().sendAction_to_from_("cut:", None, self)
|
||||
return True
|
||||
elif event.charactersIgnoringModifiers() == "c":
|
||||
NSApplication.sharedApplication().sendAction_to_from_("copy:", None, self)
|
||||
return True
|
||||
elif event.charactersIgnoringModifiers() == "v":
|
||||
NSApplication.sharedApplication().sendAction_to_from_("paste:", None, self)
|
||||
return True
|
||||
elif event.charactersIgnoringModifiers() == "z":
|
||||
NSApplication.sharedApplication().sendAction_to_from_("undo:", None, self)
|
||||
return True
|
||||
elif event.charactersIgnoringModifiers() == "a":
|
||||
NSApplication.sharedApplication().sendAction_to_from_("selectAll:", None, self)
|
||||
return True
|
|
@ -1,10 +1,15 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# rumps: Ridiculously Uncomplicated macOS Python Statusbar apps.
|
||||
# Copyright: (c) 2017, Jared Suttles. All rights reserved.
|
||||
# License: BSD, see LICENSE for details.
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
"""
|
||||
rumps.utils
|
||||
~~~~~~~~~~~
|
||||
|
||||
Generic container classes and utility functions.
|
||||
|
||||
:copyright: (c) 2020 by Jared Suttles
|
||||
:license: BSD-3-Clause, see LICENSE for details.
|
||||
"""
|
||||
|
||||
from .packages.ordereddict import OrderedDict as _OrderedDict
|
||||
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ python-twitter==3.5
|
|||
pytz==2022.6
|
||||
requests==2.28.1
|
||||
requests-oauthlib==1.3.1
|
||||
rumps==0.3.0; platform_system == "Darwin"
|
||||
rumps==0.4.0; platform_system == "Darwin"
|
||||
simplejson==3.17.6
|
||||
six==1.16.0
|
||||
soupsieve==2.3.2.post1
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue