Add desktop tray widget + installer wizard
- 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>
This commit is contained in:
91
claude_usage_widget/app.py
Normal file
91
claude_usage_widget/app.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user