quickmacapp 2025.3.16__py3-none-any.whl → 2025.4.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quickmacapp/__init__.py +2 -0
- quickmacapp/_background.py +155 -0
- quickmacapp/_notifications.py +479 -0
- quickmacapp/notifications.py +245 -0
- {quickmacapp-2025.3.16.dist-info → quickmacapp-2025.4.4.dist-info}/METADATA +4 -2
- quickmacapp-2025.4.4.dist-info/RECORD +12 -0
- {quickmacapp-2025.3.16.dist-info → quickmacapp-2025.4.4.dist-info}/WHEEL +1 -1
- quickmacapp-2025.3.16.dist-info/RECORD +0 -9
- {quickmacapp-2025.3.16.dist-info → quickmacapp-2025.4.4.dist-info/licenses}/LICENSE +0 -0
- {quickmacapp-2025.3.16.dist-info → quickmacapp-2025.4.4.dist-info}/top_level.txt +0 -0
quickmacapp/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from ._quickapp import Actionable, Status, mainpoint, menu, quit
|
|
2
2
|
from ._interactions import ask, choose, answer, getpass
|
|
3
|
+
from ._background import dockIconWhenVisible
|
|
3
4
|
|
|
4
5
|
__all__ = [
|
|
5
6
|
"Actionable",
|
|
@@ -11,4 +12,5 @@ __all__ = [
|
|
|
11
12
|
"choose",
|
|
12
13
|
"answer",
|
|
13
14
|
"getpass",
|
|
15
|
+
"dockIconWhenVisible",
|
|
14
16
|
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from AppKit import (
|
|
6
|
+
NSApplication,
|
|
7
|
+
NSApplicationActivateIgnoringOtherApps,
|
|
8
|
+
NSApplicationActivationPolicyAccessory,
|
|
9
|
+
NSApplicationActivationPolicyRegular,
|
|
10
|
+
NSLog,
|
|
11
|
+
NSNotification,
|
|
12
|
+
NSNotificationCenter,
|
|
13
|
+
NSRunningApplication,
|
|
14
|
+
NSWindow,
|
|
15
|
+
NSWindowWillCloseNotification,
|
|
16
|
+
NSWorkspace,
|
|
17
|
+
NSWorkspaceActiveSpaceDidChangeNotification,
|
|
18
|
+
NSWorkspaceApplicationKey,
|
|
19
|
+
NSWorkspaceDidActivateApplicationNotification,
|
|
20
|
+
NSWorkspaceDidHideApplicationNotification,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SometimesBackground:
|
|
26
|
+
"""
|
|
27
|
+
An application that is sometimes in the background but has a window that,
|
|
28
|
+
when visible, can own the menubar, become key, etc. However, when that
|
|
29
|
+
window is closed, we withdraw to the menu bar and continue running in the
|
|
30
|
+
background, as an accessory.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
mainWindow: NSWindow
|
|
34
|
+
hideIconOnOtherSpaces: bool
|
|
35
|
+
onSpaceChange: Callable[[], None]
|
|
36
|
+
currentlyRegular: bool = False
|
|
37
|
+
previouslyActiveApp: NSRunningApplication = field(init=False)
|
|
38
|
+
|
|
39
|
+
def someApplicationActivated_(self, notification: Any) -> None:
|
|
40
|
+
# NSLog(f"active {notification} {__file__}")
|
|
41
|
+
whichApp = notification.userInfo()[NSWorkspaceApplicationKey]
|
|
42
|
+
|
|
43
|
+
if whichApp == NSRunningApplication.currentApplication():
|
|
44
|
+
if self.currentlyRegular:
|
|
45
|
+
# NSLog("show editor window")
|
|
46
|
+
self.mainWindow.setIsVisible_(True)
|
|
47
|
+
else:
|
|
48
|
+
# NSLog("reactivate workaround")
|
|
49
|
+
self.currentlyRegular = True
|
|
50
|
+
self.previouslyActiveApp.activateWithOptions_(
|
|
51
|
+
NSApplicationActivateIgnoringOtherApps
|
|
52
|
+
)
|
|
53
|
+
app = NSApplication.sharedApplication()
|
|
54
|
+
app.setActivationPolicy_(NSApplicationActivationPolicyRegular)
|
|
55
|
+
self.mainWindow.setIsVisible_(True)
|
|
56
|
+
from twisted.internet import reactor
|
|
57
|
+
|
|
58
|
+
reactor.callLater( # type:ignore[attr-defined]
|
|
59
|
+
0.1, lambda: app.activateIgnoringOtherApps_(True)
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
self.previouslyActiveApp = whichApp
|
|
63
|
+
|
|
64
|
+
def someApplicationHidden_(self, notification: Any) -> None:
|
|
65
|
+
"""
|
|
66
|
+
An app was hidden.
|
|
67
|
+
"""
|
|
68
|
+
whichApp = notification.userInfo()[NSWorkspaceApplicationKey]
|
|
69
|
+
if whichApp == NSRunningApplication.currentApplication():
|
|
70
|
+
# 'hide others' (and similar functionality) should *not* hide the
|
|
71
|
+
# progress window; that would obviate the whole point of having
|
|
72
|
+
# this app live in the background in order to maintain a constant
|
|
73
|
+
# presence in the user's visual field. however if we're being told
|
|
74
|
+
# to hide, don't ignore the user, hide the main window and retreat
|
|
75
|
+
# into the background as if we were closed.
|
|
76
|
+
self.mainWindow.close()
|
|
77
|
+
app = NSApplication.sharedApplication()
|
|
78
|
+
app.unhide_(self)
|
|
79
|
+
|
|
80
|
+
def someSpaceActivated_(self, notification: NSNotification) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Sometimes, fullscreen application stop getting the HUD overlay.
|
|
83
|
+
"""
|
|
84
|
+
menuBarOwner = NSWorkspace.sharedWorkspace().menuBarOwningApplication()
|
|
85
|
+
# me = NSRunningApplication.currentApplication()
|
|
86
|
+
NSLog("space activated where allegedly %@ owns the menu bar", menuBarOwner)
|
|
87
|
+
if not self.mainWindow.isOnActiveSpace():
|
|
88
|
+
if self.hideIconOnOtherSpaces:
|
|
89
|
+
NSLog("I am not on the active space, closing the window")
|
|
90
|
+
self.mainWindow.close()
|
|
91
|
+
else:
|
|
92
|
+
NSLog("I am not on the active space, but that's OK, leaving window open.")
|
|
93
|
+
else:
|
|
94
|
+
NSLog("I am on the active space; not closing.")
|
|
95
|
+
self.onSpaceChange()
|
|
96
|
+
|
|
97
|
+
def someWindowWillClose_(self, notification: NSNotification) -> None:
|
|
98
|
+
"""
|
|
99
|
+
The main window that we're observing will close.
|
|
100
|
+
"""
|
|
101
|
+
if notification.object() == self.mainWindow:
|
|
102
|
+
self.currentlyRegular = False
|
|
103
|
+
NSApplication.sharedApplication().setActivationPolicy_(
|
|
104
|
+
NSApplicationActivationPolicyAccessory
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def startObserving(self) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Attach the various callbacks.
|
|
110
|
+
"""
|
|
111
|
+
NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
|
|
112
|
+
self, "someWindowWillClose:", NSWindowWillCloseNotification, None
|
|
113
|
+
)
|
|
114
|
+
wsnc = NSWorkspace.sharedWorkspace().notificationCenter()
|
|
115
|
+
|
|
116
|
+
self.previouslyActiveApp = (
|
|
117
|
+
NSWorkspace.sharedWorkspace().menuBarOwningApplication()
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
wsnc.addObserver_selector_name_object_(
|
|
121
|
+
self,
|
|
122
|
+
"someApplicationActivated:",
|
|
123
|
+
NSWorkspaceDidActivateApplicationNotification,
|
|
124
|
+
None,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
wsnc.addObserver_selector_name_object_(
|
|
128
|
+
self,
|
|
129
|
+
"someApplicationHidden:",
|
|
130
|
+
NSWorkspaceDidHideApplicationNotification,
|
|
131
|
+
None,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
wsnc.addObserver_selector_name_object_(
|
|
135
|
+
self,
|
|
136
|
+
"someSpaceActivated:",
|
|
137
|
+
NSWorkspaceActiveSpaceDidChangeNotification,
|
|
138
|
+
None,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def dockIconWhenVisible(
|
|
143
|
+
mainWindow: NSWindow,
|
|
144
|
+
hideIconOnOtherSpaces: bool = True,
|
|
145
|
+
onSpaceChange: Callable[[], None] = lambda: None,
|
|
146
|
+
):
|
|
147
|
+
"""
|
|
148
|
+
When the given main window is visible, we should have a dock icon (i.e.: be
|
|
149
|
+
NSApplicationActivationPolicyRegular). When our application is activated,
|
|
150
|
+
(i.e.: the user launches it from Spotlight, Finder, or similar) we should
|
|
151
|
+
make the window visible so that the dock icon appears. When that window is
|
|
152
|
+
then closed, or when our application is hidden, we should hide our dock
|
|
153
|
+
icon (i.e.: be NSApplicationActivationPolicyAccessory).
|
|
154
|
+
"""
|
|
155
|
+
SometimesBackground(mainWindow, hideIconOnOtherSpaces, onSpaceChange).startObserving()
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from zoneinfo import ZoneInfo
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Any, Awaitable, Callable, Protocol, TypeAlias
|
|
7
|
+
|
|
8
|
+
from datetype import DateTime
|
|
9
|
+
|
|
10
|
+
from Foundation import NSError, NSLog, NSObject, NSDateComponents
|
|
11
|
+
from objc import object_property
|
|
12
|
+
from twisted.internet.defer import Deferred
|
|
13
|
+
from UserNotifications import (
|
|
14
|
+
UNNotificationCategoryOptions,
|
|
15
|
+
UNAuthorizationOptionNone,
|
|
16
|
+
UNMutableNotificationContent,
|
|
17
|
+
UNNotification,
|
|
18
|
+
UNNotificationAction,
|
|
19
|
+
UNNotificationActionOptions,
|
|
20
|
+
UNNotificationActionOptionAuthenticationRequired,
|
|
21
|
+
UNNotificationActionOptionDestructive,
|
|
22
|
+
UNNotificationActionOptionForeground,
|
|
23
|
+
UNNotificationCategory,
|
|
24
|
+
UNNotificationCategoryOptionAllowInCarPlay,
|
|
25
|
+
UNNotificationCategoryOptionHiddenPreviewsShowSubtitle,
|
|
26
|
+
UNNotificationCategoryOptionHiddenPreviewsShowTitle,
|
|
27
|
+
UNNotificationPresentationOptionBanner,
|
|
28
|
+
UNNotificationPresentationOptions,
|
|
29
|
+
UNNotificationRequest,
|
|
30
|
+
UNNotificationResponse,
|
|
31
|
+
UNNotificationSettings,
|
|
32
|
+
UNNotificationTrigger,
|
|
33
|
+
UNCalendarNotificationTrigger,
|
|
34
|
+
UNTextInputNotificationAction,
|
|
35
|
+
UNUserNotificationCenter,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make[T: NSObject](cls: type[T], **attributes: object) -> T:
|
|
40
|
+
self: T = cls.alloc().init()
|
|
41
|
+
self.setValuesForKeysWithDictionary_(attributes)
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class _AppNotificationsCtxBuilder:
|
|
47
|
+
_center: UNUserNotificationCenter
|
|
48
|
+
_cfg: _NotifConfigImpl | None
|
|
49
|
+
|
|
50
|
+
async def __aenter__(self) -> _NotifConfigImpl:
|
|
51
|
+
"""
|
|
52
|
+
Request authorization, then start building this notifications manager.
|
|
53
|
+
"""
|
|
54
|
+
NSLog("beginning build")
|
|
55
|
+
grantDeferred: Deferred[bool] = Deferred()
|
|
56
|
+
|
|
57
|
+
def completed(granted: bool, error: NSError | None) -> None:
|
|
58
|
+
# TODO: convert non-None NSErrors into failures on this Deferred
|
|
59
|
+
grantDeferred.callback(granted)
|
|
60
|
+
NSLog(
|
|
61
|
+
"Notification authorization response: %@ with error: %@", granted, error
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
NSLog("requesting authorization")
|
|
65
|
+
self._center.requestAuthorizationWithOptions_completionHandler_(
|
|
66
|
+
UNAuthorizationOptionNone, completed
|
|
67
|
+
)
|
|
68
|
+
NSLog("requested")
|
|
69
|
+
granted = await grantDeferred
|
|
70
|
+
settingsDeferred: Deferred[UNNotificationSettings] = Deferred()
|
|
71
|
+
|
|
72
|
+
def gotSettings(settings: UNNotificationSettings) -> None:
|
|
73
|
+
NSLog("received notification settings %@", settings)
|
|
74
|
+
settingsDeferred.callback(settings)
|
|
75
|
+
|
|
76
|
+
NSLog("requesting notification settings")
|
|
77
|
+
self._center.getNotificationSettingsWithCompletionHandler_(gotSettings)
|
|
78
|
+
settings = await settingsDeferred
|
|
79
|
+
NSLog("initializing config")
|
|
80
|
+
self.cfg = _NotifConfigImpl(
|
|
81
|
+
self._center,
|
|
82
|
+
[],
|
|
83
|
+
_wasGrantedPermission=granted,
|
|
84
|
+
_settings=settings,
|
|
85
|
+
)
|
|
86
|
+
NSLog("done!")
|
|
87
|
+
return self.cfg
|
|
88
|
+
|
|
89
|
+
async def __aexit__(
|
|
90
|
+
self,
|
|
91
|
+
exc_type: type[BaseException] | None,
|
|
92
|
+
exc_value: BaseException | None,
|
|
93
|
+
traceback: TracebackType | None,
|
|
94
|
+
/,
|
|
95
|
+
) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Finalize the set of notification categories and actions in use for this application.
|
|
98
|
+
"""
|
|
99
|
+
NSLog("async exit from ctx manager")
|
|
100
|
+
if traceback is None and self.cfg is not None:
|
|
101
|
+
qmandw = _QMANotificationDelegateWrapper.alloc().initWithConfig_(self.cfg)
|
|
102
|
+
qmandw.retain()
|
|
103
|
+
NSLog("Setting delegate! %@", qmandw)
|
|
104
|
+
self._center.setDelegate_(qmandw)
|
|
105
|
+
self.cfg._register()
|
|
106
|
+
else:
|
|
107
|
+
NSLog("NOT setting delegate!!!")
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _QMANotificationDelegateWrapper(NSObject):
|
|
112
|
+
"""
|
|
113
|
+
UNUserNotificationCenterDelegate implementation.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
config: _NotifConfigImpl = object_property()
|
|
117
|
+
|
|
118
|
+
def initWithConfig_(self, cfg: _NotifConfigImpl) -> _QMANotificationDelegateWrapper:
|
|
119
|
+
self.config = cfg
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
def userNotificationCenter_willPresentNotification_withCompletionHandler_(
|
|
123
|
+
self,
|
|
124
|
+
notificationCenter: UNUserNotificationCenter,
|
|
125
|
+
notification: UNNotification,
|
|
126
|
+
completionHandler: Callable[[UNNotificationPresentationOptions], None],
|
|
127
|
+
) -> None:
|
|
128
|
+
NSLog("willPresent: %@", notification)
|
|
129
|
+
# TODO: allow for client code to customize this; here we are saying
|
|
130
|
+
# "please present the notification to the user as a banner, even if we
|
|
131
|
+
# are in the foreground". We should allow for customization on a
|
|
132
|
+
# category basis; rather than @response.something, maybe
|
|
133
|
+
# @present.something, as a method on the python category class?
|
|
134
|
+
completionHandler(UNNotificationPresentationOptionBanner)
|
|
135
|
+
|
|
136
|
+
def userNotificationCenter_didReceiveNotificationResponse_withCompletionHandler_(
|
|
137
|
+
self,
|
|
138
|
+
notificationCenter: UNUserNotificationCenter,
|
|
139
|
+
notificationResponse: UNNotificationResponse,
|
|
140
|
+
completionHandler: Callable[[], None],
|
|
141
|
+
) -> None:
|
|
142
|
+
"""
|
|
143
|
+
We received a response to a notification.
|
|
144
|
+
"""
|
|
145
|
+
NSLog("received notification repsonse %@", notificationResponse)
|
|
146
|
+
|
|
147
|
+
# TODO: actually hook up the dispatch of the notification response to
|
|
148
|
+
# the registry of action callbacks already set up in
|
|
149
|
+
# NotificationConfig.
|
|
150
|
+
async def respond() -> None:
|
|
151
|
+
notifier = self.config._notifierByCategory(
|
|
152
|
+
notificationResponse.notification()
|
|
153
|
+
.request()
|
|
154
|
+
.content()
|
|
155
|
+
.categoryIdentifier()
|
|
156
|
+
)
|
|
157
|
+
await notifier._handleResponse(notificationResponse)
|
|
158
|
+
completionHandler()
|
|
159
|
+
|
|
160
|
+
Deferred.fromCoroutine(respond()).addErrback(
|
|
161
|
+
lambda error: NSLog("error: %@", error)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class NotificationTranslator[T](Protocol):
|
|
166
|
+
"""
|
|
167
|
+
Translate notifications from the notification ID and some user data,
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def fromNotification(self, notificationID: str, userData: dict[str, Any]) -> T:
|
|
171
|
+
"""
|
|
172
|
+
A user interacted with a notification with the given parameters;
|
|
173
|
+
deserialize them into a Python object that can process that action.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def toNotification(self, notification: T) -> tuple[str, dict[str, Any]]:
|
|
177
|
+
"""
|
|
178
|
+
The application has requested to send a notification to the operating
|
|
179
|
+
system, serialize the Python object represneting this category of
|
|
180
|
+
notification into a 2-tuple of C{notificatcionID}, C{userData} that can
|
|
181
|
+
be encapsulated in a L{UNNotificationRequest}.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class Notifier[NotifT]:
|
|
187
|
+
"""
|
|
188
|
+
A notifier for a specific category.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
# Public interface:
|
|
192
|
+
def undeliver(self, notification: NotifT) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Remove the previously-delivered notification object from the
|
|
195
|
+
notification center, if it's still there.
|
|
196
|
+
"""
|
|
197
|
+
notID, _ = self._tx.toNotification(notification)
|
|
198
|
+
self._cfg._center.removeDeliveredNotificationsWithIdentifiers_([notID])
|
|
199
|
+
|
|
200
|
+
def unsend(self, notification: NotifT) -> None:
|
|
201
|
+
"""
|
|
202
|
+
Prevent the as-yet undelivered notification object from being
|
|
203
|
+
delivered.
|
|
204
|
+
"""
|
|
205
|
+
notID, _ = self._tx.toNotification(notification)
|
|
206
|
+
self._cfg._center.removePendingNotificationRequestsWithIdentifiers_([notID])
|
|
207
|
+
|
|
208
|
+
async def notifyAt(
|
|
209
|
+
self, when: DateTime[ZoneInfo], notification: NotifT, title: str, body: str
|
|
210
|
+
) -> None:
|
|
211
|
+
components: NSDateComponents = NSDateComponents.alloc().init()
|
|
212
|
+
repeats: bool = False
|
|
213
|
+
trigger: UNNotificationTrigger = (
|
|
214
|
+
UNCalendarNotificationTrigger.triggerWithDateMatchingComponents_repeats_(
|
|
215
|
+
components,
|
|
216
|
+
repeats,
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
await self._notifyWithTrigger(trigger, notification, title, body)
|
|
220
|
+
|
|
221
|
+
# Attributes:
|
|
222
|
+
_notificationCategoryID: str
|
|
223
|
+
_cfg: _NotifConfigImpl
|
|
224
|
+
_tx: NotificationTranslator[NotifT]
|
|
225
|
+
_actionInfos: list[_oneActionInfo]
|
|
226
|
+
_allowInCarPlay: bool
|
|
227
|
+
_hiddenPreviewsShowTitle: bool
|
|
228
|
+
_hiddenPreviewsShowSubtitle: bool
|
|
229
|
+
|
|
230
|
+
# Private implementation details:
|
|
231
|
+
async def _handleResponse(self, response: UNNotificationResponse) -> None:
|
|
232
|
+
userInfo = response.notification().request().content().userInfo()
|
|
233
|
+
actionID: str = response.actionIdentifier()
|
|
234
|
+
notificationID: str = response.notification().request().identifier()
|
|
235
|
+
cat = self._tx.fromNotification(notificationID, userInfo)
|
|
236
|
+
for cb, eachActionID, action, options in self._actionInfos:
|
|
237
|
+
if actionID == eachActionID:
|
|
238
|
+
break
|
|
239
|
+
else:
|
|
240
|
+
raise KeyError(actionID)
|
|
241
|
+
await cb(cat, response)
|
|
242
|
+
|
|
243
|
+
def _createUNNotificationCategory(self) -> UNNotificationCategory:
|
|
244
|
+
actions = []
|
|
245
|
+
# We don't yet support intent identifiers.
|
|
246
|
+
intentIdentifiers: list[str] = []
|
|
247
|
+
options = 0
|
|
248
|
+
for handler, actionID, toRegister, extraOptions in self._actionInfos:
|
|
249
|
+
options |= extraOptions
|
|
250
|
+
if toRegister is not None:
|
|
251
|
+
actions.append(toRegister)
|
|
252
|
+
NSLog("actions generated: %@ options: %@", actions, options)
|
|
253
|
+
if self._allowInCarPlay:
|
|
254
|
+
# Ha ha. Someday, maybe.
|
|
255
|
+
options |= UNNotificationCategoryOptionAllowInCarPlay
|
|
256
|
+
if self._hiddenPreviewsShowTitle:
|
|
257
|
+
options |= UNNotificationCategoryOptionHiddenPreviewsShowTitle
|
|
258
|
+
if self._hiddenPreviewsShowSubtitle:
|
|
259
|
+
options |= UNNotificationCategoryOptionHiddenPreviewsShowSubtitle
|
|
260
|
+
return UNNotificationCategory.categoryWithIdentifier_actions_intentIdentifiers_options_(
|
|
261
|
+
self._notificationCategoryID, actions, intentIdentifiers, options
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
async def _notifyWithTrigger(
|
|
265
|
+
self,
|
|
266
|
+
trigger: UNNotificationTrigger,
|
|
267
|
+
notification: NotifT,
|
|
268
|
+
title: str,
|
|
269
|
+
body: str,
|
|
270
|
+
) -> None:
|
|
271
|
+
notificationID, userInfo = self._tx.toNotification(notification)
|
|
272
|
+
request = UNNotificationRequest.requestWithIdentifier_content_trigger_(
|
|
273
|
+
notificationID,
|
|
274
|
+
make(
|
|
275
|
+
UNMutableNotificationContent,
|
|
276
|
+
title=title,
|
|
277
|
+
body=body,
|
|
278
|
+
categoryIdentifier=self._notificationCategoryID,
|
|
279
|
+
userInfo=userInfo,
|
|
280
|
+
),
|
|
281
|
+
trigger,
|
|
282
|
+
)
|
|
283
|
+
d: Deferred[NSError | None] = Deferred()
|
|
284
|
+
self._cfg._center.addNotificationRequest_withCompletionHandler_(
|
|
285
|
+
request, d.callback
|
|
286
|
+
)
|
|
287
|
+
error = await d
|
|
288
|
+
NSLog("completed notification request with error %@", error)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@dataclass
|
|
292
|
+
class _NotifConfigImpl:
|
|
293
|
+
_center: UNUserNotificationCenter
|
|
294
|
+
_notifiers: list[Notifier[Any]]
|
|
295
|
+
_wasGrantedPermission: bool
|
|
296
|
+
_settings: UNNotificationSettings
|
|
297
|
+
|
|
298
|
+
def add[NotifT](
|
|
299
|
+
self,
|
|
300
|
+
category: type[NotifT],
|
|
301
|
+
translator: NotificationTranslator[NotifT],
|
|
302
|
+
allowInCarPlay: bool = False,
|
|
303
|
+
hiddenPreviewsShowTitle: bool = False,
|
|
304
|
+
hiddenPreviewsShowSubtitle: bool = False,
|
|
305
|
+
# customDismissAction: bool = False,
|
|
306
|
+
) -> Notifier[NotifT]:
|
|
307
|
+
"""
|
|
308
|
+
@param category: the category to add
|
|
309
|
+
|
|
310
|
+
@param translator: a translator that can load and save a translator.
|
|
311
|
+
"""
|
|
312
|
+
catid: str = f"{category.__module__}.{category.__qualname__}"
|
|
313
|
+
notifier = Notifier(
|
|
314
|
+
catid,
|
|
315
|
+
self,
|
|
316
|
+
translator,
|
|
317
|
+
_getAllActionInfos(category),
|
|
318
|
+
_allowInCarPlay=allowInCarPlay,
|
|
319
|
+
_hiddenPreviewsShowTitle=hiddenPreviewsShowTitle,
|
|
320
|
+
_hiddenPreviewsShowSubtitle=hiddenPreviewsShowSubtitle,
|
|
321
|
+
)
|
|
322
|
+
self._notifiers.append(notifier)
|
|
323
|
+
return notifier
|
|
324
|
+
|
|
325
|
+
def _notifierByCategory(self, categoryID: str) -> Notifier[Any]:
|
|
326
|
+
for notifier in self._notifiers:
|
|
327
|
+
if categoryID == notifier._notificationCategoryID:
|
|
328
|
+
return notifier
|
|
329
|
+
raise KeyError(categoryID)
|
|
330
|
+
|
|
331
|
+
def _register(self) -> None:
|
|
332
|
+
self._center.setNotificationCategories_(
|
|
333
|
+
[pynot._createUNNotificationCategory() for pynot in self._notifiers]
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
_ACTION_INFO_ATTR = "__qma_notification_action_info__"
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
_oneActionInfo = tuple[
|
|
341
|
+
# Action handler to stuff away into dispatch; does the pulling out of
|
|
342
|
+
# userText if necessary
|
|
343
|
+
Callable[[Any, UNNotificationResponse], Awaitable[None]],
|
|
344
|
+
# action ID
|
|
345
|
+
str,
|
|
346
|
+
# the notification action to register; None for default & dismiss
|
|
347
|
+
UNNotificationAction | None,
|
|
348
|
+
UNNotificationCategoryOptions,
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
_anyActionInfo: TypeAlias = (
|
|
353
|
+
"_PlainNotificationActionInfo | _TextNotificationActionInfo | _BuiltinActionInfo"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _getActionInfo(o: object) -> _oneActionInfo | None:
|
|
358
|
+
handler: _anyActionInfo | None = getattr(o, _ACTION_INFO_ATTR, None)
|
|
359
|
+
if handler is None:
|
|
360
|
+
return None
|
|
361
|
+
appCallback: Any = o
|
|
362
|
+
actionID = handler.identifier
|
|
363
|
+
callback = handler._makeCallback(appCallback)
|
|
364
|
+
extraOptions = handler._extraOptions
|
|
365
|
+
return (callback, actionID, handler._toAction(), extraOptions)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _setActionInfo[T](wrapt: T, actionInfo: _anyActionInfo) -> T:
|
|
369
|
+
setattr(wrapt, _ACTION_INFO_ATTR, actionInfo)
|
|
370
|
+
return wrapt
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _getAllActionInfos(t: type[object]) -> list[_oneActionInfo]:
|
|
374
|
+
result = []
|
|
375
|
+
for attr in dir(t):
|
|
376
|
+
actionInfo = _getActionInfo(getattr(t, attr, None))
|
|
377
|
+
if actionInfo is not None:
|
|
378
|
+
result.append(actionInfo)
|
|
379
|
+
return result
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _py2options(
|
|
383
|
+
foreground: bool,
|
|
384
|
+
destructive: bool,
|
|
385
|
+
authenticationRequired: bool,
|
|
386
|
+
) -> UNNotificationActionOptions:
|
|
387
|
+
"""
|
|
388
|
+
Convert some sensibly-named data types into UNNotificationActionOptions.
|
|
389
|
+
"""
|
|
390
|
+
options = 0
|
|
391
|
+
if foreground:
|
|
392
|
+
options |= UNNotificationActionOptionForeground
|
|
393
|
+
if destructive:
|
|
394
|
+
options |= UNNotificationActionOptionDestructive
|
|
395
|
+
if authenticationRequired:
|
|
396
|
+
options |= UNNotificationActionOptionAuthenticationRequired
|
|
397
|
+
return options
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@dataclass
|
|
401
|
+
class _PlainNotificationActionInfo:
|
|
402
|
+
identifier: str
|
|
403
|
+
title: str
|
|
404
|
+
foreground: bool
|
|
405
|
+
destructive: bool
|
|
406
|
+
authenticationRequired: bool
|
|
407
|
+
_extraOptions: UNNotificationCategoryOptions = 0
|
|
408
|
+
|
|
409
|
+
def _makeCallback[T](
|
|
410
|
+
self, appCallback: Callable[[T], Awaitable[None]]
|
|
411
|
+
) -> Callable[[Any, UNNotificationResponse], Awaitable[None]]:
|
|
412
|
+
async def takesNotification(self: T, response: UNNotificationResponse) -> None:
|
|
413
|
+
await appCallback(self)
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
return takesNotification
|
|
417
|
+
|
|
418
|
+
def _toAction(self) -> UNNotificationAction:
|
|
419
|
+
return UNNotificationAction.actionWithIdentifier_title_options_(
|
|
420
|
+
self.identifier,
|
|
421
|
+
self.title,
|
|
422
|
+
_py2options(
|
|
423
|
+
self.foreground,
|
|
424
|
+
self.destructive,
|
|
425
|
+
self.authenticationRequired,
|
|
426
|
+
),
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@dataclass
|
|
431
|
+
class _TextNotificationActionInfo:
|
|
432
|
+
identifier: str
|
|
433
|
+
title: str
|
|
434
|
+
foreground: bool
|
|
435
|
+
destructive: bool
|
|
436
|
+
authenticationRequired: bool
|
|
437
|
+
buttonTitle: str
|
|
438
|
+
textPlaceholder: str
|
|
439
|
+
_extraOptions: UNNotificationCategoryOptions = 0
|
|
440
|
+
|
|
441
|
+
def _makeCallback[T](
|
|
442
|
+
self, appCallback: Callable[[T, str], Awaitable[None]]
|
|
443
|
+
) -> Callable[[Any, UNNotificationResponse], Awaitable[None]]:
|
|
444
|
+
async def takesNotification(self: T, response: UNNotificationResponse) -> None:
|
|
445
|
+
await appCallback(self, response.userText())
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
return takesNotification
|
|
449
|
+
|
|
450
|
+
def _toAction(self) -> UNNotificationAction:
|
|
451
|
+
return UNTextInputNotificationAction.actionWithIdentifier_title_options_textInputButtonTitle_textInputPlaceholder_(
|
|
452
|
+
self.identifier,
|
|
453
|
+
self.title,
|
|
454
|
+
_py2options(
|
|
455
|
+
self.foreground,
|
|
456
|
+
self.destructive,
|
|
457
|
+
self.authenticationRequired,
|
|
458
|
+
),
|
|
459
|
+
self.buttonTitle,
|
|
460
|
+
self.textPlaceholder,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@dataclass
|
|
465
|
+
class _BuiltinActionInfo:
|
|
466
|
+
identifier: str
|
|
467
|
+
_extraOptions: UNNotificationCategoryOptions
|
|
468
|
+
|
|
469
|
+
def _toAction(self) -> None:
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
def _makeCallback[T](
|
|
473
|
+
self, appCallback: Callable[[T], Awaitable[None]]
|
|
474
|
+
) -> Callable[[Any, UNNotificationResponse], Awaitable[None]]:
|
|
475
|
+
async def takesNotification(self: T, response: UNNotificationResponse) -> None:
|
|
476
|
+
await appCallback(self)
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
return takesNotification
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API for emitting macOS notifications.
|
|
3
|
+
|
|
4
|
+
@see: L{configureNotifications}.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from contextlib import AbstractAsyncContextManager as _AbstractAsyncContextManager
|
|
8
|
+
from dataclasses import dataclass as _dataclass
|
|
9
|
+
from typing import Callable, Protocol
|
|
10
|
+
from zoneinfo import ZoneInfo
|
|
11
|
+
|
|
12
|
+
from datetype import DateTime
|
|
13
|
+
from UserNotifications import (
|
|
14
|
+
UNNotificationCategoryOptionCustomDismissAction as _UNNotificationCategoryOptionCustomDismissAction,
|
|
15
|
+
UNUserNotificationCenter as _UNUserNotificationCenter,
|
|
16
|
+
UNNotificationDefaultActionIdentifier as _UNNotificationDefaultActionIdentifier,
|
|
17
|
+
UNNotificationDismissActionIdentifier as _UNNotificationDismissActionIdentifier,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from quickmacapp._notifications import (
|
|
21
|
+
NotificationTranslator,
|
|
22
|
+
_BuiltinActionInfo,
|
|
23
|
+
_PlainNotificationActionInfo,
|
|
24
|
+
_setActionInfo,
|
|
25
|
+
_TextNotificationActionInfo,
|
|
26
|
+
_AppNotificationsCtxBuilder,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"NotificationTranslator",
|
|
31
|
+
"Action",
|
|
32
|
+
"TextAction",
|
|
33
|
+
"response",
|
|
34
|
+
"Notifier",
|
|
35
|
+
"NotificationConfig",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Action[NotificationT](Protocol):
|
|
40
|
+
"""
|
|
41
|
+
An action is just an async method that takes its C{self} (an instance of a
|
|
42
|
+
notification class encapsulating the ID & data), and reacts to the
|
|
43
|
+
specified action.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
async def __call__(__no_self__, /, self: NotificationT) -> None:
|
|
47
|
+
"""
|
|
48
|
+
React to the action.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TextAction[NotificationT](Protocol):
|
|
53
|
+
"""
|
|
54
|
+
A L{TextAction} is just like an L{Action}, but it takes some text.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
async def __call__(__no_self__, /, self: NotificationT, text: str) -> None:
|
|
58
|
+
"""
|
|
59
|
+
React to the action with the user's input.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@_dataclass
|
|
64
|
+
class response:
|
|
65
|
+
identifier: str
|
|
66
|
+
title: str
|
|
67
|
+
foreground: bool = False
|
|
68
|
+
destructive: bool = False
|
|
69
|
+
authenticationRequired: bool = False
|
|
70
|
+
|
|
71
|
+
def __call__[NT](self, action: Action[NT], /) -> Action[NT]:
|
|
72
|
+
return _setActionInfo(
|
|
73
|
+
action,
|
|
74
|
+
_PlainNotificationActionInfo(
|
|
75
|
+
identifier=self.identifier,
|
|
76
|
+
title=self.title,
|
|
77
|
+
foreground=self.foreground,
|
|
78
|
+
destructive=self.destructive,
|
|
79
|
+
authenticationRequired=self.authenticationRequired,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def text[NT](
|
|
84
|
+
self, *, title: str | None = None, placeholder: str = ""
|
|
85
|
+
) -> Callable[[TextAction[NT]], TextAction[NT]]:
|
|
86
|
+
return lambda wrapt: _setActionInfo(
|
|
87
|
+
wrapt,
|
|
88
|
+
_TextNotificationActionInfo(
|
|
89
|
+
identifier=self.identifier,
|
|
90
|
+
title=self.title,
|
|
91
|
+
buttonTitle=title if title is not None else self.title,
|
|
92
|
+
textPlaceholder=placeholder,
|
|
93
|
+
foreground=self.foreground,
|
|
94
|
+
destructive=self.destructive,
|
|
95
|
+
authenticationRequired=self.authenticationRequired,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def default[NT]() -> Callable[[Action[NT]], Action[NT]]:
|
|
101
|
+
return lambda wrapt: _setActionInfo(
|
|
102
|
+
wrapt, _BuiltinActionInfo(_UNNotificationDefaultActionIdentifier, 0)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def dismiss[NT]() -> Callable[[Action[NT]], Action[NT]]:
|
|
107
|
+
return lambda wrapt: _setActionInfo(
|
|
108
|
+
wrapt,
|
|
109
|
+
_BuiltinActionInfo(
|
|
110
|
+
_UNNotificationDismissActionIdentifier,
|
|
111
|
+
_UNNotificationCategoryOptionCustomDismissAction,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Notifier[NotifT](Protocol):
|
|
117
|
+
"""
|
|
118
|
+
A L{Notifier} can deliver notifications.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
async def notifyAt(
|
|
122
|
+
self, when: DateTime[ZoneInfo], notification: NotifT, title: str, body: str
|
|
123
|
+
) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Request a future notification at the given time.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def undeliver(self, notification: NotifT) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Remove the previously-delivered notification object from the
|
|
131
|
+
notification center, if it's still there.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def unsend(self, notification: NotifT) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Prevent the as-yet undelivered notification object from being
|
|
137
|
+
delivered.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class NotificationConfig(Protocol):
|
|
142
|
+
|
|
143
|
+
def add[NotifT](
|
|
144
|
+
self,
|
|
145
|
+
category: type[NotifT],
|
|
146
|
+
translator: NotificationTranslator[NotifT],
|
|
147
|
+
allowInCarPlay: bool = False,
|
|
148
|
+
hiddenPreviewsShowTitle: bool = False,
|
|
149
|
+
hiddenPreviewsShowSubtitle: bool = False,
|
|
150
|
+
) -> Notifier[NotifT]: ...
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def configureNotifications() -> _AbstractAsyncContextManager[NotificationConfig]:
|
|
154
|
+
"""
|
|
155
|
+
Configure notifications for the current application.
|
|
156
|
+
|
|
157
|
+
This is an asynchronous (using Twisted's Deferred) context manager, run
|
|
158
|
+
with `with` statement, which works like this::
|
|
159
|
+
|
|
160
|
+
async with configureNotifications() as cfg:
|
|
161
|
+
notifier = cfg.add(MyNotificationData, MyNotificationLoader())
|
|
162
|
+
|
|
163
|
+
Each L{add <NotificationConfig.add>} invocation adds a category of
|
|
164
|
+
notifications you can send, and returns an object (a L{Notifier}) that can
|
|
165
|
+
send that category of notification.
|
|
166
|
+
|
|
167
|
+
At the end of the C{async with} block, the notification configuration is
|
|
168
|
+
finalized, its state is sent to macOS, and the categories of notification
|
|
169
|
+
your application can send is frozen for the rest of the lifetime of your
|
|
170
|
+
process; the L{Notifier} objects returned from L{add
|
|
171
|
+
<NotificationConfig.add>} are now active nad can be used. Note that you
|
|
172
|
+
may only call L{configureNotifications} once in your entire process, so you
|
|
173
|
+
will need to pass those notifiers elsewhere!
|
|
174
|
+
|
|
175
|
+
Each call to add requires 2 arguments: a notification-data class which
|
|
176
|
+
stores the sent notification's ID and any other ancillary data transmitted
|
|
177
|
+
along with it, and an object that can load and store that first class, when
|
|
178
|
+
notification responses from the operating system convey data that was
|
|
179
|
+
previously scheduled as a notification. In our example above, they can be
|
|
180
|
+
as simple as this::
|
|
181
|
+
|
|
182
|
+
class MyNotificationData:
|
|
183
|
+
id: str
|
|
184
|
+
|
|
185
|
+
class MyNotificationLoader:
|
|
186
|
+
def fromNotification(
|
|
187
|
+
self, notificationID: str, userData: dict[str, object]
|
|
188
|
+
) -> MyNotificationData:
|
|
189
|
+
return MyNotificationData(notificationID)
|
|
190
|
+
def toNotification(
|
|
191
|
+
self,
|
|
192
|
+
notification: MyNotificationData,
|
|
193
|
+
) -> tuple[str, dict[str, object]]:
|
|
194
|
+
return (notification.id, {})
|
|
195
|
+
|
|
196
|
+
Then, when you want to I{send} a notification, you can do::
|
|
197
|
+
|
|
198
|
+
await notifier.notifyAt(
|
|
199
|
+
aware(datetime.now(TZ) + timedelta(seconds=5), TZ),
|
|
200
|
+
MyNotificationData("my.notification.id.1"),
|
|
201
|
+
"Title Here",
|
|
202
|
+
"Subtitle Here",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
And that will show the user a notification.
|
|
206
|
+
|
|
207
|
+
The C{MyNotificationData} class might seem simplistic to the point of
|
|
208
|
+
uselessness, and in this oversimplified case, it is! However, if you are
|
|
209
|
+
sending notifications to a user, you really need to be able to I{respond}
|
|
210
|
+
to notifications from a user, and that's where your notification data class
|
|
211
|
+
as well as L{responder} comes in. To respond to a notification when the
|
|
212
|
+
user clicks on it, you can add a method like so::
|
|
213
|
+
|
|
214
|
+
class MyNotificationData:
|
|
215
|
+
id: str
|
|
216
|
+
|
|
217
|
+
@response(identifier="response-action-1", title="Action 1")
|
|
218
|
+
async def responseAction1(self) -> None:
|
|
219
|
+
await answer("User pressed 'Action 1' button")
|
|
220
|
+
|
|
221
|
+
@response.default()
|
|
222
|
+
async def userClicked(self) -> None:
|
|
223
|
+
await answer("User clicked the notification.")
|
|
224
|
+
|
|
225
|
+
When sent with L{Notifier.notifyAt}, your C{MyNotificationData} class will
|
|
226
|
+
be serialized and deserialized with C{MyNotificationLoader.toNotification}
|
|
227
|
+
(converting your Python class into a macOS notification, to send along to
|
|
228
|
+
the OS) and C{MyNotificationLoader.fromNotification} (converting the data
|
|
229
|
+
sent along with the user's response back into a L{MyNotificationData}).
|
|
230
|
+
|
|
231
|
+
@note: If your app schedules a notification, then quits, when the user
|
|
232
|
+
responds (clicks on it, uses a button, dismisses it, etc) then the OS
|
|
233
|
+
will re-launch your application and send the notification data back in,
|
|
234
|
+
which is why all the serialization and deserialization is required.
|
|
235
|
+
Your process may have exited and thus the original notification will no
|
|
236
|
+
longer be around. However, if you are just running as a Python script,
|
|
237
|
+
piggybacking on the 'Python Launcher' app bundle, macOS will not be
|
|
238
|
+
able to re-launch your app. Notifications going back to the same
|
|
239
|
+
process seem to work okay, but note that as documented, macOS really
|
|
240
|
+
requires your application to have its own bundle and its own unique
|
|
241
|
+
CFBundleIdentifier in order to avoid any weird behavior.
|
|
242
|
+
"""
|
|
243
|
+
return _AppNotificationsCtxBuilder(
|
|
244
|
+
_UNUserNotificationCenter.currentNotificationCenter(), None
|
|
245
|
+
)
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: quickmacapp
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.4.4
|
|
4
4
|
Summary: Make it easier to write Mac apps in Python
|
|
5
5
|
Description-Content-Type: text/x-rst
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Dist: pyobjc-framework-Cocoa
|
|
8
8
|
Requires-Dist: pyobjc-framework-ExceptionHandling
|
|
9
9
|
Requires-Dist: pyobjc-framework-UserNotifications
|
|
10
|
+
Requires-Dist: datetype
|
|
10
11
|
Requires-Dist: twisted[macos_platform,tls]
|
|
12
|
+
Dynamic: license-file
|
|
11
13
|
|
|
12
14
|
QuickMacApp
|
|
13
15
|
==============================
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
quickmacapp/__init__.py,sha256=NsZW_cPvJTunDShN3RjOyK1gTwTKsghMb7wb6vRgTXI,335
|
|
2
|
+
quickmacapp/_background.py,sha256=M_ob3EF8v-Z_gpKuFDl2N9XrQz0eBLFfUp2XErDB3DU,5943
|
|
3
|
+
quickmacapp/_interactions.py,sha256=eLv_mVf5Jr-puNizlR8tDL_DlLy1Rc82XQ80o0GP5R4,3439
|
|
4
|
+
quickmacapp/_notifications.py,sha256=sT09LocFK1egW8c8miT_XD_A_nf2GfyopuGAVf_fwFA,16434
|
|
5
|
+
quickmacapp/_quickapp.py,sha256=UdZQKIJG40OSzzJ4bdMWix7bpgyZnDAGftF-ksk1VDw,7571
|
|
6
|
+
quickmacapp/notifications.py,sha256=DBkIiIDW33o5SPhUfvNmk-11_cHTiBzmfXYyaLj6pHw,8913
|
|
7
|
+
quickmacapp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
quickmacapp-2025.4.4.dist-info/licenses/LICENSE,sha256=7RIBNNvrnKHR3lw9z3KXv3Q6RRjhyxtNQoMnoUsf3_M,1091
|
|
9
|
+
quickmacapp-2025.4.4.dist-info/METADATA,sha256=Bh6bUXbBZmFR5WD-TIvRlCMb5H4P_iUx8c_g1S5ZOBA,1343
|
|
10
|
+
quickmacapp-2025.4.4.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
11
|
+
quickmacapp-2025.4.4.dist-info/top_level.txt,sha256=_iJkekUYnuWhCZbFSQyo2d5_6B7OoPwx7k527bokzeA,12
|
|
12
|
+
quickmacapp-2025.4.4.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
quickmacapp/__init__.py,sha256=w__TD52X29oKrDsFT_f5cJI8IE5HRJ6q48xLsufdlTU,263
|
|
2
|
-
quickmacapp/_interactions.py,sha256=eLv_mVf5Jr-puNizlR8tDL_DlLy1Rc82XQ80o0GP5R4,3439
|
|
3
|
-
quickmacapp/_quickapp.py,sha256=UdZQKIJG40OSzzJ4bdMWix7bpgyZnDAGftF-ksk1VDw,7571
|
|
4
|
-
quickmacapp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
quickmacapp-2025.3.16.dist-info/LICENSE,sha256=7RIBNNvrnKHR3lw9z3KXv3Q6RRjhyxtNQoMnoUsf3_M,1091
|
|
6
|
-
quickmacapp-2025.3.16.dist-info/METADATA,sha256=qkgzp4WyUHJye5Z9RaTNiUXhDKB4bPVJsqwlzneY91Q,1298
|
|
7
|
-
quickmacapp-2025.3.16.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
8
|
-
quickmacapp-2025.3.16.dist-info/top_level.txt,sha256=_iJkekUYnuWhCZbFSQyo2d5_6B7OoPwx7k527bokzeA,12
|
|
9
|
-
quickmacapp-2025.3.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|