- Desktop widget (Python/pystray): system tray icon showing 5h usage as circular progress bar with Claude starburst logo, 10-step green-to-red color scale, right-click menu with usage stats and configuration - Shared cache: both widget and CLI statusline read/write the same /tmp/claude_usage.json — only one fetcher needs to run - Installer wizard (install_wizard.py): interactive cross-platform setup with component selection, session key prompt, cron/autostart config - OS wrappers: install.sh (Linux/macOS) and install.ps1 (Windows) find Python 3.9+ and launch the wizard - README with topology diagram, usage docs, and configuration reference Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
92 lines
2.5 KiB
Python
92 lines
2.5 KiB
Python
"""Main orchestrator: tray icon, state, threads."""
|
|
|
|
import logging
|
|
import sys
|
|
import threading
|
|
import tkinter as tk
|
|
from tkinter import simpledialog
|
|
|
|
import pystray
|
|
|
|
from . import config
|
|
from .fetcher import UsageFetcher
|
|
from .menu import build_menu
|
|
from .renderer import render_icon
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class App:
|
|
def __init__(self):
|
|
self.usage_data = {}
|
|
self._lock = threading.Lock()
|
|
self.fetcher = UsageFetcher(on_update=self._on_usage_update)
|
|
self._icon = pystray.Icon(
|
|
name="claude-usage",
|
|
icon=render_icon(0),
|
|
title="Claude Usage Widget",
|
|
menu=build_menu(self),
|
|
)
|
|
|
|
def run(self):
|
|
"""Start fetcher and run tray icon (blocks until quit)."""
|
|
self.fetcher.start()
|
|
self._icon.run()
|
|
|
|
def _on_usage_update(self, data):
|
|
"""Callback from fetcher thread — update icon + menu."""
|
|
with self._lock:
|
|
self.usage_data = data
|
|
|
|
pct = data.get("five_hour_pct", 0) if not data.get("error") else 0
|
|
try:
|
|
self._icon.icon = render_icon(pct)
|
|
# Force menu rebuild for dynamic text
|
|
self._icon.update_menu()
|
|
except Exception:
|
|
pass # icon not yet visible
|
|
|
|
# Update tooltip
|
|
if data.get("error"):
|
|
self._icon.title = f"Claude Usage: {data['error']}"
|
|
else:
|
|
self._icon.title = f"Claude Usage: {data.get('five_hour_pct', 0)}%"
|
|
|
|
def on_refresh(self):
|
|
self.fetcher.refresh()
|
|
|
|
def on_set_interval(self, seconds):
|
|
self.fetcher.set_interval(seconds)
|
|
|
|
def on_session_key(self):
|
|
"""Show a simple dialog to enter/update the session key."""
|
|
threading.Thread(target=self._session_key_dialog, daemon=True).start()
|
|
|
|
def _session_key_dialog(self):
|
|
root = tk.Tk()
|
|
root.withdraw()
|
|
current = config.get_session_key()
|
|
key = simpledialog.askstring(
|
|
"Claude Session Key",
|
|
"Paste your claude.ai sessionKey cookie value:",
|
|
initialvalue=current,
|
|
parent=root,
|
|
)
|
|
root.destroy()
|
|
if key is not None and key.strip():
|
|
config.set_session_key(key.strip())
|
|
self.fetcher.refresh()
|
|
|
|
def on_quit(self):
|
|
self.fetcher.stop()
|
|
self._icon.stop()
|
|
|
|
|
|
def main():
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
)
|
|
app = App()
|
|
app.run()
|