claude-code-usage 1.0.3__py3-none-any.whl → 2.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.
src/tray.py CHANGED
@@ -1,25 +1,26 @@
1
1
  """
2
2
  System tray indicator for Claude Code usage overlay.
3
3
 
4
+ Cross-platform: uses pystray for Windows, macOS, and Linux.
5
+
4
6
  Displays usage data via:
5
- - System tray icon with color-coded gauge (green/yellow/orange/red)
6
- - Panel label showing compact status (e.g., "45%|67%")
7
+ - System tray icon with color-coded gauge (green/yellow/red)
8
+ - Tooltip showing compact status (e.g., "Session: 45% | Weekly: 67%")
7
9
  - Dropdown menu with detailed usage info and reset times
8
10
 
9
- Note: Hover tooltips don't work on GNOME Shell due to AppIndicator limitations.
10
- See KNOWN_ISSUES.md for details.
11
+ Note: Panel labels (text next to icon) are not supported by pystray.
12
+ Tooltip is used instead for quick status display.
11
13
  """
12
- import gi
13
-
14
- gi.require_version('AyatanaAppIndicator3', '0.1')
15
- gi.require_version('Gtk', '3.0')
16
- from gi.repository import AyatanaAppIndicator3 as AppIndicator3, Gtk, GLib
17
14
  import sys
15
+ import threading
16
+ from typing import Optional
17
+
18
+ import pystray
18
19
 
19
20
  from src.icon_generator import generate_gauge_icon
20
21
  from src.utils import format_time_until
21
22
  from src.config import get_access_token, UserConfig, get_config_path
22
- from src.api import fetch_with_retry, APIError, AuthenticationError
23
+ from src.api import fetch_with_retry, APIError, AuthenticationError, UsageData
23
24
  from src.notifier import UsageNotifier
24
25
  from src.autostart import create_autostart_entry, is_autostart_enabled, remove_autostart_entry
25
26
 
@@ -27,14 +28,12 @@ from src.autostart import create_autostart_entry, is_autostart_enabled, remove_a
27
28
  class TrayIndicator:
28
29
  """System tray indicator for Claude Code usage."""
29
30
 
30
- APPINDICATOR_ID = 'claude-usage-overlay'
31
-
32
31
  def __init__(self):
33
32
  """Initialize the tray indicator."""
34
- self.usage_data = None
35
- self.indicator = None
36
- self.menu = None
37
- self.timer_id = None
33
+ self.usage_data: Optional[UsageData] = None
34
+ self.icon: Optional[pystray.Icon] = None
35
+ self._stop_event = threading.Event()
36
+ self._update_thread: Optional[threading.Thread] = None
38
37
 
39
38
  # Load configuration
40
39
  self.config = UserConfig.load()
@@ -47,42 +46,71 @@ class TrayIndicator:
47
46
 
48
47
  self._setup_indicator()
49
48
 
50
- # Defer initial update and timer start until GTK main loop is running
51
- # This ensures the UI is fully initialized before we update it
52
- GLib.idle_add(self._initial_update)
53
-
54
49
  def _setup_indicator(self):
55
- """Set up the AppIndicator with initial icon and menu."""
56
- # Create initial icon at 0% fill with green color (both 0%)
50
+ """Set up the pystray icon with initial state."""
51
+ # Create initial icon at 0% fill with green color
57
52
  initial_icon = generate_gauge_icon(0, 0)
58
53
 
