-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
110 lines (89 loc) · 3.84 KB
/
Copy pathapp.py
File metadata and controls
110 lines (89 loc) · 3.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""
Hangar: a menu bar home for launching and updating my homebuilt apps.
This file is only the menu bar and wiring. The two hard parts live
elsewhere on purpose: storage.py owns the JSON, runner.py owns the
processes. Read those first and this file is just glue.
Two small pieces of PyObjC are unavoidable on macOS and are marked
GLUE below: hiding the Dock icon, and refreshing the menu each time
it opens (so hand-edits to apps.json show up without a restart).
"""
import functools
import rumps
from AppKit import NSApplication, NSApplicationActivationPolicyAccessory
from Foundation import NSObject
import objc
import storage
import runner
# --- GLUE: rebuild the menu right before it opens ------------------------
# macOS asks a menu's delegate to refresh itself just before display.
# We use that hook so external edits to apps.json appear on next open.
class _MenuRefresher(NSObject):
def initWithApp_(self, app):
self = objc.super(_MenuRefresher, self).init()
if self is None:
return None
self._app = app
return self
def menuNeedsUpdate_(self, menu):
self._app.rebuild_menu()
# ------------------------------------------------------------------------
class HangarApp(rumps.App):
def __init__(self):
# Change the title here to any text or emoji you like.
super().__init__("Hangar", title="🛫")
# GLUE: menu bar only, no Dock icon.
NSApplication.sharedApplication().setActivationPolicy_(
NSApplicationActivationPolicyAccessory
)
self.rebuild_menu() # build once so the first open is populated
# GLUE: attach the refresher to the real NSMenu behind our menu.
self._refresher = _MenuRefresher.alloc().initWithApp_(self)
self.menu._menu.setDelegate_(self._refresher)
def rebuild_menu(self):
"""Throw away the current menu and rebuild it from apps.json."""
self.menu.clear()
entries = storage.load_apps()
rows = []
if not entries:
empty = rumps.MenuItem("(no apps yet -- use Add app...)")
empty.set_callback(None) # greyed out
rows.append(empty)
else:
for index, entry in enumerate(entries):
rows.append(self._build_app_row(index, entry))
rows.append(rumps.separator)
rows.append(rumps.MenuItem("Add app...", callback=self._todo))
rows.append(rumps.MenuItem("Show update log", callback=self._todo))
# rumps adds "Quit Hangar" automatically at the very bottom.
self.menu.update(rows)
def _build_app_row(self, index, entry):
"""One app becomes a submenu: name -> Launch / Update / Edit / Remove."""
row = rumps.MenuItem(entry["name"])
row.add(rumps.MenuItem(
"Launch",
callback=functools.partial(self.launch_entry, entry),
))
if entry.get("update"):
row.add(rumps.MenuItem(
"Update",
callback=self._todo, # real update flow lands next step
))
row.add(rumps.separator)
row.add(rumps.MenuItem("Edit...", callback=self._todo))
row.add(rumps.MenuItem("Remove...", callback=self._todo))
if entry.get("notes"):
note = rumps.MenuItem(entry["notes"])
note.set_callback(None) # greyed out, just a reminder to yourself
row.add(rumps.separator)
row.add(note)
return row
def launch_entry(self, entry, sender=None):
"""Run an entry's launch action, surfacing any failure in a dialog."""
try:
runner.launch(entry)
except Exception as error:
rumps.alert(f"Could not launch {entry['name']}", str(error))
def _todo(self, sender=None):
rumps.alert("Coming in the next step", "Not wired up yet.")
if __name__ == "__main__":
HangarApp().run()