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.
- app.py +54 -0
- claudometer-1.0.0.dist-info/METADATA +345 -0
- claudometer-1.0.0.dist-info/RECORD +16 -0
- claudometer-1.0.0.dist-info/WHEEL +5 -0
- claudometer-1.0.0.dist-info/entry_points.txt +2 -0
- claudometer-1.0.0.dist-info/licenses/LICENSE +21 -0
- claudometer-1.0.0.dist-info/top_level.txt +10 -0
- config.py +35 -0
- cost.py +121 -0
- menubar_mac.py +153 -0
- render.py +587 -0
- resume.py +145 -0
- settings.py +287 -0
- tray_windows.py +146 -0
- usage_core.py +642 -0
- widget_bar.py +1326 -0
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()
|