59
- # Create AppIndicator
60
- self.indicator = AppIndicator3.Indicator.new(
61
- self.APPINDICATOR_ID,
62
- initial_icon,
63
- AppIndicator3.IndicatorCategory.APPLICATION_STATUS
54
+ # Create pystray Icon
55
+ self.icon = pystray.Icon(
56
+ name="claude-usage-overlay",
57
+ icon=initial_icon,
58
+ title="Claude Usage: Loading...",
59
+ menu=self._build_menu()
64
60
  )
65
- self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
66
-
67
- # Set initial label while loading
68
- self.indicator.set_label("...", "100%|100%")
69
61
 
70
- # Build initial menu with loading message
71
- menu = Gtk.Menu()
72
- loading_item = Gtk.MenuItem(label="Loading...")
73
- loading_item.set_sensitive(False)
74
- menu.append(loading_item)
75
- menu.show_all()
76
-
77
- self.menu = menu
78
- self.indicator.set_menu(menu)
79
-
80
- def _update_usage(self) -> bool:
81
- """Fetch usage data and refresh display.
62
+ def _build_menu(self) -> pystray.Menu:
63
+ """Build the dropdown menu with usage info and actions.
82
64
 
83
65
  Returns:
84
- bool: GLib.SOURCE_CONTINUE to keep timer running
66
+ pystray.Menu: The constructed menu
85
67
  """
68
+ # Usage line (shows current percentages or loading state)
69
+ if self.usage_data is not None:
70
+ session_percent = self.usage_data.session_percent
71
+ weekly_percent = self.usage_data.weekly_percent
72
+ usage_text = f"Session: {session_percent:.0f}% | Weekly: {weekly_percent:.0f}%"
73
+ reset_time_str = format_time_until(self.usage_data.session_resets_at)
74
+ reset_text = f"Resets in {reset_time_str}"
75
+ else:
76
+ usage_text = "Loading..."
77
+ reset_text = ""
78
+
79
+ menu_items = [
80
+ pystray.MenuItem(usage_text, None, enabled=False),
81
+ ]
82
+
83
+ if reset_text:
84
+ menu_items.append(pystray.MenuItem(reset_text, None, enabled=False))
85
+
86
+ menu_items.extend([
87
+ pystray.Menu.SEPARATOR,
88
+ pystray.MenuItem("Refresh", self._on_refresh_clicked),
89
+ pystray.MenuItem(
90
+ "Settings",
91
+ pystray.Menu(
92
+ pystray.MenuItem(
93
+ "Pause Notifications",
94
+ self._on_pause_toggled,
95
+ checked=lambda item: self.config.pause_notifications
96
+ ),
97
+ pystray.MenuItem(
98
+ "Autostart on Login",
99
+ self._on_autostart_toggled,
100
+ checked=lambda item: is_autostart_enabled()
101
+ ),
102
+ pystray.Menu.SEPARATOR,
103
+ pystray.MenuItem("Edit Config File...", self._on_edit_config_clicked),
104
+ )
105
+ ),
106
+ pystray.Menu.SEPARATOR,
107
+ pystray.MenuItem("Quit", self._on_quit_clicked),
108
+ ])
109
+
110
+ return pystray.Menu(*menu_items)
111
+
112
+ def _update_usage(self):
113
+ """Fetch usage data and refresh display."""
86
114
  try:
87
115
  token = get_access_token()
88
116
  self.usage_data = fetch_with_retry(token)
@@ -91,53 +119,18 @@ class TrayIndicator:
91
119
  except AuthenticationError as e:
92
120
  error_msg = f"Authentication failed: {e}"
93
121
  print(error_msg, file=sys.stderr)
94
- self.indicator.set_title(error_msg)
122
+ if self.icon:
123
+ self.icon.title = error_msg
95
124
 
96
125
  except (APIError, ValueError, FileNotFoundError) as e:
97
126
  error_msg = f"Error: {e}"
98
127
  print(error_msg, file=sys.stderr)
99
- self.indicator.set_title(error_msg)
100
-
101
- # Return GLib.SOURCE_CONTINUE to keep timer running
102
- return GLib.SOURCE_CONTINUE
103
-
104
- def _initial_update(self):
105
- """Perform initial update after GTK main loop starts.
106
-
107
- Sets loading label, then defers the API call to give GTK time to render.
108
-
109
- Returns:
110
- GLib.SOURCE_REMOVE to run only once
111
- """
112
- # Set loading label now that main loop is running
113
- self.indicator.set_label("...", "100%|100%")
114
-
115
- # Defer API call with small timeout so GTK can render the loading label
116
- GLib.timeout_add(50, self._do_initial_fetch) # 50ms delay
117
- return GLib.SOURCE_REMOVE
118
-
119
- def _do_initial_fetch(self):
120
- """Fetch initial data and start update timer.
121
-
122
- Returns:
123
- GLib.SOURCE_REMOVE to run only once
124
- """
125
- self._update_usage()
126
- self._start_update_timer()
127
- return GLib.SOURCE_REMOVE
128
-
129
- def _start_update_timer(self):
130
- """Start or restart periodic update timer with config interval."""
131
- if self.timer_id is not None:
132
- GLib.source_remove(self.timer_id)
133
- self.timer_id = GLib.timeout_add_seconds(
134
- self.config.polling_interval,
135
- self._update_usage
136
- )
128
+ if self.icon:
129
+ self.icon.title = error_msg
137
130
 
138
131
  def _refresh_display(self):
139
132
  """Update icon, tooltip, and menu based on current usage data."""
140
- if self.usage_data is None:
133
+ if self.usage_data is None or self.icon is None:
141
134
  return
142
135
 
143
136
  session_percent = self.usage_data.session_percent
@@ -147,32 +140,25 @@ class TrayIndicator:
147
140
  # Color shows worst-case urgency (max of session and weekly)
148
141
  worst_case_percent = max(session_percent, weekly_percent)
149
142
 
150
- # Generate icon with separate percentage (arc fill) and color
151
- icon_path = generate_gauge_icon(session_percent, worst_case_percent)
152
- self.indicator.set_icon_full(icon_path, 'Claude usage gauge')
143
+ # Generate new icon image
144
+ new_icon = generate_gauge_icon(session_percent, worst_case_percent)
145
+ self.icon.icon = new_icon
153
146
 
154
- # Update accessibility label (for screen readers, not displayed as tooltip on GNOME)
147
+ # Update tooltip (shown on hover)
155
148
  tooltip = f"Session: {session_percent:.0f}% | Weekly: {weekly_percent:.0f}%"
156
- self.indicator.set_title(tooltip)
157
-
158
- # Display compact status in panel (next to icon)
159
- # Note: AppIndicator on GNOME Shell doesn't show hover tooltips (known limitation).
160
- # This label appears as text in the panel for quick visual reference.
161
- compact_label = f"{session_percent:.0f}%|{weekly_percent:.0f}%"
162
- self.indicator.set_label(compact_label, "100%|100%") # guide string for sizing
149
+ self.icon.title = tooltip
163
150
 
164
151
  # Rebuild menu with current data
165
- self._build_menu()
152
+ self.icon.menu = self._build_menu()
166
153
 
167
154
  # Pass pause flag to notifier before check_and_notify
168
155
  self.notifier.pause_notifications = self.config.pause_notifications
169
156
 
170
157
  # Check thresholds and show notifications if needed
171
- # Convert datetime to timestamp for notifier
172
158
  session_reset_ts = (self.usage_data.session_resets_at.timestamp()
173
- if self.usage_data.session_resets_at else 0)
159
+ if self.usage_data.session_resets_at else 0)
174
160
  weekly_reset_ts = (self.usage_data.weekly_resets_at.timestamp()
175
- if self.usage_data.weekly_resets_at else 0)
161
+ if self.usage_data.weekly_resets_at else 0)
176
162
 
177
163
  self.notifier.check_and_notify(
178
164
  session_percent,
@@ -181,122 +167,65 @@ class TrayIndicator:
181
167
  weekly_reset_ts
182
168
  )
183
169
 
184
- def _build_menu(self) -> Gtk.Menu:
185
- """Build the dropdown menu with usage info and actions.
186
-
187
- Returns:
188
- Gtk.Menu: The constructed menu
189
- """
190
- menu = Gtk.Menu()
170
+ def _update_loop(self):
171
+ """Background thread that periodically updates usage data."""
172
+ # Initial update
173
+ self._update_usage()
191
174
 
192
- # Add usage line (non-clickable)
193
- session_percent = self.usage_data.session_percent
194
- weekly_percent = self.usage_data.weekly_percent
195
- usage_text = f"Session: {session_percent:.0f}% | Weekly: {weekly_percent:.0f}%"
196
- usage_item = Gtk.MenuItem(label=usage_text)
197
- usage_item.set_sensitive(False)
198
- menu.append(usage_item)
199
-
200
- # Add reset time line (non-clickable)
201
- # Use session_resets_at (5-hour is more relevant for active users)
202
- reset_time_str = format_time_until(self.usage_data.session_resets_at)
203
- reset_item = Gtk.MenuItem(label=f"Resets in {reset_time_str}")
204
- reset_item.set_sensitive(False)
205
- menu.append(reset_item)
206
-
207
- # Add separator
208
- menu.append(Gtk.SeparatorMenuItem())
209
-
210
- # Add Refresh item
211
- # Note: AppIndicator menus don't support GTK accelerators (no window to attach AccelGroup)
212
- # Show the shortcut hint in the label instead
213
- refresh_item = Gtk.MenuItem(label="Refresh")
214
- refresh_item.connect('activate', self._on_refresh_clicked)
215
- menu.append(refresh_item)
216
-
217
- # Add Settings submenu
218
- settings_item = Gtk.MenuItem(label="Settings")
219
- settings_submenu = Gtk.Menu()
220
-
221
- # Pause Notifications checkbox
222
- pause_item = Gtk.CheckMenuItem(label="Pause Notifications")
223
- pause_item.set_active(self.config.pause_notifications)
224
- pause_item.connect('activate', self._on_pause_toggled)
225
- settings_submenu.append(pause_item)
226
-
227
- # Autostart on Login checkbox
228
- autostart_item = Gtk.CheckMenuItem(label="Autostart on Login")
229
- autostart_item.set_active(is_autostart_enabled())
230
- autostart_item.connect('activate', self._on_autostart_toggled)
231
- settings_submenu.append(autostart_item)
232
-
233
- # Separator
234
- settings_submenu.append(Gtk.SeparatorMenuItem())
235
-
236
- # Edit Config File item
237
- edit_config_item = Gtk.MenuItem(label="Edit Config File...")
238
- edit_config_item.connect('activate', self._on_edit_config_clicked)
239
- settings_submenu.append(edit_config_item)
240
-
241
- settings_item.set_submenu(settings_submenu)
242
- menu.append(settings_item)
243
-
244
- # Add separator
245
- menu.append(Gtk.SeparatorMenuItem())
246
-
247
- # Add Quit item
248
- quit_item = Gtk.MenuItem(label="Quit")
249
- quit_item.connect('activate', self._on_quit_clicked)
250
- menu.append(quit_item)
251
-
252
- # Show all menu items
253
- menu.show_all()
254
-
255
- # Update instance menu and indicator
256
- self.menu = menu
257
- self.indicator.set_menu(menu)
258
-
259
- return menu
260
-
261
- def _on_refresh_clicked(self, widget):
262
- """Handle Refresh menu item click.
263
-
264
- Uses GLib.idle_add to defer the update until after the menu closes,
265
- avoiding GTK crashes from rebuilding the menu during event handling.
266
- """
267
- def do_refresh():
175
+ # Periodic updates
176
+ while not self._stop_event.wait(timeout=self.config.polling_interval):
268
177
  self._update_usage()
269
- return GLib.SOURCE_REMOVE # Run only once, don't repeat
270
- GLib.idle_add(do_refresh)
271
178
 
272
- def _on_pause_toggled(self, widget):
179
+ def _on_refresh_clicked(self, icon=None, item=None):
180
+ """Handle Refresh menu item click."""
181
+ # Run update in background thread to avoid blocking UI
182
+ threading.Thread(target=self._update_usage, daemon=True).start()
183
+
184
+ def _on_pause_toggled(self, icon=None, item=None):
273
185
  """Handle Pause Notifications toggle."""
274
- self.config.pause_notifications = widget.get_active()
186
+ self.config.pause_notifications = not self.config.pause_notifications
275
187
  self.config.save()
276
188
  self.notifier.pause_notifications = self.config.pause_notifications
277
189
 
278
- def _on_autostart_toggled(self, widget):
190
+ def _on_autostart_toggled(self, icon=None, item=None):
279
191
  """Handle Autostart toggle."""
280
- if widget.get_active():
281
- create_autostart_entry(True)
282
- else:
192
+ if is_autostart_enabled():
283
193
  remove_autostart_entry()
284
- self.config.autostart_enabled = widget.get_active()
194
+ self.config.autostart_enabled = False
195
+ else:
196
+ create_autostart_entry(True)
197
+ self.config.autostart_enabled = True
285
198
  self.config.save()
286
199
 
287
- def _on_edit_config_clicked(self, widget):
288
- """Open config file in default editor."""
200
+ def _on_edit_config_clicked(self, icon=None, item=None):
201
+ """Open config file in default editor/viewer."""
202
+ import os
289
203
  import subprocess
204
+
290
205
  config_path = get_config_path()
291
206
  # Ensure file exists with defaults
292
207
  if not config_path.exists():
293
208
  self.config.save()
294
- subprocess.Popen(["/usr/bin/xdg-open", str(config_path)])
295
209
 
296
- def _on_quit_clicked(self, widget):
210
+ # Platform-specific file opening
211
+ if sys.platform == 'win32':
212
+ os.startfile(config_path)
213
+ elif sys.platform == 'darwin':
214
+ subprocess.run(['open', str(config_path)])
215
+ else:
216
+ subprocess.run(['xdg-open', str(config_path)])
217
+
218
+ def _on_quit_clicked(self, icon=None, item=None):
297
219
  """Handle Quit menu item click."""
298
- Gtk.main_quit()
220
+ self._stop_event.set()
221
+ if self.icon:
222
+ self.icon.stop()
299
223
 
300
224
  def run(self):
301
- """Start the GTK main loop."""
302
- Gtk.main()
225
+ """Start the tray indicator."""
226
+ # Start background update thread
227
+ self._update_thread = threading.Thread(target=self._update_loop, daemon=True)
228
+ self._update_thread.start()
229
+
230
+ # Run pystray main loop (blocks until icon.stop() is called)
231
+ self.icon.run()
@@ -1,15 +0,0 @@
1
- claude_code_usage-1.0.3.dist-info/licenses/LICENSE,sha256=alXcUJ2-k8eqG_Amt8y9Hhlb8fT1nABafZOqWDmdzUw,1060
2
- src/__init__.py,sha256=rzeXYIMbNbWPF6gpjv7luGXLZA67r_5GU5ZGUNb8pKA,27
3
- src/api.py,sha256=IymlJFR7_Ou5I_Asij8oyR2b7bx9WurBtREwcysbiJQ,6168
4
- src/autostart.py,sha256=Xo0JcjUJ8RZ-pervQ6f8mQAxngO73Rf-IDdg0uWzlKw,3369
5
- src/config.py,sha256=joQwr-XwExNB3vVg2GcEVxBAEs0y1D7m3T6bJBCQMwA,5395
6
- src/icon_generator.py,sha256=h1xJtzjjJp958ulmsla5DOyutOVTR1Q_0T2ymlO1IEk,3537
7
- src/main.py,sha256=A7Ej5kqbtOYWUGSuZxx6bM-7WNimjjAs--04_kvvta0,506
8
- src/notifier.py,sha256=3j5ZmhaFMfv5XDLMCFaOgSCCw5jffn4N6dNz2R7gd8I,10267
9
- src/tray.py,sha256=sCvQLsNdm3uBtdeNGzFmLxzLT1h1w_LH4ZsajZIlg4I,10995
10
- src/utils.py,sha256=ZqGvw-oyzHompxRMUeYT5FdJFaQ6uvoAthhu-3bPGJk,893
11
- claude_code_usage-1.0.3.dist-info/METADATA,sha256=ZvqJB-uNiJ-x_EeWA_IxeZWqUmvfsBp362jRMDUiNqU,5568
12
- claude_code_usage-1.0.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
13
- claude_code_usage-1.0.3.dist-info/entry_points.txt,sha256=MY_VQ3t41-QKCQuBjBsd4ug0uOUlCz1Hz-OyK78gfns,47
14
- claude_code_usage-1.0.3.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
15
- claude_code_usage-1.0.3.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- claude-usage = src.main:main