claudometer 1.0.0__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.
menubar_mac.py ADDED
@@ -0,0 +1,153 @@
1
+ """macOS menu-bar adapter (rumps).
2
+
3
+ The menu-bar title shows a colored dot + the "most critical" percent as native
4
+ text; the dropdown holds the full breakdown. Polling runs synchronously on the
5
+ main run loop so all UI mutations stay main-thread-safe (a rare slow request
6
+ briefly delays a tick, which is acceptable for a menu-bar app).
7
+
8
+ Settings: the menu-bar app is a lighter adapter than the Windows widget — it
9
+ does not (yet) implement alerts, resume, cost, fullscreen-hide, theme or accent,
10
+ so its Settings submenu only exposes what it actually honors (which meters to
11
+ show, the poll interval) plus an "Open config file…" shortcut for everything
12
+ else. All of it reads/writes the same ~/.claudometer.toml via settings.load/save.
13
+ """
14
+
15
+ import os
16
+ import subprocess
17
+
18
+ import rumps
19
+
20
+ import usage_core as core
21
+ import settings
22
+ import config
23
+
24
+ DOT = {"green": "🟢", "amber": "🟡", "red": "🔴", "grey": "⚪"}
25
+
26
+
27
+ class MenuApp(rumps.App):
28
+ def __init__(self):
29
+ super().__init__(name="Claudometer", title="…", quit_button=None)
30
+ self._cfg = settings.load()
31
+ self._metrics = list(self._cfg["metrics"])
32
+ self._state = core.PollState()
33
+ self._last_sig = None
34
+ self.menu = ["Loading…"]
35
+ self._timer = rumps.Timer(self._tick, self._cfg["poll"])
36
+ self._timer.start()
37
+ self._tick(None) # immediate first render
38
+
39
+ def _tick(self, _):
40
+ try:
41
+ result = core.poll_once(self._state)
42
+ if isinstance(result, core.Usage):
43
+ disp = core.format_breakdown(result)
44
+ else:
45
+ disp = core.status_display(result)
46
+ except core.CredentialsMissing:
47
+ disp = core.status_display(core.Status.NO_CREDS)
48
+ except Exception:
49
+ disp = core.status_display(core.Status.ERROR)
50
+ self.title = f"{DOT.get(disp['face_color'], '⚪')} {disp['face_pct']}"
51
+ # Rebuild only when the menu content changes — rebuilding every tick
52
+ # leaks rumps callback registrations (they're never pruned).
53
+ sig = (disp.get("plan"), disp.get("session"), disp.get("weekly"),
54
+ tuple(disp.get("models", [])), tuple(self._metrics))
55
+ if sig != self._last_sig:
56
+ self._last_sig = sig
57
+ self._rebuild(disp)
58
+
59
+ def _rebuild(self, disp) -> None:
60
+ rows = []
61
+ if disp.get("plan"):
62
+ rows.append(disp["plan"])
63
+ rows.append(None) # separator
64
+ if "session" in self._metrics and disp.get("session"):
65
+ rows.append(disp["session"])
66
+ if "weekly" in self._metrics and disp.get("weekly"):
67
+ rows.append(disp["weekly"])
68
+ elif disp.get("session"):
69
+ rows.append(disp["session"]) # status/error note — always shown
70
+ seen = {}
71
+ for model in disp.get("models", []):
72
+ row = " " + model
73
+ if row in seen: # rumps dedupes by title; pad so no meter is dropped
74
+ seen[row] += 1
75
+ row += " " * seen[row]
76
+ else:
77
+ seen[row] = 0
78
+ rows.append(row)
79
+ rows.append(None)
80
+ rows.append(self._settings_menu())
81
+ rows.append(rumps.MenuItem("Refresh now", callback=self._refresh))
82
+ rows.append(rumps.MenuItem("View on GitHub", callback=self._github))
83
+ rows.append(rumps.MenuItem("Quit", callback=rumps.quit_application))
84
+ self.menu.clear()
85
+ self.menu = rows
86
+
87
+ # -- settings --------------------------------------------------------- #
88
+ def _settings_menu(self):
89
+ m = rumps.MenuItem("Settings")
90
+ sess = rumps.MenuItem("Show Session meter", callback=self._toggle_session)
91
+ sess.state = "session" in self._metrics
92
+ week = rumps.MenuItem("Show Weekly meter", callback=self._toggle_weekly)
93
+ week.state = "weekly" in self._metrics
94
+ m.update([sess, week, None,
95
+ rumps.MenuItem("Poll interval…", callback=self._set_poll), None,
96
+ rumps.MenuItem("Open config file…", callback=self._open_config)])
97
+ return m
98
+
99
+ def _toggle_session(self, sender):
100
+ self._toggle_metric("session", sender)
101
+
102
+ def _toggle_weekly(self, sender):
103
+ self._toggle_metric("weekly", sender)
104
+
105
+ def _toggle_metric(self, name, sender):
106
+ chosen = set(self._metrics)
107
+ if name in chosen and len(chosen) > 1: # keep at least one meter
108
+ chosen.discard(name)
109
+ else:
110
+ chosen.add(name)
111
+ self._metrics = [x for x in ("session", "weekly") if x in chosen]
112
+ sender.state = name in self._metrics
113
+ self._cfg["metrics"] = list(self._metrics)
114
+ self._save()
115
+ self._tick(None)
116
+
117
+ def _set_poll(self, _):
118
+ resp = rumps.Window("Seconds between usage updates (60–300):", "Poll interval",
119
+ default_text=str(self._cfg["poll"]), ok="Save",
120
+ cancel="Cancel", dimensions=(120, 22)).run()
121
+ if not resp.clicked:
122
+ return
123
+ try:
124
+ v = max(60, min(300, int(resp.text.strip())))
125
+ except ValueError:
126
+ return
127
+ self._cfg["poll"] = v
128
+ self._save()
129
+ self._timer.stop()
130
+ self._timer = rumps.Timer(self._tick, v)
131
+ self._timer.start()
132
+
133
+ def _open_config(self, _):
134
+ path = str(settings.config_path())
135
+ if not os.path.exists(path):
136
+ self._save() # materialize the file so there's something to edit
137
+ subprocess.Popen(["open", path])
138
+
139
+ def _save(self):
140
+ try:
141
+ settings.save(self._cfg)
142
+ except Exception:
143
+ pass
144
+
145
+ def _github(self, _):
146
+ import webbrowser
147
+ webbrowser.open(config.REPO_URL)
148
+
149
+ def _refresh(self, _):
150
+ self._tick(None)
151
+
152
+ def run(self) -> None:
153
+ super().run()