From 8bfb6831187820fdf1d6d3093ff870ef973005be Mon Sep 17 00:00:00 2001 From: psychofict Date: Wed, 6 May 2026 11:22:21 +0900 Subject: [PATCH] feat: multi-account, configure window, escalating notifications, polished installer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds nine features to the widget. Single-account behaviour is preserved when only one account is configured. Features: - Multi-account: tray label aggregates across configured accounts (Work:67% Personal:12%); per-account Claude config dir; per-account hide-from-tray and disable-polling flags. - Configure window: tray menu → Configure… opens a tabbed Gtk window for editing accounts, thresholds, poll interval, burn-rate alerts. Live-applies, no restart. - Notification escalation: warn → critical → 100% tiers per window per account, persisted across restarts so a mid-window relaunch doesn't re-fire alerts. Re-arms when the window's resets_at shifts. - Burn-rate alerts: configurable multiplier, 8-hour grace at start of new 7d window to avoid false alarms on low-elapsed-time data. - Graceful failure UX: ! for auth/network/rate errors, ? for the brief gap between rollover and fresh data; last-known % preserved so transient blips don't blank the indicator. - Compact reset times: countdown (2h 15m) when polling, absolute (9:00P / Th 7:00P) when polling is disabled for that account. - Interactive installer: asks how many accounts to monitor, gathers label + claude_dir + creds.json check per account. - pyenv detection: --copies venv that survives pyenv version switches; PyGObject pinned <3.51 on Ubuntu 22.04. - Uninstaller: explicit prompt before removing user config. Architecture changes: - Split out shared.py (config + utility functions), config_window.py (Configure UI), usage_popup.py (details popup) to keep claude_usage_widget.py focused on the indicator + poll loop. --- .gitignore | 1 + README.md | 290 ++--- claude_usage_widget.py | 1082 ++++++++--------- config_window.py | 452 +++++++ install.sh | 220 +++- screenshots/screenshot_alert.png | Bin 0 -> 8149 bytes screenshots/screenshot_config_accounts.png | Bin 0 -> 22475 bytes .../screenshot_config_notifications.png | Bin 0 -> 29426 bytes screenshots/screenshot_popup.png | Bin 0 -> 16733 bytes screenshots/screenshot_tray.png | Bin 0 -> 28672 bytes shared.py | 101 ++ uninstall.sh | 59 +- usage_popup.py | 215 ++++ 13 files changed, 1647 insertions(+), 773 deletions(-) create mode 100644 config_window.py create mode 100644 screenshots/screenshot_alert.png create mode 100644 screenshots/screenshot_config_accounts.png create mode 100644 screenshots/screenshot_config_notifications.png create mode 100644 screenshots/screenshot_popup.png create mode 100644 screenshots/screenshot_tray.png create mode 100644 shared.py create mode 100644 usage_popup.py diff --git a/.gitignore b/.gitignore index 8bb204b..2fe5f95 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ ENV/ # IDE .vscode/ .idea/ +.claude/ *.swp *.swo *~ diff --git a/README.md b/README.md index 9078b07..3ed4b9c 100644 --- a/README.md +++ b/README.md @@ -1,249 +1,199 @@ -# ⚡ Claude AI Usage Widget — Linux Taskbar +# ⚡ Claude AI Usage Widget — Linux Taskbar (Multi-Account Fork) -A lightweight system tray widget that shows your Claude AI subscription usage percentage (5-hour and 7-day rate limit windows) directly in your Linux taskbar. +> **Fork of [StaticB1/claude_ai_usage_widget](https://github.com/StaticB1/claude_ai_usage_widget)** -![Claude Usage Widget Screenshot](screenshot.png) - +A lightweight system tray widget that shows your Claude AI subscription usage (5h and 7d rate limit windows) directly in your Linux taskbar. Supports multiple accounts and is fully configurable via a built-in UI — no config file editing required. -## Quick Start +| Tray menu | Details popup | +|---|---| +| ![Tray menu](screenshots/screenshot_tray.png) | ![Details popup](screenshots/screenshot_popup.png) | -```bash -# Install dependencies -sudo apt install python3 python3-gi gir1.2-appindicator3-0.1 gir1.2-notify-0.7 +## Features -# Install widget -git clone https://github.com/StaticB1/claude_ai_usage_widget.git && cd claude_ai_usage_widget -chmod +x install.sh && ./install.sh +- **Multiple accounts** — tray label shows `Work:67% Personal:12%` for all accounts at a glance; individual accounts can be hidden from the tray while still appearing in the dropdown and popup +- **Redesigned popup** — two-column table layout showing 5h and 7d side-by-side with inline reset times (`72% — 2h 15m`) +- **Configure window** — edit accounts, thresholds, burn rate alerts, and poll interval live from the tray menu (no config file editing needed) +- **Burn rate alerts** — warns when your 7d usage pace suggests you'll exceed your weekly allocation (e.g. 50% used with only 25% of the week elapsed) +- **Configurable notifications** — set your own warn/critical thresholds (defaults: 60% / 85%) +- **Per-account polling control** — disable background auto-refresh per account (e.g. keep personal accounts polling, skip a work account). The widget always fetches all accounts once on launch and "Refresh Now" always fetches everything. When polling is disabled for an account, reset times switch from a countdown (`2h 15m`) to the actual reset time (`9:00P` for 5h, `Th 7:00P` for 7d) +- **Configurable poll interval** — change how often the widget checks (default: 5 min) +- **Config-driven accounts** — `~/.config/claude-usage-widget/config.json` lists each account's label and Claude Code config dir +- **Graceful failures** — if one account errors it shows `Work:!`; if a period just rolled over and the API hasn't returned fresh data yet it shows `Work:?`; in between resets the last known value is preserved instead of flashing an error +- **Interactive install** — `install.sh` asks how many accounts you want and where their credentials are +- **pyenv support** — installer detects pyenv and creates an isolated venv so the widget survives Python version switches -# Start widget +--- + +## Quick Start + +```bash +git clone https://github.com/gqcorneby/claude_ai_usage_widget.git +cd claude_ai_usage_widget +./install.sh claude-widget-start ``` -## Features - -- **Taskbar percentage** — shows your 5h usage % at a glance with color-coded icon -- **Click for details** — popup with 5h + 7d utilization, progress bars, reset timers, and subscription plan -- **Extra usage tracking** — displays pay-as-you-go monthly credit usage if enabled on your account -- **Threshold notifications** — desktop alerts at startup, 75%, 90%, and 100% usage -- **Auto-refresh** — polls every 2 minutes (configurable) -- **Auto-detect credentials** — reads Claude Code's `~/.claude/.credentials.json` on Linux -- **Manual token entry** — dialog for manual OAuth token if you don't use Claude Code -- **Autostart** — installs a `.desktop` entry for autostart on login +The installer will ask how many accounts to monitor and where each one's Claude Code config directory is. ## Requirements - Linux with GTK3 (GNOME, KDE, XFCE, etc.) - Python 3.10+ -- System packages: - ``` - sudo apt install python3 python3-gi gir1.2-appindicator3-0.1 gir1.2-notify-0.7 - ``` +- `gir1.2-appindicator3-0.1`, `gir1.2-notify-0.7` (installer handles these) ## Install ```bash -git clone https://github.com/StaticB1/claude_ai_usage_widget.git && cd claude_ai_usage_widget -chmod +x install.sh ./install.sh ``` -Then run: -```bash -claude-widget-start -``` +The installer will: +1. Detect pyenv or fall back to system Python +2. Install system GI libraries via apt +3. Create an isolated venv (survives pyenv version changes) +4. Ask you to configure your accounts interactively +5. Set up autostart on login -It will autostart on next login. +``` +▸ Setting up accounts… + How many accounts do you want to monitor? [1]: 2 + + — Account 1 of 2 — + Label [Account1]: Work + Claude config dir [~/.claude]: ~/.claude/work + ✓ Found credentials + + — Account 2 of 2 — + Label [Account2]: Personal + Claude config dir [~/.claude]: ~/.claude + ✓ Found credentials +``` ## Usage ```bash -claude-widget-start # Start the widget -claude-widget-stop # Stop the widget +claude-widget-start # Start +claude-widget-stop # Stop ``` -The widget runs in the background and displays in your system tray. +The tray label updates every 5 minutes by default. Click for the full breakdown popup; right-click for the menu. -**Check if running:** -```bash -ps aux | grep '[c]laude_usage_widget' -``` +## What's Displayed Where -## Getting Your OAuth Token +| Location | Shows | +|---|---| +| **Tray label** | 5h usage % per account — `Work:67% Personal:12%`; `?` if the period just reset and fresh data hasn't arrived yet; `!` on auth/network error | +| **Dropdown menu** | 7d usage %, burn rate, 5h reset time — `Work: 45% ↑1.8× ↺ 1h 20m` (or `↺ 9:00P` when polling is disabled for that account) | +| **Details popup (5h column)** | Progress bar, 5h % and reset time (`2h 15m` or `9:00P`) | +| **Details popup (7d column)** | Progress bar, 7d % and reset time (`3d 4h` or `Th 7:00P`), burn rate pace (`↑1.8×` / `↓0.3×`) | -### Option A: Claude Code (automatic) +The tray gives you the quick hourly glance; the dropdown shows weekly pace at a glance; the popup has the full picture. -If you have [Claude Code](https://code.claude.com) installed and logged in: +### Notifications -```bash -claude login # if not already -``` +![Alert notification](screenshots/screenshot_alert.png) -The widget auto-reads `~/.claude/.credentials.json` — no extra config needed. +| Type | Triggers | Urgency | +|---|---|---| +| Usage threshold — warn | 5h or 7d hits warn % (default 60%) | Normal | +| Usage threshold — critical | 5h or 7d hits critical % (default 85%) | Critical | +| Burn rate — early | 7d pace exceeds multiplier before warn % | Normal | +| Burn rate — warn | 7d pace exceeds multiplier at warn % | Normal | +| Burn rate — critical | 7d pace exceeds multiplier at critical % | Critical | -### Option B: Browser DevTools (manual) +#### Window reset behaviour -1. Open https://claude.ai and log in -2. Open DevTools → **Network** tab -3. Send a message, then filter requests for `api.anthropic.com` -4. Find the `Authorization: Bearer sk-ant-oat01-...` header -5. Copy the full token starting with `sk-ant-oat01-` -6. Enter it via the widget's **Set Token…** menu item +Threshold and burn rate notifications each track the API's `resets_at` timestamp for their respective windows. When that timestamp shifts (i.e. the window rolled over), the escalation level resets to zero and the full warn → critical → 100% sequence can fire again. -The token is saved to `~/.config/claude-usage-widget/config.json` (mode 600). +- **5h threshold**: resets when the 5h `resets_at` shifts by more than 1 hour +- **7d threshold**: resets when the 7d `resets_at` shifts by more than 6 hours +- **Burn rate**: resets when the 7d `resets_at` shifts by more than 6 hours; additionally skips the first ~8 hours of each new window to avoid false alarms on low-elapsed-time data -## How It Works +State is persisted to `~/.config/claude-usage-widget/notification_state.json`, so restarting the widget mid-window will not re-fire notifications that already fired for the current window. -Uses the same internal API endpoint Claude Code uses: +## Configuration -``` -GET https://api.anthropic.com/api/oauth/usage -Authorization: Bearer -anthropic-beta: oauth-2025-04-20 -``` +The easiest way is via the tray menu → **Configure...**: -Returns: -```json -{ - "five_hour": { "utilization": 10.0, "resets_at": "2026-02-19T05:00:00Z" }, - "seven_day": { "utilization": 2.0, "resets_at": "2026-02-24T08:00:00Z" }, - "extra_usage": { - "is_enabled": true, - "monthly_limit": 2000, - "used_credits": 500.0, - "utilization": 25.0 - } -} -``` +| Accounts tab | Notifications tab | +|---|---| +| ![Config accounts](screenshots/screenshot_config_accounts.png) | ![Config notifications](screenshots/screenshot_config_notifications.png) | -The widget handles all three sections. `extra_usage` is shown only when `is_enabled` is true. +- **Accounts tab** — add, edit, or remove accounts (label + credentials directory); check **Hide tray** to exclude an account from the tray label; check **No poll** to disable background auto-refresh for that account +- **Notifications tab** — set the poll interval, warn/critical thresholds, and burn rate alert -## Configuration +Changes take effect immediately without restarting the widget. -Edit `~/.config/claude-usage-widget/config.json`: +The config is stored at `~/.config/claude-usage-widget/config.json` and can also be edited directly: ```json { - "oauth_token": "sk-ant-oat01-..." + "accounts": [ + { "label": "Work", "credentials_dir": "~/.claude/work", "disable_polling": true }, + { "label": "Personal", "credentials_dir": "~/.claude", "hide_from_tray": true } + ], + "poll_interval_seconds": 300, + "thresholds": { "warn": 60, "critical": 85 }, + "burn_rate": { "enabled": false, "multiplier": 1.5 } } ``` -To change refresh interval, edit `REFRESH_INTERVAL_SEC` in the Python script (default: 120s). +Each `credentials_dir` must contain a `.credentials.json` file from Claude Code (`claude login`). -## Upgrade +### Burn Rate Alert -```bash -cd claude_ai_usage_widget -chmod +x upgrade.sh && ./upgrade.sh -``` +When enabled, fires a notification if your 7-day usage rate suggests you'll exceed your weekly limit. The multiplier controls sensitivity — `1.5` means: warn if you're on pace to use 150% of your allocation. -This will pull the latest version, reinstall, and restart the widget automatically. Your OAuth token and config are preserved. - -**Manual upgrade** (if you prefer step by step): -```bash -cd claude_ai_usage_widget -git pull -claude-widget-stop -./install.sh -claude-widget-start -``` +Notifications escalate up to 3 times per window, mirroring the usage thresholds: -## Uninstall +| When | Condition | +|---|---| +| Early in the week (below warn %) | First alert — catches it before it's serious | +| At warn % (default 60%) | Second alert if burn rate is still high | +| At critical % (default 85%) | Final alert — critical urgency | -```bash -chmod +x uninstall.sh -./uninstall.sh -``` +Each level fires at most once. The first ~8 hours of a new window are ignored to avoid false alarms, and all levels reset when the window rolls over. -This will remove: -- Installation directory (`~/.local/share/claude-usage-widget/`) -- Wrapper scripts (`claude-widget-start`, `claude-widget-stop`) -- Symlink (`~/.local/bin/claude-usage-widget`) -- Autostart entry (`~/.config/autostart/claude-usage-widget.desktop`) -- Application entry (`~/.local/share/applications/claude-usage-widget.desktop`) +## How It Works -You'll be prompted whether to keep or remove your config (OAuth token). +Uses the same internal API endpoint as Claude Code's `/usage`: -## Development +``` +GET https://api.anthropic.com/api/oauth/usage +Authorization: Bearer +anthropic-beta: oauth-2025-04-20 +``` -### Pre-Release Validation +Credentials are read directly from Claude Code's credential files — no separate login required. -Before creating a release or pushing to the repository, run the validation script to check for common issues: +## Uninstall ```bash -chmod +x validate.sh -./validate.sh +./uninstall.sh ``` -The script performs these checks: -- **Python syntax** — validates `claude_usage_widget.py` compiles -- **Shell scripts** — validates `install.sh` and `uninstall.sh` syntax -- **Token leaks** — scans for real OAuth tokens in repository (placeholders OK) -- **File permissions** — verifies secure file modes -- **Required files** — checks all distribution files exist -- **TODO/FIXME** — finds unresolved comments -- **README placeholders** — ensures no template placeholders remain -- **Version tags** — validates git tag matches code version - -**Exit codes:** -- `0` — All checks passed, ready for release -- `1` — Errors found, fix before releasing - -This is especially useful for: -- Pre-commit validation -- CI/CD integration -- Ensuring quality before releases -- Catching common mistakes (token leaks, missing files, etc.) +Removes all installed files, scripts, desktop entries, and temp files. Prompts before removing your account config. ## Troubleshooting | Problem | Fix | |---|---| +| Account shows `!` | Check `/tmp/claude-widget.log`. Usually an expired token — re-run `claude login` | +| Account shows `?` | The usage window just rolled over and the API hasn't returned data for the new period yet — it will clear on the next successful poll | | No tray icon on GNOME 43+ | Install `gnome-shell-extension-appindicator` and enable it | | `AppIndicator3` import fails | `sudo apt install gir1.2-appindicator3-0.1` | -| `ModuleNotFoundError: No module named 'gi'` | You're using pyenv/conda Python. Use `claude-widget-start` which uses system Python | -| `symbol lookup error: libpthread.so.0` | Snap library conflict. Use `claude-widget-start` which sets clean environment | -| `command not found` after install/uninstall | Run `hash -r` to clear bash's command cache, or close/reopen terminal | -| Token expired / 401 | Re-run `claude login` (Claude Code) or re-extract from browser | -| Icon shows "ERR" | Check token validity and network connectivity | - -### Python Environment Issues - -If you use **pyenv**, **conda**, or other Python version managers, the `python3-gi` system package may not be accessible. The installer creates wrapper scripts (`claude-widget-start`/`claude-widget-stop`) that automatically use the system Python (`/usr/bin/python3`) with a clean environment to avoid conflicts. - -### Checking Logs - -If the widget fails to start or behaves unexpectedly, check the log file: +| Widget broke after pyenv switch | Re-run `./install.sh` — creates a new venv from current pyenv version | +| `command not found` after install | Run `hash -r` or open a new terminal | ```bash -cat /tmp/claude-widget.log +cat /tmp/claude-widget.log # Check logs ``` -Common issues in logs: -- **Symbol lookup errors**: Snap library conflicts (use `claude-widget-start`) -- **Module import errors**: Python environment issues (use `claude-widget-start`) -- **HTTP 401/403 errors**: Token expired or invalid (refresh token) -- **Network errors**: Check internet connectivity or API availability - -## Contributing & Contact - -Contributions are welcome! - -- **Bug reports / feature requests** — [Open an issue](https://github.com/StaticB1/claude_ai_usage_widget/issues) -- **Discussions / collaboration** — [GitHub Discussions](https://github.com/StaticB1/claude_ai_usage_widget/discussions) -- **Email** — contact@statotec.com - -## Changelog - -See [CHANGELOG.md](CHANGELOG.md) for the full release history. - ## License -MIT - -## Author +MIT — see original repo for full history and credits. -Created by **Statotech Systems** - ---- +## Credits -Made with ⚡ by [Statotech Systems](https://github.com/StaticB1) +Original widget by **[Statotech Systems](https://github.com/StaticB1)**. +Multi-account fork by [gqcorneby](https://github.com/gqcorneby). diff --git a/claude_usage_widget.py b/claude_usage_widget.py index 2bbdad7..6df31da 100644 --- a/claude_usage_widget.py +++ b/claude_usage_widget.py @@ -1,19 +1,18 @@ #!/usr/bin/env python3 """ -Claude AI Usage Widget — Linux System Tray -Shows claude.ai subscription usage (5h / 7d) in the taskbar. +Claude AI Usage Widget — Linux System Tray (Multi-Account) +Shows claude.ai subscription usage (5h / 7d) in the taskbar for multiple accounts. Click to see detailed breakdown + reset timers. -Supports two auth methods: - 1. Auto-detect from Claude Code credentials (~/.claude/.credentials.json) - 2. Manual OAuth token via config file (~/.config/claude-usage-widget/config.json) +Reads credentials from configurable Claude Code directories. +Config: ~/.config/claude-usage-widget/config.json -Author: Statotech Systems -Version: 1.0.0 +Author: Statotech Systems (original), extended for multi-account +Version: 2.0.0 License: MIT """ -__version__ = "1.0.4" +__version__ = "2.0.0" __author__ = "Statotech Systems" import gi @@ -21,7 +20,7 @@ gi.require_version('AppIndicator3', '0.1') gi.require_version('Notify', '0.7') -from gi.repository import Gtk, AppIndicator3, GLib, Notify, Gdk, Pango +from gi.repository import Gtk, AppIndicator3, GLib, Notify import cairo import json import os @@ -34,148 +33,181 @@ from datetime import datetime, timezone from pathlib import Path +from shared import ( + COLOR_GRAY, DEFAULT_THRESHOLDS, get_color_for_pct, hex_to_rgb, + parse_utilization, format_reset_time, format_reset_clock, compute_burn_rate, +) +from usage_popup import UsageDetailWindow +from config_window import ConfigWindow + # ── Config ────────────────────────────────────────────────────────────────── APP_ID = "claude-usage-widget" APP_NAME = "Claude Usage" -ICON_NAME = "network-transmit-receive" # fallback icon -REFRESH_INTERVAL_SEC = 120 # 2 minutes USAGE_API_URL = "https://api.anthropic.com/api/oauth/usage" CONFIG_DIR = Path.home() / ".config" / APP_ID CONFIG_FILE = CONFIG_DIR / "config.json" -CREDENTIALS_FILE = Path.home() / ".claude" / ".credentials.json" - -# ── Colors for the dynamic SVG icon ───────────────────────────────────────── - -COLOR_GREEN = "#22c55e" -COLOR_YELLOW = "#eab308" -COLOR_ORANGE = "#f97316" -COLOR_RED = "#ef4444" -COLOR_GRAY = "#6b7280" - +NOTIFICATION_STATE_FILE = CONFIG_DIR / "notification_state.json" -def get_color_for_pct(pct: float) -> str: - if pct < 0.5: - return COLOR_GREEN - elif pct < 0.75: - return COLOR_YELLOW - elif pct < 0.9: - return COLOR_ORANGE - else: - return COLOR_RED +DEFAULT_ACCOUNTS = [ + {"label": "Claude", "credentials_dir": "~/.claude"}, +] +DEFAULT_POLL_INTERVAL = 300 # 5 minutes -def hex_to_rgb(hex_color: str) -> tuple: - """Convert hex color to RGB tuple (0-1 range).""" - hex_color = hex_color.lstrip('#') - return tuple(int(hex_color[i:i+2], 16) / 255.0 for i in (0, 2, 4)) - +# ── Icon generation ───────────────────────────────────────────────────────── def write_icon(pct: float, error: bool = False) -> str: """Generate PNG icon with Cairo and return path.""" - if error: - color = COLOR_GRAY - else: - color = get_color_for_pct(pct) - + color = COLOR_GRAY if error else get_color_for_pct(pct) r, g, b = hex_to_rgb(color) - # Create PNG icon with Cairo size = 32 surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size) ctx = cairo.Context(surface) - # Clear background (transparent) ctx.set_operator(cairo.OPERATOR_CLEAR) ctx.paint() ctx.set_operator(cairo.OPERATOR_OVER) - # Draw filled circle (background) ctx.set_source_rgba(r, g, b, 0.25) ctx.arc(size/2, size/2, 13, 0, 2 * 3.14159) ctx.fill() - # Draw circle border ctx.set_source_rgb(r, g, b) ctx.set_line_width(2) ctx.arc(size/2, size/2, 13, 0, 2 * 3.14159) ctx.stroke() - # Draw "C" text ctx.set_source_rgb(r, g, b) ctx.select_font_face("Sans", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) ctx.set_font_size(22) - text = "C" - x_bearing, y_bearing, width, height, x_advance, y_advance = ctx.text_extents(text) + x_bearing, y_bearing, width, height, *_ = ctx.text_extents(text) ctx.move_to(size/2 - width/2 - x_bearing, size/2 - height/2 - y_bearing) ctx.show_text(text) - # Save to file icon_dir = Path("/tmp") / APP_ID icon_dir.mkdir(exist_ok=True) - icon_path = icon_dir / "icon.png" + # Reason: AppIndicator3 compares the path string and skips updates if + # unchanged, so we encode the color into the filename to force a reload. + icon_path = icon_dir / f"icon_{color.lstrip('#')}.png" surface.write_to_png(str(icon_path)) - return str(icon_path) -# ── Token loading ─────────────────────────────────────────────────────────── +def write_loading_icon(frame: int) -> str: + """Generate a spinner icon for the given frame (0–7), rotating a 3/4 arc.""" + PI2 = 2 * 3.14159 + size = 32 -def load_token() -> str | None: - """Try loading OAuth token from Claude Code creds, then config file.""" - # 1. Claude Code credentials (Linux) - if CREDENTIALS_FILE.exists(): - try: - data = json.loads(CREDENTIALS_FILE.read_text()) - token = data.get("claudeAiOauth", {}).get("accessToken") - if token: - return token - except (json.JSONDecodeError, KeyError): - pass + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size) + ctx = cairo.Context(surface) + ctx.set_operator(cairo.OPERATOR_CLEAR) + ctx.paint() + ctx.set_operator(cairo.OPERATOR_OVER) - # 2. Widget config file - if CONFIG_FILE.exists(): - try: - data = json.loads(CONFIG_FILE.read_text()) - token = data.get("oauth_token") - if token: - return token - except (json.JSONDecodeError, KeyError): - pass + # Dim background circle + ctx.set_source_rgba(0.5, 0.5, 0.5, 0.2) + ctx.arc(size/2, size/2, 13, 0, PI2) + ctx.fill() - return None + # Rotating 3/4 arc — start angle advances by 45° per frame + angle_start = (frame / 8) * PI2 - (PI2 / 4) # offset so 0 starts at top + angle_end = angle_start + PI2 * 0.75 + ctx.set_source_rgba(0.55, 0.55, 0.55, 0.9) + ctx.set_line_width(2.5) + ctx.arc(size/2, size/2, 13, angle_start, angle_end) + ctx.stroke() + + # Gray "C" to keep it recognisable while loading + ctx.set_source_rgba(0.5, 0.5, 0.5, 0.7) + ctx.select_font_face("Sans", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) + ctx.set_font_size(22) + text = "C" + x_bearing, y_bearing, width, height, *_ = ctx.text_extents(text) + ctx.move_to(size/2 - width/2 - x_bearing, size/2 - height/2 - y_bearing) + ctx.show_text(text) + icon_dir = Path("/tmp") / APP_ID + icon_dir.mkdir(exist_ok=True) + icon_path = icon_dir / f"icon_loading_{frame}.png" + surface.write_to_png(str(icon_path)) + return str(icon_path) -def load_subscription_info() -> dict | None: - """Load subscription information from Claude Code credentials.""" - if CREDENTIALS_FILE.exists(): - try: - data = json.loads(CREDENTIALS_FILE.read_text()) - oauth = data.get("claudeAiOauth", {}) - if oauth: - return { - "subscription_type": oauth.get("subscriptionType", "").title(), - "rate_limit_tier": oauth.get("rateLimitTier", ""), - } - except (json.JSONDecodeError, KeyError): - pass - return None +# ── Config loading ────────────────────────────────────────────────────────── -def save_token(token: str): - """Save token to widget config.""" - CONFIG_DIR.mkdir(parents=True, exist_ok=True) - config = {} +def load_config() -> dict: + """Load widget config, creating defaults if missing.""" if CONFIG_FILE.exists(): try: - config = json.loads(CONFIG_FILE.read_text()) - except json.JSONDecodeError: - pass - config["oauth_token"] = token + return json.loads(CONFIG_FILE.read_text()) + except (json.JSONDecodeError, OSError) as e: + print(f"[claude-usage] Bad config, using defaults: {e}", file=sys.stderr) + + config = { + "accounts": DEFAULT_ACCOUNTS, + "poll_interval_seconds": DEFAULT_POLL_INTERVAL, + "thresholds": DEFAULT_THRESHOLDS, + "burn_rate": {"enabled": False, "multiplier": 1.5}, + } + CONFIG_DIR.mkdir(parents=True, exist_ok=True) CONFIG_FILE.write_text(json.dumps(config, indent=2)) os.chmod(CONFIG_FILE, 0o600) + return config + + +def load_notification_state() -> dict: + """Load persisted notification state (keyed by account label).""" + if NOTIFICATION_STATE_FILE.exists(): + try: + return json.loads(NOTIFICATION_STATE_FILE.read_text()) + except (json.JSONDecodeError, OSError): + pass + return {} + + +def save_notification_state(state_map: dict): + """Persist notification state so restarts don't re-fire.""" + try: + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + NOTIFICATION_STATE_FILE.write_text(json.dumps(state_map, indent=2)) + except OSError as e: + print(f"[claude-usage] Could not save notification state: {e}", file=sys.stderr) + + +# ── Per-account token loading ─────────────────────────────────────────────── + +def load_token(credentials_dir: str) -> str | None: + """Load OAuth access token from a Claude Code credentials directory.""" + cred_path = Path(credentials_dir).expanduser() / ".credentials.json" + if not cred_path.exists(): + return None + try: + data = json.loads(cred_path.read_text()) + return data.get("claudeAiOauth", {}).get("accessToken") + except (json.JSONDecodeError, KeyError, OSError): + return None + + +def load_subscription_info(credentials_dir: str) -> dict | None: + """Load subscription info from a Claude Code credentials directory.""" + cred_path = Path(credentials_dir).expanduser() / ".credentials.json" + if not cred_path.exists(): + return None + try: + data = json.loads(cred_path.read_text()) + oauth = data.get("claudeAiOauth", {}) + if oauth: + return { + "subscription_type": oauth.get("subscriptionType", "").title(), + "rate_limit_tier": oauth.get("rateLimitTier", ""), + } + except (json.JSONDecodeError, KeyError, OSError): + pass + return None # ── API call ──────────────────────────────────────────────────────────────── @@ -189,13 +221,11 @@ def fetch_usage(token: str) -> dict | None: headers = { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "claude-usage-widget/1.0", + "User-Agent": f"claude-usage-widget/{__version__}", "Authorization": f"Bearer {token}", "anthropic-beta": "oauth-2025-04-20", } - req = urllib.request.Request(USAGE_API_URL, headers=headers, method="GET") - ctx = ssl.create_default_context() try: with urllib.request.urlopen(req, context=ctx, timeout=15) as resp: @@ -210,314 +240,94 @@ def fetch_usage(token: str) -> dict | None: return None -# ── Time formatting ───────────────────────────────────────────────────────── - -def format_reset_time(iso_str: str | None) -> str: - if not iso_str: - return "unknown" +def _is_resets_at_future(resets_at_str: str | None) -> bool: + """Return True if the given resets_at timestamp is still in the future.""" + if not resets_at_str: + return False try: - reset_dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00")) - now = datetime.now(timezone.utc) - delta = reset_dt - now - total_sec = int(delta.total_seconds()) - if total_sec <= 0: - return "any moment" - days, remainder = divmod(total_sec, 86400) - hours, remainder = divmod(remainder, 3600) - minutes, _ = divmod(remainder, 60) - if days > 0: - return f"{days}d {hours}h" - if hours > 0: - return f"{hours}h {minutes}m" - return f"{minutes}m" + resets_at = datetime.fromisoformat(resets_at_str.replace("Z", "+00:00")) + return datetime.now(timezone.utc) < resets_at except Exception: - return iso_str - - -# ── Detail popup window ───────────────────────────────────────────────────── - -class UsageDetailWindow(Gtk.Window): - """Popup window showing detailed usage info.""" - - def __init__(self, usage_data: dict | None, last_updated: str, token_status: str, user_info: dict | None = None): - super().__init__(title="Claude AI Usage") - self.set_default_size(380, -1) - self.set_resizable(False) - self.set_position(Gtk.WindowPosition.MOUSE) - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - self.set_keep_above(True) - self.set_decorated(True) - - # Lose focus → close - self.connect("focus-out-event", lambda *_: self.destroy()) - - # Apply CSS - css = Gtk.CssProvider() - css.load_from_data(b""" - window { background-color: #1a1a2e; } - .title-label { color: #e0e0ff; font-size: 16px; font-weight: bold; } - .section-label { color: #a0a0c0; font-size: 11px; font-weight: bold; letter-spacing: 2px; } - .metric-value { color: #ffffff; font-size: 28px; font-weight: bold; } - .metric-sub { color: #8888aa; font-size: 11px; } - .reset-label { color: #6b7280; font-size: 11px; } - .status-ok { color: #22c55e; font-size: 11px; } - .status-warn { color: #eab308; font-size: 11px; } - .status-err { color: #ef4444; font-size: 11px; } - .bar-bg { background-color: #2a2a4a; border-radius: 4px; } - .separator { background-color: #2a2a4a; } - """) - Gtk.StyleContext.add_provider_for_screen( - Gdk.Screen.get_default(), css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ) - - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) - vbox.set_margin_top(16) - vbox.set_margin_bottom(16) - vbox.set_margin_start(20) - vbox.set_margin_end(20) - - # Header - header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - title = Gtk.Label(label="⚡ Claude Usage") - title.get_style_context().add_class("title-label") - header.pack_start(title, False, False, 0) - - status_label = Gtk.Label(label=f"● {token_status}") - sc = "status-ok" if token_status == "Connected" else ("status-warn" if token_status == "Rate limited" else "status-err") - status_label.get_style_context().add_class(sc) - status_label.set_halign(Gtk.Align.END) - header.pack_end(status_label, False, False, 0) - vbox.pack_start(header, False, False, 0) - - # Subscription info (if available) - if user_info: - sub_type = user_info.get("subscription_type") - if sub_type: - sub_label = Gtk.Label(label=f"Plan: {sub_type}") - sub_label.get_style_context().add_class("metric-sub") - sub_label.set_halign(Gtk.Align.START) - vbox.pack_start(sub_label, False, False, 0) - - if usage_data: - # Extra usage section (pay-as-you-go credits) - extra = usage_data.get("extra_usage") or {} - if extra and extra.get("is_enabled"): - sep = Gtk.Separator() - sep.get_style_context().add_class("separator") - vbox.pack_start(sep, False, False, 4) - - extra_section = Gtk.Label(label="EXTRA USAGE (MONTHLY)") - extra_section.get_style_context().add_class("section-label") - extra_section.set_halign(Gtk.Align.START) - vbox.pack_start(extra_section, False, False, 0) - - used = extra.get("used_credits", 0) - limit = extra.get("monthly_limit", 0) - extra_pct = min(int(extra.get("utilization", 0)), 100) - extra_decimal = extra_pct / 100 - extra_color = get_color_for_pct(extra_decimal) - - extra_val = Gtk.Label() - extra_val.set_markup( - f'{extra_pct}%' - ) - extra_val.set_halign(Gtk.Align.START) - vbox.pack_start(extra_val, False, False, 0) - - credits_lbl = Gtk.Label(label=f"{used:.0f} / {limit:.0f} credits used") - credits_lbl.get_style_context().add_class("metric-sub") - credits_lbl.set_halign(Gtk.Align.START) - vbox.pack_start(credits_lbl, False, False, 0) - - for key, label_text in [("five_hour", "5-HOUR WINDOW"), ("seven_day", "7-DAY WINDOW")]: - bucket = usage_data.get(key) - if not bucket: - continue - - sep = Gtk.Separator() - sep.get_style_context().add_class("separator") - vbox.pack_start(sep, False, False, 4) - - section_label = Gtk.Label(label=label_text) - section_label.get_style_context().add_class("section-label") - section_label.set_halign(Gtk.Align.START) - vbox.pack_start(section_label, False, False, 0) - - utilization = bucket.get("utilization", 0) - # Handle both decimal (0-1) and percentage (0-100) formats - if utilization > 1: # Already a percentage - pct = int(utilization) - utilization_decimal = utilization / 100 - else: # Decimal format - pct = int(utilization * 100) - utilization_decimal = utilization - - # Big number - val = Gtk.Label(label=f"{pct}%") - val.get_style_context().add_class("metric-value") - color = get_color_for_pct(utilization_decimal) - val.set_markup(f'{pct}%') - val.set_halign(Gtk.Align.START) - vbox.pack_start(val, False, False, 0) - - # Progress bar (GTK level bar) - bar_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - bar_box.get_style_context().add_class("bar-bg") - - bar = Gtk.LevelBar() - bar.set_min_value(0) - bar.set_max_value(1.0) - bar.set_value(utilization_decimal) - bar.set_size_request(-1, 8) - - # Remove default offset classes and add custom - bar.remove_offset_value("low") - bar.remove_offset_value("high") - bar.remove_offset_value("full") - bar_css = Gtk.CssProvider() - bar_css.load_from_data(f""" - levelbar trough {{ - background-color: #2a2a4a; - border-radius: 4px; - min-height: 8px; - }} - levelbar trough block.filled {{ - background-color: {color}; - border-radius: 4px; - min-height: 8px; - }} - """.encode()) - bar.get_style_context().add_provider(bar_css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - - bar_box.pack_start(bar, True, True, 0) - vbox.pack_start(bar_box, False, False, 0) - - # Reset time - resets = bucket.get("resets_at") - reset_str = format_reset_time(resets) - reset_lbl = Gtk.Label(label=f"Resets in {reset_str}") - reset_lbl.get_style_context().add_class("reset-label") - reset_lbl.set_halign(Gtk.Align.START) - vbox.pack_start(reset_lbl, False, False, 0) - else: - err_label = Gtk.Label(label="Unable to fetch usage data.\nCheck token and connectivity.") - err_label.get_style_context().add_class("status-err") - vbox.pack_start(err_label, False, False, 8) - - # Footer - sep = Gtk.Separator() - sep.get_style_context().add_class("separator") - vbox.pack_start(sep, False, False, 4) - - footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) - updated = Gtk.Label(label=f"Updated: {last_updated}") - updated.get_style_context().add_class("metric-sub") - footer.pack_start(updated, False, False, 0) - - refresh_btn = Gtk.Button(label="↻ Refresh") - refresh_btn.set_relief(Gtk.ReliefStyle.NONE) - refresh_css = Gtk.CssProvider() - refresh_css.load_from_data(b""" - button { color: #8888aa; background: transparent; border: none; padding: 2px 8px; } - button:hover { color: #e0e0ff; } - """) - refresh_btn.get_style_context().add_provider(refresh_css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - refresh_btn.connect("clicked", lambda _: (self.destroy(), app.force_refresh())) - footer.pack_end(refresh_btn, False, False, 0) - - vbox.pack_start(footer, False, False, 0) - - # Version info - version_label = Gtk.Label(label=f"v{__version__}") - version_label.get_style_context().add_class("reset-label") - version_label.set_halign(Gtk.Align.CENTER) - version_label.set_margin_top(4) - vbox.pack_start(version_label, False, False, 0) - - self.add(vbox) - self.show_all() - - -# ── Token entry dialog ────────────────────────────────────────────────────── - -class TokenDialog(Gtk.Dialog): - def __init__(self, parent=None): - super().__init__(title="Claude OAuth Token", transient_for=parent, flags=0) - self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, - Gtk.STOCK_OK, Gtk.ResponseType.OK) - self.set_default_size(450, -1) - - box = self.get_content_area() - box.set_spacing(8) - box.set_margin_top(12) - box.set_margin_bottom(12) - box.set_margin_start(12) - box.set_margin_end(12) - - label = Gtk.Label() - label.set_markup( - "Enter your Claude OAuth token.\n" - "Get it from ~/.claude/.credentials.json (Claude Code)\n" - "or browser DevTools → Network → api.anthropic.com headers." - ) - label.set_line_wrap(True) - box.pack_start(label, False, False, 0) - - self.entry = Gtk.Entry() - self.entry.set_placeholder_text("sk-ant-oat01-...") - self.entry.set_visibility(False) - box.pack_start(self.entry, False, False, 0) + return False - self.show_all() - def get_token(self) -> str: - return self.entry.get_text().strip() +def _is_usage_stale(usage: dict) -> bool: + """Return True if any usage window's resets_at has passed, meaning the period rolled over.""" + now = datetime.now(timezone.utc) + for key in ("five_hour", "seven_day"): + window = usage.get(key) or {} + resets_at_str = window.get("resets_at") + if resets_at_str: + try: + resets_at = datetime.fromisoformat(resets_at_str.replace("Z", "+00:00")) + if now >= resets_at: + return True + except Exception: + pass + return False # ── Main App ──────────────────────────────────────────────────────────────── class ClaudeUsageApp: def __init__(self): - self.usage_data: dict | None = None - self.subscription_info: dict | None = None - self.last_updated: str = "never" - self.token: str | None = None + self.config = load_config() + self.accounts = self.config.get("accounts", DEFAULT_ACCOUNTS) + self.poll_interval = self.config.get("poll_interval_seconds", DEFAULT_POLL_INTERVAL) + self.thresholds = self.config.get("thresholds", DEFAULT_THRESHOLDS) + self.burn_rate_cfg = self.config.get("burn_rate", {"enabled": False, "multiplier": 1.5}) + + # Per-account state — seed notification fields from disk so restarts don't re-fire + persisted = load_notification_state() + self.account_states: dict[str, dict] = {} + for acct in self.accounts: + state = self._blank_state(acct["credentials_dir"]) + saved = persisted.get(acct["label"], {}) + state["last_notification_threshold"] = saved.get("last_notification_threshold", 0) + state["last_threshold_five_resets_at"] = saved.get("last_threshold_five_resets_at") + state["last_threshold_seven_resets_at"] = saved.get("last_threshold_seven_resets_at") + state["last_burn_rate_resets_at"] = saved.get("last_burn_rate_resets_at") + state["last_burn_rate_threshold"] = saved.get("last_burn_rate_threshold", 0) + state["last_seven_resets_at"] = saved.get("last_seven_resets_at") + self.account_states[acct["label"]] = state + + self.last_updated = "never" self.running = True - self.last_notification_threshold: int = 0 # Track last notified threshold (0, 75, 90, 100) - self.startup_notification_sent: bool = False + self.startup_notification_sent = False + self._loading = False + self._loading_frame = 0 Notify.init(APP_NAME) # Create indicator icon_path = write_icon(0, error=True) self.indicator = AppIndicator3.Indicator.new( - APP_ID, - icon_path, - AppIndicator3.IndicatorCategory.APPLICATION_STATUS, + APP_ID, icon_path, AppIndicator3.IndicatorCategory.APPLICATION_STATUS, ) self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) self.indicator.set_title(APP_NAME) self.indicator.set_label("--", "") - # Build menu - self.menu = Gtk.Menu() + self._build_menu() - self.item_5h = Gtk.MenuItem(label="5h: --%") - self.item_5h.set_sensitive(False) - self.menu.append(self.item_5h) + # Start background polling + self.poll_thread = threading.Thread(target=self._poll_loop, daemon=True) + self.poll_thread.start() - self.item_7d = Gtk.MenuItem(label="7d: --%") - self.item_7d.set_sensitive(False) - self.menu.append(self.item_7d) + def _build_menu(self): + self.menu = Gtk.Menu() + self.menu_items = {} - self.item_extra = Gtk.MenuItem(label="") - self.item_extra.set_sensitive(False) - self.item_extra.set_no_show_all(True) - self.menu.append(self.item_extra) + for acct in self.accounts: + lbl = acct["label"] + item = Gtk.MenuItem(label=f"{lbl}: --%") + item.set_sensitive(False) + self.menu.append(item) + self.menu_items[lbl] = item self.menu.append(Gtk.SeparatorMenuItem()) - item_details = Gtk.MenuItem(label="Show Details…") + item_details = Gtk.MenuItem(label="Show Details...") item_details.connect("activate", self.on_show_details) self.menu.append(item_details) @@ -525,9 +335,9 @@ def __init__(self): item_refresh.connect("activate", lambda _: self.force_refresh()) self.menu.append(item_refresh) - item_token = Gtk.MenuItem(label="Set Token…") - item_token.connect("activate", self.on_set_token) - self.menu.append(item_token) + item_configure = Gtk.MenuItem(label="Configure...") + item_configure.connect("activate", self.on_configure) + self.menu.append(item_configure) self.menu.append(Gtk.SeparatorMenuItem()) @@ -538,192 +348,376 @@ def __init__(self): self.menu.show_all() self.indicator.set_menu(self.menu) - # Load token and subscription info - self.token = load_token() - self.subscription_info = load_subscription_info() - if not self.token: - GLib.timeout_add_seconds(2, self._prompt_token_once) - - # Start background polling - self.poll_thread = threading.Thread(target=self._poll_loop, daemon=True) - self.poll_thread.start() - - def _prompt_token_once(self): - """Show token dialog on first run if no token found.""" - if not self.token: - self.on_set_token(None) - return False # don't repeat - def _poll_loop(self): - """Background thread: fetch usage periodically.""" + """Background thread: fetch usage for all accounts periodically.""" + # Reason: initial fetch always runs for all accounts regardless of disable_polling + results = {label: self._fetch_account(label, state) + for label, state in self.account_states.items()} + GLib.idle_add(self._update_ui, results) while self.running: - try: - # Re-read credentials on every cycle so a token refreshed - # by Claude Code overnight is picked up automatically. - fresh = load_token() - if fresh: - self.token = fresh - if self.token: - data = fetch_usage(self.token) - GLib.idle_add(self._update_ui, data) - except RateLimitError: - # Show ERR but keep last good data so details window still works - print("[claude-usage] Rate limited, backing off 10 min", file=sys.stderr) - GLib.idle_add(self._set_rate_limit_ui) - time.sleep(600) - continue - except Exception as e: - print(f"[claude-usage] Poll error: {e}", file=sys.stderr) - time.sleep(REFRESH_INTERVAL_SEC) + time.sleep(self.poll_interval) + if any(not a.get("disable_polling", False) for a in self.accounts): + self._do_poll() + + def _do_poll(self): + """Background periodic poll — respects per-account disable_polling.""" + acct_cfg = {a["label"]: a for a in self.accounts} + results = {} + for label, state in self.account_states.items(): + if not acct_cfg.get(label, {}).get("disable_polling", False): + results[label] = self._fetch_account(label, state) + GLib.idle_add(self._update_ui, results) + + def _fetch_account(self, label: str, state: dict) -> dict: + """Fetch usage for a single account. Returns update dict.""" + cred_dir = state["credentials_dir"] + try: + token = load_token(cred_dir) + if not token: + return {"error": "No token", "usage_data": None, "token": None} + data = fetch_usage(token) + sub_info = load_subscription_info(cred_dir) + if data is None: + return {"error": "API error", "usage_data": None, "token": token, + "subscription_info": sub_info} + return {"error": None, "usage_data": data, "token": token, + "subscription_info": sub_info} + except RateLimitError: + print(f"[claude-usage] {label}: rate limited", file=sys.stderr) + return {"error": "Rate limited", "usage_data": None, "token": state.get("token")} + except Exception as e: + print(f"[claude-usage] {label}: {e}", file=sys.stderr) + return {"error": str(e), "usage_data": None, "token": state.get("token")} def force_refresh(self): - """Immediate refresh triggered by user.""" + GLib.idle_add(self._start_loading_animation) def _do(): - if self.token: - data = fetch_usage(self.token) - GLib.idle_add(self._update_ui, data) + results = {} + for label, state in self.account_states.items(): + results[label] = self._fetch_account(label, state) + GLib.idle_add(self._update_ui, results) threading.Thread(target=_do, daemon=True).start() - def _set_rate_limit_ui(self): - """Show ERR state without clearing cached usage_data (preserves details window).""" - self.last_updated = datetime.now().strftime("%H:%M:%S") - self.indicator.set_label("ERR", "") - icon_path = write_icon(0, error=True) - self.indicator.set_icon_full(icon_path, "Error") - self.item_5h.set_label("5h: rate limited") - self.item_7d.set_label("7d: rate limited") - return False - - def _update_ui(self, data: dict | None): - """Update indicator label + icon from fetched data (runs on GTK thread).""" - self.usage_data = data + def _start_loading_animation(self): + """Begin spinner animation (must run on GTK thread via idle_add).""" + self._loading = True + self._loading_frame = 0 + GLib.timeout_add(120, self._tick_loading_icon) + return False # one-shot + + def _tick_loading_icon(self): + """Advance spinner one frame; stops automatically when _loading is cleared.""" + if not self._loading: + return False + self._loading_frame = (self._loading_frame + 1) % 8 + icon_path = write_loading_icon(self._loading_frame) + self.indicator.set_icon_full(icon_path, "Refreshing...") + return True # keep going + + def _update_ui(self, results: dict): + """Update indicator label + icon from fetched data (GTK thread).""" + self._loading = False # stop spinner; _tick_loading_icon will not reschedule self.last_updated = datetime.now().strftime("%H:%M:%S") - if data: - five = data.get("five_hour", {}) or {} - seven = data.get("seven_day", {}) or {} - u5 = five.get("utilization", 0) - u7 = seven.get("utilization", 0) - - # Handle both decimal (0-1) and percentage (0-100) formats - if u5 > 1: # Already a percentage - pct5 = int(u5) - u5_decimal = u5 / 100 - else: # Decimal format, convert to percentage - pct5 = int(u5 * 100) - u5_decimal = u5 - - if u7 > 1: # Already a percentage - pct7 = int(u7) - u7_decimal = u7 / 100 - else: # Decimal format, convert to percentage - pct7 = int(u7 * 100) - u7_decimal = u7 - - dominant = max(u5_decimal, u7_decimal) - - self.indicator.set_label(f"{pct5}%", "") - icon_path = write_icon(dominant) - self.indicator.set_icon_full(icon_path, f"{pct5}%") - - self.item_5h.set_label(f"5h: {pct5}% (resets {format_reset_time(five.get('resets_at'))})") - self.item_7d.set_label(f"7d: {pct7}% (resets {format_reset_time(seven.get('resets_at'))})") - - # Extra usage (pay-as-you-go credits) - extra = data.get("extra_usage") or {} - if extra and extra.get("is_enabled"): - used = extra.get("used_credits", 0) - limit = extra.get("monthly_limit", 0) - self.item_extra.set_label(f"Extra: {used:.0f}/{limit:.0f} credits") - self.item_extra.show() + persist_needed = False + for label, result in results.items(): + if result.get("usage_data"): + result["pending_reset"] = False + seven = result["usage_data"].get("seven_day") or {} + new_resets_at = seven.get("resets_at") + if new_resets_at and new_resets_at != self.account_states[label].get("last_seven_resets_at"): + result["last_seven_resets_at"] = new_resets_at + persist_needed = True else: - self.item_extra.hide() + cached = self.account_states[label].get("usage_data") + if cached and _is_usage_stale(cached): + # Window(s) have reset — cached data is misleading, clear it and wait for fresh data + result = {k: v for k, v in result.items() if k != "usage_data"} + result["pending_reset"] = True + elif cached: + # Still within the window, preserve last known values + result = {k: v for k, v in result.items() if k != "usage_data"} + # else: no cached data at all — leave as-is (will show ! or ? if 7d still active) + self.account_states[label].update(result) + + if persist_needed: + self._persist_notification_state() + + # Build tray label: "G:67% N:12%" — accounts with hide_from_tray are skipped + label_parts = [] + max_pct = 0 + any_ok = False + + for acct in self.accounts: + lbl = acct["label"] + hide = acct.get("hide_from_tray", False) + state = self.account_states[lbl] + usage = state.get("usage_data") + + if usage: + five = usage.get("five_hour", {}) or {} + seven = usage.get("seven_day", {}) or {} + pct5, _ = parse_utilization(five.get("utilization", 0)) + pct7, _ = parse_utilization(seven.get("utilization", 0)) + + if not hide: + any_ok = True + label_parts.append(f"{lbl}:{pct5}%") + max_pct = max(max_pct, pct5, pct7) + + r5 = format_reset_clock(five.get("resets_at")) if acct.get("disable_polling", False) else format_reset_time(five.get("resets_at")) + burn_rate = compute_burn_rate(seven) + if burn_rate is not None: + arrow = "\u2191" if burn_rate >= 1.0 else "\u2193" + br_str = f" {arrow}{burn_rate:.1f}\u00d7 \u21ba {r5}" + else: + br_str = f" \u21ba {r5}" + self.menu_items[lbl].set_label(f"{lbl}: {pct7}%{br_str}") + else: + seven_resets_at = state.get("last_seven_resets_at") + seven_active = _is_resets_at_future(seven_resets_at) + if not hide: + if state.get("pending_reset") or seven_active: + label_parts.append(f"{lbl}:?") + else: + label_parts.append(f"{lbl}:!") + if state.get("pending_reset"): + self.menu_items[lbl].set_label(f"{lbl}: waiting for new period data...") + elif seven_active: + reset_str = format_reset_time(seven_resets_at) + self.menu_items[lbl].set_label(f"{lbl}: ? \u21ba {reset_str}") + else: + self.menu_items[lbl].set_label(f"{lbl}: {state.get('error', 'error')}") + + tray_text = " ".join(label_parts) + self.indicator.set_label(tray_text, "") + + icon_path = write_icon(max_pct) if any_ok else write_icon(0, error=True) + self.indicator.set_icon_full(icon_path, tray_text) + + # Startup notification + if not self.startup_notification_sent and any_ok: + self.startup_notification_sent = True + n = Notify.Notification.new("Claude Usage Widget Started", tray_text, "dialog-information") + n.show() - # Send notifications at specific thresholds only - self._check_and_notify_threshold(pct5, pct7, dominant) - else: - self.indicator.set_label("ERR", "") - icon_path = write_icon(0, error=True) - self.indicator.set_icon_full(icon_path, "Error") - self.item_5h.set_label("5h: error") - self.item_7d.set_label("7d: error") + # Per-account threshold notifications + for acct in self.accounts: + lbl = acct["label"] + state = self.account_states[lbl] + usage = state.get("usage_data") + if not usage: + continue + self._check_threshold(lbl, usage, state) + self._check_burn_rate(lbl, usage, state) return False # GLib.idle_add one-shot - def _check_and_notify_threshold(self, pct5: int, pct7: int, dominant: float): - """Send notifications only at specific thresholds: startup, 75%, 90%, 100%""" - # Startup notification (first successful data fetch) + @staticmethod + def _resets_at_changed(stored_str: str | None, new_str: str | None, tolerance_hours: float) -> bool: + """Return True if resets_at shifted enough to indicate a new window.""" + if not new_str: + return False + if stored_str is None: + return True + try: + stored_dt = datetime.fromisoformat(stored_str.replace("Z", "+00:00")) + new_dt = datetime.fromisoformat(new_str.replace("Z", "+00:00")) + return abs((new_dt - stored_dt).total_seconds()) > tolerance_hours * 3600 + except Exception: + return stored_str != new_str + + def _check_threshold(self, label: str, usage: dict, state: dict): + """Send notification when an account crosses a threshold. + + Resets the escalation level whenever either the 5h or 7d window rolls + over, so notifications fire again at the start of each new window. + """ if not self.startup_notification_sent: - self.startup_notification_sent = True - n = Notify.Notification.new( - "✓ Claude Usage Widget Started", - f"Current usage: 5h: {pct5}% | 7d: {pct7}%", - "dialog-information", - ) - n.show() return - # Determine current threshold - pct_val = int(dominant * 100) - current_threshold = 0 - - if pct_val >= 100: - current_threshold = 100 - elif pct_val >= 90: - current_threshold = 90 - elif pct_val >= 75: - current_threshold = 75 - - # Only notify if crossing a new threshold - if current_threshold > self.last_notification_threshold: - if current_threshold == 75: - n = Notify.Notification.new( - "⚠️ Claude Usage: 75%", - f"5h: {pct5}% | 7d: {pct7}%\nApproaching rate limits.", - "dialog-warning", - ) - n.set_urgency(Notify.Urgency.NORMAL) - elif current_threshold == 90: - n = Notify.Notification.new( - "⚠️ Claude Usage: 90%", - f"5h: {pct5}% | 7d: {pct7}%\nClose to rate limits!", - "dialog-warning", - ) - n.set_urgency(Notify.Urgency.CRITICAL) - elif current_threshold == 100: - n = Notify.Notification.new( - "🛑 Claude Usage: 100%", - f"5h: {pct5}% | 7d: {pct7}%\nRate limit reached!", - "dialog-error", - ) - n.set_urgency(Notify.Urgency.CRITICAL) + five = usage.get("five_hour", {}) or {} + seven = usage.get("seven_day", {}) or {} + pct5, _ = parse_utilization(five.get("utilization", 0)) + pct7, _ = parse_utilization(seven.get("utilization", 0)) + pct = max(pct5, pct7) + + five_resets_at = five.get("resets_at") + seven_resets_at = seven.get("resets_at") + + # Detect window rollovers — 5h uses 1h tolerance, 7d uses 6h tolerance + five_new = self._resets_at_changed(state.get("last_threshold_five_resets_at"), five_resets_at, 1) + seven_new = self._resets_at_changed(state.get("last_threshold_seven_resets_at"), seven_resets_at, 6) + if five_new or seven_new: + state["last_notification_threshold"] = 0 + if five_resets_at: + state["last_threshold_five_resets_at"] = five_resets_at + if seven_resets_at: + state["last_threshold_seven_resets_at"] = seven_resets_at + + prev = state.get("last_notification_threshold", 0) + warn = self.thresholds.get("warn", 60) + crit = self.thresholds.get("critical", 85) + + current = 0 + if pct >= 100: + current = 100 + elif pct >= crit: + current = crit + elif pct >= warn: + current = warn + + if current <= prev: + return + + urgency = Notify.Urgency.CRITICAL if current >= crit else Notify.Urgency.NORMAL + icon = "dialog-warning" if current >= crit else "dialog-information" + n = Notify.Notification.new(f"{label}: Usage at {pct}%", f"Account {label} reached {pct}%", icon) + n.set_urgency(urgency) + n.show() + state["last_notification_threshold"] = current + self._persist_notification_state() + + @staticmethod + def _blank_state(credentials_dir: str) -> dict: + return { + "credentials_dir": credentials_dir, + "token": None, "usage_data": None, "pending_reset": False, + "subscription_info": None, "error": None, + "last_notification_threshold": 0, # highest level already notified this window + "last_threshold_five_resets_at": None, # 5h window resets_at seen last poll + "last_threshold_seven_resets_at": None, # 7d window resets_at seen last poll + "last_burn_rate_resets_at": None, # resets_at value of the current window + "last_burn_rate_threshold": 0, # highest usage % level already notified (0/warn/critical) + "last_seven_resets_at": None, # last known 7d resets_at, persisted across restarts + } + + def _persist_notification_state(self): + """Persist notification state to disk so restarts don't re-fire. + + Both threshold and burn-rate state are now safe to persist because both + track their respective window resets_at timestamps — a new window is + detected on the next poll and the level resets to 0 automatically. + """ + state_map = { + lbl: { + "last_notification_threshold": s.get("last_notification_threshold", 0), + "last_threshold_five_resets_at": s.get("last_threshold_five_resets_at"), + "last_threshold_seven_resets_at": s.get("last_threshold_seven_resets_at"), + "last_burn_rate_resets_at": s.get("last_burn_rate_resets_at"), + "last_burn_rate_threshold": s.get("last_burn_rate_threshold", 0), + "last_seven_resets_at": s.get("last_seven_resets_at"), + } + for lbl, s in self.account_states.items() + } + save_notification_state(state_map) + + def on_configure(self, _widget): + ConfigWindow(self.accounts, self.thresholds, self.burn_rate_cfg, + self.poll_interval, self._on_config_saved) + + def _on_config_saved(self, new_config: dict): + """Rebuild live state from saved config and refresh.""" + self.accounts = new_config["accounts"] + self.thresholds = new_config["thresholds"] + self.burn_rate_cfg = new_config["burn_rate"] + self.poll_interval = new_config["poll_interval_seconds"] + persisted = load_notification_state() + self.account_states = {} + for acct in self.accounts: + state = self._blank_state(acct["credentials_dir"]) + saved = persisted.get(acct["label"], {}) + state["last_notification_threshold"] = saved.get("last_notification_threshold", 0) + state["last_threshold_five_resets_at"] = saved.get("last_threshold_five_resets_at") + state["last_threshold_seven_resets_at"] = saved.get("last_threshold_seven_resets_at") + state["last_burn_rate_resets_at"] = saved.get("last_burn_rate_resets_at") + state["last_burn_rate_threshold"] = saved.get("last_burn_rate_threshold", 0) + state["last_seven_resets_at"] = saved.get("last_seven_resets_at") + self.account_states[acct["label"]] = state + self._build_menu() + self.force_refresh() + + def _check_burn_rate(self, label: str, usage: dict, state: dict): + """Notify when 7d burn rate exceeds the multiplier. + + Escalation levels mirror the usage thresholds: + 0 → fires once early in the window (before warn threshold) + warn % → fires again if burn rate still high at warn level + critical % → fires again if burn rate still high at critical level + Each level fires at most once per window cycle. + """ + if not self.burn_rate_cfg.get("enabled"): + return + if not self.startup_notification_sent: + return + + seven = usage.get("seven_day") or {} + resets_at_str = seven.get("resets_at") + if not resets_at_str: + return + + pct7, _ = parse_utilization(seven.get("utilization", 0)) + if pct7 <= 0: + return + + try: + burn_rate = compute_burn_rate(seven) + if burn_rate is None: + return + + multiplier = self.burn_rate_cfg.get("multiplier", 1.5) + if burn_rate < multiplier: + return + + # Reset escalation tracker when a genuinely new window starts (6h tolerance). + if self._resets_at_changed(state.get("last_burn_rate_resets_at"), resets_at_str, 6): + state["last_burn_rate_resets_at"] = resets_at_str + state["last_burn_rate_threshold"] = 0 + + # Determine which escalation level we're at (mirrors usage thresholds) + warn = self.thresholds.get("warn", 60) + crit = self.thresholds.get("critical", 85) + if pct7 >= crit: + current_level = crit + elif pct7 >= warn: + current_level = warn else: - return # No notification for this threshold + current_level = 1 # early warning — below warn threshold + prev_level = state.get("last_burn_rate_threshold", 0) + if current_level <= prev_level: + return + + projected = int(burn_rate * 100) + urgency = Notify.Urgency.CRITICAL if pct7 >= crit else Notify.Urgency.NORMAL + icon = "dialog-warning" if pct7 >= crit else "dialog-information" + n = Notify.Notification.new( + f"{label}: High burn rate", + f"7d usage at {pct7}% — on pace for {projected}%", + icon, + ) + n.set_urgency(urgency) n.show() - self.last_notification_threshold = current_threshold + state["last_burn_rate_threshold"] = current_level + # Persist so a widget restart won't re-fire for the same window/level + self._persist_notification_state() + except Exception as e: + print(f"[claude-usage] burn rate check error: {e}", file=sys.stderr) def on_show_details(self, _widget): - if self.usage_data: - token_status = "Connected" - elif not self.token: - token_status = "No token" - elif self.item_5h.get_label().endswith("rate limited"): - token_status = "Rate limited" - else: - token_status = "Error" - UsageDetailWindow(self.usage_data, self.last_updated, token_status, self.subscription_info) - - def on_set_token(self, _widget): - dialog = TokenDialog() - response = dialog.run() - if response == Gtk.ResponseType.OK: - token = dialog.get_token() - if token: - self.token = token - save_token(token) - self.force_refresh() - dialog.destroy() + accts_data = [] + for acct in self.accounts: + lbl = acct["label"] + state = self.account_states[lbl] + accts_data.append({ + "label": lbl, + "usage_data": state.get("usage_data"), + "error": state.get("error"), + "subscription_info": state.get("subscription_info"), + "disable_polling": acct.get("disable_polling", False), + }) + UsageDetailWindow(accts_data, self.last_updated, self.thresholds, + self.burn_rate_cfg, __version__, self.force_refresh) def on_quit(self, _widget): self.running = False diff --git a/config_window.py b/config_window.py new file mode 100644 index 0000000..2f1b346 --- /dev/null +++ b/config_window.py @@ -0,0 +1,452 @@ +""" +Configuration window for Claude Usage Widget. +Two tabs: Accounts (add/edit/remove) and Notifications (thresholds + burn rate). +On save, writes config.json and calls back so the app reloads live. +""" + +import gi +gi.require_version('Gtk', '3.0') + +import json +import os +from pathlib import Path + +from gi.repository import Gtk, Gdk + +CONFIG_DIR = Path.home() / ".config" / "claude-usage-widget" +CONFIG_FILE = CONFIG_DIR / "config.json" + +DEFAULT_THRESHOLDS = {"warn": 60, "critical": 85} +DEFAULT_BURN_RATE = {"enabled": False, "multiplier": 1.5} + + +class ConfigWindow(Gtk.Window): + """Dialog for editing accounts and notification settings.""" + + def __init__(self, current_accounts: list[dict], thresholds: dict, + burn_rate_cfg: dict, poll_interval_secs: int, on_save): + super().__init__(title="Configure") + self.set_default_size(540, -1) + self.set_resizable(False) + self.set_position(Gtk.WindowPosition.CENTER) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + self.set_keep_above(True) + self._on_save_cb = on_save + self._rows: list[tuple[Gtk.Entry, Gtk.Entry, Gtk.CheckButton, Gtk.CheckButton]] = [] + + self._apply_css() + + outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + + notebook = Gtk.Notebook() + notebook.set_margin_top(8) + notebook.set_margin_start(4) + notebook.set_margin_end(4) + notebook.append_page( + self._build_accounts_tab(current_accounts), + Gtk.Label(label="Accounts"), + ) + notebook.append_page( + self._build_notifications_tab(thresholds, burn_rate_cfg, poll_interval_secs), + Gtk.Label(label="Notifications"), + ) + outer.pack_start(notebook, True, True, 0) + + # Footer (shared across tabs) + sep = Gtk.Separator() + sep.get_style_context().add_class("cfg-sep") + outer.pack_start(sep, False, False, 0) + + footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + footer.set_margin_top(10) + footer.set_margin_bottom(12) + footer.set_margin_start(20) + footer.set_margin_end(20) + cancel_btn = Gtk.Button(label="Cancel") + cancel_btn.connect("clicked", lambda _: self.destroy()) + save_btn = Gtk.Button(label="Save & Refresh") + save_btn.connect("clicked", self._on_save) + footer.pack_end(save_btn, False, False, 0) + footer.pack_end(cancel_btn, False, False, 0) + outer.pack_start(footer, False, False, 0) + + self.add(outer) + self.show_all() + + # ── Tab builders ────────────────────────────────────────────────────────── + + def _build_accounts_tab(self, current_accounts: list[dict]) -> Gtk.Widget: + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + box.set_margin_top(16) + box.set_margin_bottom(8) + box.set_margin_start(20) + box.set_margin_end(20) + + # Column headers + hdr_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + for text, expand in [("Label", False), ("Credentials Directory", True)]: + h = Gtk.Label(label=text) + h.get_style_context().add_class("cfg-col-header") + h.set_halign(Gtk.Align.START) + hdr_row.pack_start(h, expand, expand, 0) + box.pack_start(hdr_row, False, False, 0) + + self._rows_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + box.pack_start(self._rows_box, False, False, 0) + + for acct in current_accounts: + self._add_row(acct.get("label", ""), acct.get("credentials_dir", ""), + acct.get("hide_from_tray", False), acct.get("disable_polling", False)) + + add_btn = Gtk.Button(label="+ Add Account") + add_btn.get_style_context().add_class("cfg-add-btn") + add_btn.set_halign(Gtk.Align.START) + add_btn.connect("clicked", lambda _: self._add_row("", "~/.claude", False, False)) + box.pack_start(add_btn, False, False, 4) + + return box + + def _build_notifications_tab(self, thresholds: dict, + burn_rate_cfg: dict, + poll_interval_secs: int) -> Gtk.Widget: + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) + box.set_margin_top(16) + box.set_margin_bottom(8) + box.set_margin_start(20) + box.set_margin_end(20) + + # ── Polling interval ───────────────────────────────────────────────── + poll_lbl = Gtk.Label(label="POLLING") + poll_lbl.get_style_context().add_class("cfg-section-header") + poll_lbl.set_halign(Gtk.Align.START) + box.pack_start(poll_lbl, False, False, 0) + + poll_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + poll_field_lbl = Gtk.Label(label="Check every") + poll_field_lbl.get_style_context().add_class("cfg-field-label") + self._poll_spin = Gtk.SpinButton.new_with_range(1, 60, 1) + self._poll_spin.set_value(max(1, poll_interval_secs // 60)) + poll_suffix = Gtk.Label(label="minutes") + poll_suffix.get_style_context().add_class("cfg-field-label") + poll_row.pack_start(poll_field_lbl, False, False, 0) + poll_row.pack_start(self._poll_spin, False, False, 0) + poll_row.pack_start(poll_suffix, False, False, 0) + box.pack_start(poll_row, False, False, 0) + + box.pack_start(Gtk.Separator(), False, False, 0) + + # ── Usage thresholds ────────────────────────────────────────────────── + thresh_lbl = Gtk.Label(label="USAGE THRESHOLDS") + thresh_lbl.get_style_context().add_class("cfg-section-header") + thresh_lbl.set_halign(Gtk.Align.START) + box.pack_start(thresh_lbl, False, False, 0) + + grid = Gtk.Grid() + grid.set_column_spacing(12) + grid.set_row_spacing(8) + + warn_label = Gtk.Label(label="Warn at") + warn_label.get_style_context().add_class("cfg-field-label") + warn_label.set_halign(Gtk.Align.START) + self._warn_spin = Gtk.SpinButton.new_with_range(0, 100, 1) + self._warn_spin.set_value(thresholds.get("warn", DEFAULT_THRESHOLDS["warn"])) + warn_suffix = Gtk.Label(label="%") + warn_suffix.get_style_context().add_class("cfg-field-label") + + crit_label = Gtk.Label(label="Critical at") + crit_label.get_style_context().add_class("cfg-field-label") + crit_label.set_halign(Gtk.Align.START) + self._crit_spin = Gtk.SpinButton.new_with_range(0, 100, 1) + self._crit_spin.set_value(thresholds.get("critical", DEFAULT_THRESHOLDS["critical"])) + crit_suffix = Gtk.Label(label="%") + crit_suffix.get_style_context().add_class("cfg-field-label") + + grid.attach(warn_label, 0, 0, 1, 1) + grid.attach(self._warn_spin, 1, 0, 1, 1) + grid.attach(warn_suffix, 2, 0, 1, 1) + grid.attach(crit_label, 0, 1, 1, 1) + grid.attach(self._crit_spin, 1, 1, 1, 1) + grid.attach(crit_suffix, 2, 1, 1, 1) + box.pack_start(grid, False, False, 0) + + box.pack_start(Gtk.Separator(), False, False, 0) + + # ── Burn rate alert ─────────────────────────────────────────────────── + burn_lbl = Gtk.Label(label="BURN RATE ALERT (7-day window)") + burn_lbl.get_style_context().add_class("cfg-section-header") + burn_lbl.set_halign(Gtk.Align.START) + box.pack_start(burn_lbl, False, False, 0) + + desc = Gtk.Label( + label="Warns when your usage rate suggests you'll exceed\n" + "your weekly limit — e.g. 50% used with only 25% of\n" + "the week elapsed." + ) + desc.get_style_context().add_class("cfg-desc") + desc.set_halign(Gtk.Align.START) + box.pack_start(desc, False, False, 0) + + self._burn_check = Gtk.CheckButton(label="Enable burn rate warnings") + self._burn_check.set_active(burn_rate_cfg.get("enabled", False)) + box.pack_start(self._burn_check, False, False, 0) + + mult_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + mult_lbl = Gtk.Label(label="Alert when on pace for") + mult_lbl.get_style_context().add_class("cfg-field-label") + self._mult_spin = Gtk.SpinButton.new_with_range(1.0, 5.0, 0.1) + self._mult_spin.set_digits(1) + self._mult_spin.set_value(burn_rate_cfg.get("multiplier", DEFAULT_BURN_RATE["multiplier"])) + mult_suffix = Gtk.Label(label="× your weekly allocation") + mult_suffix.get_style_context().add_class("cfg-field-label") + mult_row.pack_start(mult_lbl, False, False, 0) + mult_row.pack_start(self._mult_spin, False, False, 0) + mult_row.pack_start(mult_suffix, False, False, 0) + + # Dim the multiplier row when burn rate is disabled + def on_burn_toggle(btn): + mult_row.set_sensitive(btn.get_active()) + + self._burn_check.connect("toggled", on_burn_toggle) + mult_row.set_sensitive(self._burn_check.get_active()) + box.pack_start(mult_row, False, False, 0) + + return box + + # ── Account row helpers ─────────────────────────────────────────────────── + + def _add_row(self, label: str, cred_dir: str, + hide_from_tray: bool = False, disable_polling: bool = False): + row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + + label_entry = Gtk.Entry() + label_entry.set_text(label) + label_entry.set_width_chars(10) + label_entry.set_max_width_chars(12) + label_entry.set_placeholder_text("Name") + + dir_entry = Gtk.Entry() + dir_entry.set_text(cred_dir) + dir_entry.set_placeholder_text("~/.claude") + + hide_check = Gtk.CheckButton(label="Hide tray") + hide_check.set_active(hide_from_tray) + hide_check.set_tooltip_text("Hide this account from the tray label") + + disable_check = Gtk.CheckButton(label="No poll") + disable_check.set_active(disable_polling) + disable_check.set_tooltip_text("Disable background auto-refresh for this account") + + remove_btn = Gtk.Button(label="✕") + remove_btn.get_style_context().add_class("cfg-remove-btn") + remove_btn.set_relief(Gtk.ReliefStyle.NONE) + + row.pack_start(label_entry, False, False, 0) + row.pack_start(dir_entry, True, True, 0) + row.pack_start(hide_check, False, False, 0) + row.pack_start(disable_check, False, False, 0) + row.pack_start(remove_btn, False, False, 0) + + entry_quad = (label_entry, dir_entry, hide_check, disable_check) + self._rows.append(entry_quad) + self._rows_box.pack_start(row, False, False, 0) + row.show_all() + + def on_remove(_btn, r=row, eq=entry_quad): + self._rows_box.remove(r) + self._rows.remove(eq) + + remove_btn.connect("clicked", on_remove) + + # ── Save ────────────────────────────────────────────────────────────────── + + def _on_save(self, _btn): + accounts = [ + { + "label": le.get_text().strip(), + "credentials_dir": de.get_text().strip(), + "hide_from_tray": hc.get_active(), + "disable_polling": dc.get_active(), + } + for le, de, hc, dc in self._rows + if le.get_text().strip() and de.get_text().strip() + ] + + if not accounts: + dlg = Gtk.MessageDialog( + transient_for=self, flags=0, + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.OK, + text="At least one account is required.", + ) + dlg.run() + dlg.destroy() + return + + labels = [a["label"] for a in accounts] + if len(labels) != len(set(labels)): + dlg = Gtk.MessageDialog( + transient_for=self, flags=0, + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.OK, + text="Account labels must be unique.", + ) + dlg.run() + dlg.destroy() + return + + warn = int(self._warn_spin.get_value()) + crit = int(self._crit_spin.get_value()) + if warn >= crit: + dlg = Gtk.MessageDialog( + transient_for=self, flags=0, + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.OK, + text="Warn threshold must be lower than Critical threshold.", + ) + dlg.run() + dlg.destroy() + return + + new_config = { + "accounts": accounts, + "poll_interval_seconds": int(self._poll_spin.get_value()) * 60, + "thresholds": {"warn": warn, "critical": crit}, + "burn_rate": { + "enabled": self._burn_check.get_active(), + "multiplier": round(self._mult_spin.get_value(), 1), + }, + } + + try: + existing = json.loads(CONFIG_FILE.read_text()) if CONFIG_FILE.exists() else {} + except (json.JSONDecodeError, OSError): + existing = {} + + existing.update(new_config) + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + CONFIG_FILE.write_text(json.dumps(existing, indent=2)) + os.chmod(CONFIG_FILE, 0o600) + + self.destroy() + self._on_save_cb(new_config) + + # ── CSS ─────────────────────────────────────────────────────────────────── + + def _apply_css(self): + css = Gtk.CssProvider() + css.load_from_data(b""" + /* Base */ + window, box, grid { background-color: #1a1a2e; } + + /* Notebook tabs */ + notebook > header { + background-color: #12122a; + border-bottom: 1px solid #2a2a4a; + padding: 0; + } + notebook > header > tabs > tab { + background-color: #12122a; + color: #6b7280; + padding: 6px 18px; + border: none; + box-shadow: none; + outline: none; + } + notebook > header > tabs > tab:hover { + background-color: #1e1e38; + color: #c0c0ff; + } + notebook > header > tabs > tab:checked { + background-color: #1a1a2e; + color: #e0e0ff; + font-weight: bold; + border-bottom: 2px solid #7070cc; + } + /* Page content area */ + notebook > stack { + background-color: #1a1a2e; + border: none; + } + + /* Fields */ + .cfg-section-header { color: #7070aa; font-size: 10px; font-weight: bold; letter-spacing: 1px; } + .cfg-col-header { color: #6b7280; font-size: 11px; font-weight: bold; } + .cfg-field-label { color: #a0a0c0; font-size: 12px; } + .cfg-desc { color: #555577; font-size: 11px; } + + /* Inputs */ + entry { + background-color: #20203a; + color: #e0e0ff; + border: 1px solid #3a3a5a; + border-radius: 4px; + padding: 4px 8px; + caret-color: #e0e0ff; + } + entry:focus { border-color: #7070cc; } + + spinbutton entry { border: none; padding: 4px 6px; } + spinbutton { + background-color: #20203a; + color: #e0e0ff; + border: 1px solid #3a3a5a; + border-radius: 4px; + } + spinbutton button { + background-color: #2a2a4a; + color: #a0a0c0; + border: none; + border-left: 1px solid #3a3a5a; + border-radius: 0; + padding: 2px 6px; + min-width: 0; + } + spinbutton button:hover { background-color: #3a3a6a; color: #e0e0ff; } + + checkbutton { color: #c0c0ff; background-color: transparent; } + checkbutton check { + background-color: #20203a; + border: 1px solid #3a3a5a; + border-radius: 3px; + } + checkbutton check:checked { background-color: #5050aa; border-color: #7070cc; } + + /* Buttons */ + button { + color: #8888aa; + background-color: transparent; + border: 1px solid #3a3a5a; + border-radius: 4px; + padding: 4px 12px; + } + button:hover { color: #e0e0ff; border-color: #6060aa; } + + .cfg-remove-btn { + color: #ef4444; background: transparent; + border: none; padding: 2px 6px; + } + .cfg-remove-btn:hover { color: #ff7070; } + + .cfg-add-btn { + color: #7070aa; background: transparent; + border: 1px solid #3a3a5a; border-radius: 4px; padding: 4px 12px; + } + .cfg-add-btn:hover { color: #c0c0ff; border-color: #6060aa; } + + .cfg-save-btn { + background-color: #4545b0; + color: #ffffff; + border: 1px solid #7575cc; + border-radius: 4px; + padding: 4px 16px; + font-weight: bold; + } + .cfg-save-btn:hover { background-color: #5a5acc; color: #ffffff; border-color: #9090dd; } + + /* Misc */ + separator, .cfg-sep { background-color: #2a2a4a; min-height: 1px; } + label { background-color: transparent; } + """) + Gtk.StyleContext.add_provider_for_screen( + Gdk.Screen.get_default(), css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) diff --git a/install.sh b/install.sh index b3d6c0f..9dc5d50 100755 --- a/install.sh +++ b/install.sh @@ -9,39 +9,124 @@ echo "╔═══════════════════════ echo "║ Claude AI Usage Widget — Installer ║" echo "╚══════════════════════════════════════════╝" -# ── Dependencies ──────────────────────────────────────────────────────────── +# ── Python — detect pyenv vs system ───────────────────────────────────────── echo "" -echo "▸ Checking dependencies…" +echo "▸ Detecting Python environment…" -MISSING=() +PYTHON="" -# Python 3 -if ! command -v python3 &>/dev/null; then - MISSING+=("python3") +if command -v pyenv &>/dev/null; then + # Resolve pyenv's active python binary + PYTHON=$(pyenv which python3 2>/dev/null || pyenv which python 2>/dev/null || true) + if [ -z "$PYTHON" ]; then + echo " ✗ pyenv found but no Python version is active." + echo " Run: pyenv install 3.12 && pyenv global 3.12" + exit 1 + fi + echo " ✓ pyenv found — using $PYTHON" +else + echo " ✗ pyenv not found." + echo "" + echo " pyenv is recommended for managing Python environments." + echo " Install it with: curl https://pyenv.run | bash" + echo " Then reload your shell and run: pyenv install 3.12 && pyenv global 3.12" + echo "" + read -rp " Continue with system Python instead? [Y/n] " yn + case "${yn,,}" in + n|no) echo " Aborted. Install pyenv then re-run."; exit 1 ;; + *) PYTHON=$(command -v python3) ;; + esac + echo " Using system Python: $PYTHON" fi -# GIR packages -python3 -c "import gi; gi.require_version('Gtk','3.0'); gi.require_version('AppIndicator3','0.1'); gi.require_version('Notify','0.7')" 2>/dev/null || { - MISSING+=("gir1.2-appindicator3-0.1" "gir1.2-notify-0.7") -} +# ── Dependencies ───────────────────────────────────────────────────────────── -if [ ${#MISSING[@]} -gt 0 ]; then - echo " ✗ Missing packages: ${MISSING[*]}" - echo "" - echo " Install them with:" - echo " sudo apt install python3 gir1.2-appindicator3-0.1 gir1.2-notify-0.7 python3-gi" - echo "" - read -rp " Install now? [Y/n] " yn +echo "" +echo "▸ Checking dependencies…" + +# System GI type libraries — always required via apt regardless of Python env. +# These are runtime type data, not Python packages; pip cannot provide them. +SYSTEM_MISSING=() +for pkg in gir1.2-appindicator3-0.1 gir1.2-notify-0.7; do + dpkg -s "$pkg" &>/dev/null || SYSTEM_MISSING+=("$pkg") +done + +if [ ${#SYSTEM_MISSING[@]} -gt 0 ]; then + echo " ✗ Missing system GI libraries: ${SYSTEM_MISSING[*]}" + echo " (These are needed even with pyenv — they are not pip-installable.)" + read -rp " Install via apt now? [Y/n] " yn case "${yn,,}" in n|no) echo " Aborted."; exit 1 ;; *) - sudo apt update - sudo apt install -y python3 python3-gi gir1.2-appindicator3-0.1 gir1.2-notify-0.7 + sudo apt update -qq + sudo apt install -y "${SYSTEM_MISSING[@]}" ;; esac fi +# Python packages — always use a dedicated venv with --copies. +# --copies physically copies the Python binary into the venv, so the widget +# keeps working even if pyenv switches versions or removes the source version. +VENV_DIR="$INSTALL_DIR/venv" +echo " ▸ Creating isolated venv at $VENV_DIR …" +mkdir -p "$INSTALL_DIR" +"$PYTHON" -m venv --copies "$VENV_DIR" +VENV_PYTHON="$VENV_DIR/bin/python3" + +if command -v pyenv &>/dev/null; then + # Build deps needed to compile PyGObject from source. + # PyGObject ≥3.51 requires girepository-2.0 (libgirepository-2.0-dev), + # which is only available on Ubuntu 24.04+. On 22.04 we must use the + # 1.0 API and pin PyGObject to the last compatible release (<3.51). + BUILD_PKGS=(libcairo2-dev pkg-config python3-dev) + if apt-cache show libgirepository-2.0-dev &>/dev/null 2>&1; then + BUILD_PKGS+=(libgirepository-2.0-dev) + PYGOBJECT_VERSION="" # latest works fine + else + BUILD_PKGS+=(libgirepository1.0-dev) + PYGOBJECT_VERSION="<3.51" # last version supporting girepository-1.0 + fi + + MISSING_BUILD=() + for pkg in "${BUILD_PKGS[@]}"; do + dpkg -s "$pkg" &>/dev/null || MISSING_BUILD+=("$pkg") + done + if [ ${#MISSING_BUILD[@]} -gt 0 ]; then + echo " ▸ Installing build deps: ${MISSING_BUILD[*]}" + sudo apt install -y "${MISSING_BUILD[@]}" + fi + + echo " ▸ Installing PyGObject${PYGOBJECT_VERSION:+ (pinned to ${PYGOBJECT_VERSION})} + pycairo into venv…" + "$VENV_PYTHON" -m pip install --quiet --upgrade pip + "$VENV_PYTHON" -m pip install --quiet "PyGObject${PYGOBJECT_VERSION}" pycairo + echo " ✓ pip packages installed into venv" +else + # System python — python3-gi is managed by apt and lives outside venv. + # Add a .pth file to the venv so it can see system site-packages for gi. + if ! "$PYTHON" -c "import gi" 2>/dev/null; then + echo " ✗ python3-gi not found" + read -rp " Install via apt? [Y/n] " yn + case "${yn,,}" in + n|no) echo " Aborted."; exit 1 ;; + *) sudo apt install -y python3-gi ;; + esac + fi + # Allow venv to see system gi package (apt-installed) + SITE_PKG=$("$VENV_PYTHON" -c "import site; print(site.getsitepackages()[0])") + SYS_SITE=$("$PYTHON" -c "import site; print(site.getsitepackages()[0])") + echo "$SYS_SITE" > "$SITE_PKG/system-gi.pth" +fi + +# Final check inside the venv +"$VENV_PYTHON" -c " +import gi +gi.require_version('Gtk','3.0') +gi.require_version('AppIndicator3','0.1') +gi.require_version('Notify','0.7') +from gi.repository import Gtk, AppIndicator3, Notify +" || { echo " ✗ GI import failed inside venv — check errors above"; exit 1; } + echo " ✓ All dependencies satisfied" # ── Install files ─────────────────────────────────────────────────────────── @@ -49,24 +134,23 @@ echo " ✓ All dependencies satisfied" echo "" echo "▸ Installing to $INSTALL_DIR …" -mkdir -p "$INSTALL_DIR" -cp claude_usage_widget.py "$INSTALL_DIR/claude_usage_widget.py" +cp claude_usage_widget.py shared.py usage_popup.py "$INSTALL_DIR/" chmod +x "$INSTALL_DIR/claude_usage_widget.py" mkdir -p "$(dirname "$BIN_LINK")" ln -sf "$INSTALL_DIR/claude_usage_widget.py" "$BIN_LINK" -# Create wrapper scripts for easy start/stop -cat > "$HOME/.local/bin/claude-widget-start" <<'EOFSTART' +# Wrapper scripts use the venv Python — fully independent of pyenv version changes +cat > "$HOME/.local/bin/claude-widget-start" < /tmp/claude-widget.log 2>&1 & +# Start Claude Usage Widget — uses dedicated venv (pyenv-version-independent) +env -i \\ + HOME="\$HOME" \\ + DISPLAY="\$DISPLAY" \\ + DBUS_SESSION_BUS_ADDRESS="\$DBUS_SESSION_BUS_ADDRESS" \\ + XDG_RUNTIME_DIR="\$XDG_RUNTIME_DIR" \\ + PATH="/usr/local/bin:/usr/bin:/bin" \\ + $VENV_PYTHON $INSTALL_DIR/claude_usage_widget.py > /tmp/claude-widget.log 2>&1 & sleep 1 if ps aux | grep -q '[c]laude_usage_widget'; then @@ -106,7 +190,7 @@ cat > "$AUTOSTART_DIR/$APP_ID.desktop" < "$CONFIG_OUT" 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 A-v9sr literal 0 HcmV?d00001 diff --git a/screenshots/screenshot_config_notifications.png b/screenshots/screenshot_config_notifications.png new file mode 100644 index 0000000000000000000000000000000000000000..1820c43a81381aa4e8959b709767985d249fc6f5 GIT binary patch literal 29426 zcmbrmby!s2z6U(E0#YI&B_kzL0)mn%1Bi4;cS?5-q0%iP3^0UrcMd6C0@6Kn!_Zyt z@^|jJ?>YCr=ic+&_YWR;_On^D*V=2X@2A!T$V!Xd$9jYXfk5tyzk|s`AlK=@kNMr} z;7Xnfj1>I3XZ=pq4g$e#{_}G!@)<5U1o9Xn4tt~M9Jev$;-Uh`Z8>4RyJs=9yvatR)*HfDN|A(88#gmKKR&NZ^uWZ|*!AoQ8h_dA8Z(McPKHADRHOW1(7tB{ zHQ(anS%`yV;$H6j;_P)!++EvEnI=eZ-|rK{^7-N~4X*b~upQXu37ya4$!G1aW+dVo znL!{o?l-Z27ON3*ba@O-{HaoDJ^MRJc-n2RJ3E`HWNd?+7Tq?Yi{?MyZi4Nft`}5K zVbDDDj@6p1%G0q|3lTDh8)JLDo|k*s2~}!OpFRyDX6bBi|Jt>vlyd+YqITPEC_~(W z+$CgHt~~nStE&=q5Xz48WcU8J=H}*}o*pSAH%fywV_?;>y4LKrvo@{^+B%e42t77t z!i|KZ&O0I)TJ&tT5Jw-BlpHaWujPGjah{2C^65J2R1_uZIJho@Pt7CO5dGH7QGf3P4+O&gShMa% z>5m_u+#Fqg8QIMi#^G1ohCoK{`!Y(eDf*%U+E?()#$d`06uBDlNUS?5O( zh_i@awWSP(G5Cd8;1v3em#_+&ep<0$a7^Ud@WI?SA6-%c8Y2kg=bfVtj)4f?rS!Fy z>riM^oxlm)%AG!sbUd5+bpZbCkhkMF03{?wg+Au=_l_rM}Y`KC&Gdb%(CcTS2G zucnyt*n3V76gA#|*3q=eg5T()e6cAbBL9D0;$ zdF?G7@8pMX*;^O8d-4a1dGhXTJ&SkqD{uaUuy2;DhF$H(tVEB-Pn3ToU$deVdP70w zQ6}QCKRVweMzk?mZg?@y78XT`A>_XP`7tj&cfHQwlOWGw&yXA27DmO-e=(53_Jr`K z_U?(NuwF5L2*8}cxVEU#x0ls0z0W&!EUvKOI59o#TC${Za7OLP z8vf4zn_GmSo5*QRjft1#&Knk01E(W?@5v9h(5Af&u$&nxJ5rCIXychQz^ zlh`lq!rw+EW#wE~Yp7{s10&BOTUBArspLDdbge?2|@fR%TvZ;(TxFh5)jn?PajV%53!$k$1J zUtLG@sT!-Jo<9@n+c)f>)|(gxdhSE&t@F*SHfqXP1T_hDz1i7tH+kLVLR0;NKJN~N zGne^m`J$gFm3`7eZaN@AGF-w>cQdU1TnmP8qyh>Iw_x~6C_|#A@^Zi=FXSl{a#JGV))@L zdiM(X31XzupQPRKXQX)^?vIJ)+X_YPRc7eT&z+TrK+ad!3A7jPaQp0CnAX^= z2MzRJ*%|g0wYWjh(!wyPnM(%^Pz=P*r70aZhk6NH-^S!QII$o; zIdy@&$mV2ui&2><RcH~7)l7P!?$o1{H$wVLlog@K-^1g@n4k{`Q?~Py zMFf@ead^jo@n~*&_j%t7hty61V*k>_#y%AC3YRc}nTLU+ixpe4;~p)r*L3dBc3PrK(aW(Q&Q zd!-+?)J}eEeQFXWUx&gK#(MkKZrK?hqxom?p4Ubh1p z8(9bAghzkgw@~-uke-W8#?;Jxn;?J4SK6tiRqR`Ht&%>`^PJ%9#$o=+bVR2?JSFAX z(S};thES3v){GiUPJm@u!27xr!7msJDm6{=GU1#JcPR>)8xY8qqTbOjqWMe}yYIbz zxL^hpE1k5SzDp4vMM7UDfZC|6V|^Df1Y;`*2#u_)45M0UIueJ|SOG|fAC);oZ@Sq+ z$^ChDZvX=Mgx3y`-pq|%@L$KDYCwGxd<^dUr^_?Tu4|6*NoS8isVv)$;K?wRoVo+< z%)b?y0e(?`1x5PjodrFVA94Czj|mP}I*Sy6uYK$D%%8%-q>K z4`tJ?3tg7Bye)81a`V%=_jEu#8!Ip7VbS`oC)b zUwrI8U%sKBp>bYpXr}Fc&`qAn0l_gp=IZzi@SWlBAA`}+(S__q1FMxZOgDc1^sPZr z(b1*lYMeYIh(u6dj`dQ}Bn!Yhrz}SnI`psS9t@vIs}k+V@F&>%`t#%CYVeZn8kZw1pGF?Ev(H&k?#9=ntmQ;c zm(jx6w6rtwM{6)>xRSc(2aB7TIFR2GH0|UL^2rmM#8^ISsm)&(qz-2!MStG)8977c z93BowM^|PkSg9Ay*-h_w-#k!|5kMwt>y(TgZcXp?@L8!aFM&S#yrm(?)2g*OJM5*7}RF_5L2~|fn!i5NlC2q^oNLgH^;%u=h7Ma({)qS z9>*8}o^#I6uSYUv=jAyJWv6vTUGTkLOo;91e008=@eG%e*Lwfs*!js(G4t7O7aIky z{U_aM>dS2gC-f`jtl%2!*>lj#8vQon1nm!yyASL3M4^!|k(@QQ7tO6Lw5EilzPhSW zHV^zD=p88-!Zju)xL$HRSNkkt)yvx zXzifG>l?i!<;d`O-Qv1M_Q#>zYD=?z?lP`2BfJhhRq#?W#V6dRy^h$g`}$)}(eVh= z^}|hmcDIGX{f&L=qS22Vnc4iB%5VqIvpc;poF9CAI#&mbPnV=rR7Ux>+%o%UH^)ji zSy?^MHSpx*WY0Zh#^vQvg~x6L!>#KL#)LWt#R)z)NXt+okW zny9Fd65PA_r2>8Wn1yBjR}521pRBCxM~)c9>`YE$X;tQ^rwT^=Ckvbm(uy}TClxp+ zOGHE}XlZG=xlQ->#8|b?_k{g$e*qX3=CL2KwA5NqcWQgQO)xg*70v_U!2{!%Cr+Du zIP3_0PxYPX0r}MMcn|xp(wK6 z>Khz9ZhvxAJ!KgH)2o;On~a<#3yNRw{8;?eE8**km@G1ks$Wx)^G?&#r&*p?XAF}% z@82^Kl$COM?sf<{R5^a0ah-0t`N^aC$a8xw6$Ujmv#>HVVMuNUe}kb)4OWbmijvZ{ znksS`X?0K^6c5l8<$Aj2e;4(}YP8m3E3~^>1rPZxmN}y9y3Dd;b%o`q%|^QobdESb zn_r5Vpby2l980cF^!^rsYIjsbTMoB-*+Fw(=Zxjm(AW2A>J^sIWRn5Q3YFYR9W}Mv z1Ve)W%8B`f^OZ%!#t!?P#>9-l6hC|blro^*sN^}$-f!Wse2tT#d`+>|utXS4gX4hK zno}ufNaweDp3>GXpOd8nZ+w$qm`1f@S584)Rv+y_k_Dvl%}y*2P7_!`Dp5mas}pJy zE6Az%_RM$C{<2jSJPDX3qWa9ALtY$qyEfdP!tu@#YkIKa&4j*1`!NegMAy*3bqA7l z0V)}DH}j5KJFt*QlD-}dlpo#*e`i1QlPYJ3*Zov7BCF}>Y1GA`ZsIz&4-~rV%#RL# zWw#`Bx-yUJH?LgpYTKJYS(`g>W+a^`c(9T(JzeDx(X|-MuG=|%d3epq=q|+acFmHM zqNHLaZJcg2oGMz#eOM717!X7p)G}%FaCmtt`>fs29C6jFp)XP8c(GJap;;xnyyN`s z4xJRrg2TyinO7HVY?~AD@d7GLwKWV*tMV)ahDJsN;qqi;j3-p{)t~Ue zkd4hw?FKQ#(!M64zK@oUFg#zgz-)jZoL==wgv5gv_b6L&p@E?RbF9?(f!j;4#GH+A8h3o6EEFQH=jQ*!u5;ycib4Qa^C4JDY;XBzPrA&6Yw_4dZJvayE_D? zs1Em7`K`-Vcibj{Fugh`z{^*0X$#E@6Fe=xp+Zq^*yLE!F=^PWS1u}&(<(#u zETW6_nq5|Tk6!s*QeXd6j@41|ip+??T9c-aEB*l?bq;eZ@vYJk9dY6DT24h;(0dHf zoEFpP>IEvFW$xf^l&JjT7+bvIj_Y?}EOge#=CNb0J4+hWvO9gn3SW-rsm!?CMJOl0 zF=-16Z+bLmEsnv|G8*O$zN&Jf?uyj-FHS^?%&04I^QW-s@n{-tUss zvl%`2p~Gp))h_ACW-GaMTO*03m1mtHOQ307hKEjNo(_0Mk`8`Z0Q`OxI{K_Xbau9ht?vk(~o98X_ zK??-7?$xu+MV$?(SAEar-X<01m0gU6V_}HoJj35@-^0D=HCo^Crhs@);(4%Jo$NZG zsmsRaCvRNtE-iW2+`GMS|K~w&Moq(g{QnKF{WEa-_wKITttg?TPj{d;H_hHWQnLK$ zp-l&^+AK#LTP&m(y6o*GSzd$Wk4Nsc>SQh_g^^Qg(Fx8D-L9;AcucuDUm-JR_6%;) z5=iY^bqn%o{d&*zf5DLdmuLEKK=dCQ`EvFmJ}b+mrG*Z*V6B3T?LkE5Iq`17Qu_>+ z7o|e|{nu7L=46hJkGpnMZ)2t;kg=w2%3CJhaC2@zc~3W!gxkP<8mL`E%cAJ$1%fB( zS&H|FC5#;$Or)e-WO*I{gzO)7fh;b*pCPkF&Gm7#Q1?N=yR2YJ9t^ridJ0|pOoO$} z``(c_ErEN^#*XuX?FS8w?5!V?#IH>HFPOV9Q`K`DV_uHcY1HL^;P!oL7kzH>p17w6 z&iwBUnF-R;LCsAswl$WJu=p_K5&kYNjz85ZlFWB}d~IN0&;8E8fS5l_cX5$iUCp#N zQAkbOv+6%vdVwTNp>oH-B?(PJ%IWdg!46{trYN&${w)+~l5BJSzzrqot;S{ux&g&CAN5{AQ|N0-_` zX(_!e{PYd4wW2YqQ0kzRMEV-Y;7 zlc*=czQGPCiu?DcsdfWHhuLw2o4L7%_qpb}R!hywj3d6?hKsGON#Cb(lh8shV3UOU z_;r0LR|Q(*!?)G~r47G}J+GM3I4m2zeROqatDV>u58u6XafzUptSl(BcKj61YHAjS zee>;B7;k)e>3-MB$L{8Upj7#mjtKadQW^;4H(c_FCS+-1U<;f0muj6GW!MFn{vml1 z)*s8-PuI*V?PiRGovLWYkG{j&{qxXHtJiAjgwZ-gkSq4`xd<)|BhBEhtqDJZ9F$UP{OuE zv%M*G*ZQc^e4ev}HLZ0#{ro`CtKs*SLZ)K22ir45TdX7tD-x|8H(dstoEi|hBRw3T)IJ?xj(jD0c7Q{BUg^+(h9~aMvZxuG=E*ZtGHNF7YQlZmsi8?3b z9c{5}1q71hU9-yPOF)qq=~op*ep}k++T;REY&2h(XYfrde#y&1yBiSvc9fO;ZBJX^ zJp3a+{KsYacQ#3#D({}D4uRZ#?v~K~x{3YQ2bSLADa_BgB4)@Wf1&3eqWSYu>I!~w zz#|q)vFKQhqNaAU4)61yH*sem!b)^qb21yr=eJuj_%lFt*sf@H98QO|Uvep}-aY|l zW@f{A>O*5=I|~u@S|vs;EkVm?veq5nNR)GrORd&-mjK$-Dl_#t63gQ9uD#ipEP|~X z6~(IgXi#<1^5}4ryllKGM=j@vj-+r?T2xf}V~1N1bNy-KLpg}?O>Sslm`pT}m7JT~ zTM3DQV#7pdJjuV%p%BVG%W$Ex5g&fvH##=Vc-cvmIz5(8@?w`z z47;wWsOVu|iXV@qhOi)CnM71grNjDZkv>DV;&W{e)W$*w7W8z}>gpIU!8{b|IVb0G z58u}1y7pD2^Nzy3d$A}xl7+>Gxhgv*N7_}gjBz|xy3#$e83S^1a^)_2%1IvAAh+vg zwAk=T5U-oLxVh~;nO?ox^uoKWGU^W8na?03XktH_zDl9wjPWjv@)|N^juzt@a#fwqmxZ1w7id-MG&Bn4df0XTqBzG?7bL#2+ier_ z(7U?Sw5mNYwb^kldRfz@=i>52@OUQgg;`V>IZs-K{8sG|V##PT_C*(%PlH=*0&E3< z)9uN4y6$xIXy@nWV_|%+K?rWauSBzgOI0L26345ZkWI56EiEM_2l2zz;+Z2mv$MfV zH#BSsCUg38B~@Mo5rwg-o*Ej;s9mRLzhk^H59ARBu9^K(I?s*85r=}Jg zn|m)_7@zkvzOr3-ZL#SB1-QJ~qGF1M2ulPCnzF}CR_q=2pKaB5vH$YHUJeSfa$ilm z;`69UE_a+BB1Sm^*K1?dS=rs)JyE90o~_!TTU13U;ll_NVba188)k!0*-Z16NH`Q&H#2)bhg`V4#fW<>w1_35-@dIazG_oSzfW zbRa+R{M^4-ihdlKW_2a*#^*DQbgnw@LP6{6_ghLZ-t1tFr(N?OK zy-Q^yTG*{H_nD*g!32-hwopnz$Bpfoh9)Yv?LI*IW4n#&#&1@N-#R)E={6E@w(iw* zntDK%MHti+6x1drDte+>oR!sJj$vk{l7aR0J)eA2-r9OQsD;bu*GuRlE=JAAm6LfB zn_6XSdruWCgTiERG(LABb8{Oe^T<6AtNGb$Fvr8+;3?hPk(rq*)8|W0tE*&sMU|-w zmf`PI)LdswgMNN6v9NgmPS(#SzzSuU>?B(-lBM{ZFyPO`elC}y#6S=}WJ&|An0Szk zzGex1Q%~hOkX|;B(d)5Mqzy1gS=kGLS}$O<_6l7UrW!OXbZ~-w4wdt*j;zM2HJDFL zEf&Fcb$D6|UWfacqsJB;ujcAfo1aQX=3y+7ys5E~mltPJj;OxNURm2yE=e}Fo8D_S zduxE}(f#G#k(%vBiF@|~%F31#-4A1ymX;)06LuODSmd>lo-1H!=pu*0g|Qin}pw|3TFyw|G}2vl;>+{rHu~?QUxaGU4WG zkny%VH^e#wxGMh|oBSh~Xh)hs@wQH<_yI5>*-hV48Rpc(uj5LqP$5n{!LwEn22i5g zh#=r0$Hv@#XtkCn=ijpc-A4Buup#*G+mUqx>&TvRyhjmFND%`BG_y|~hzO51n;ejy z4jaF6?wrIF)vwq~{ek>%?VDjydV_M=1}{wV1|CMx&PZZrLv7Fl8IDd zAE22w{R(yaD*B9pVeJ>!W<ZT^e>7H+$iun4q z&h;P>2DOpvi)(JiL3AYzCFEs}uGzi{U}R#lX$=s3u3b z4Zmw#_U@opmwMcp_%)U;Y^LiTLc=R7D-Dh?X;M-zT3YNB6#NQQcJ4rTiEVf-HaG8! zh2~bM1W!FXvxrpBg-b#EKNj!p(u=`h4)&!V6wNIxY8T>z)DzT$$#qqSEqMUK52K#O zSdaQ=W+>l=%(SM-XBlvixg1$7^~66zbO8Y=Wpkp!jO*l0JiPs@x`MoK6vC=hCD_I$ z%d!GVKJwA_mr*S&SvIGHP2;5~j6-!HRs3lEAOwDt@9*z#`!W{|H8fmTS6`pRY}HT# zJ@P%h0x=0fqeewlT|H^QEb>F)@|9iVd!9w6;D<<4{UL#qq4JK7l0{?(T+&>PtvSwpHdn{#vVXm5w+DBfdU4}S-(+Se(vAKf&94$q$)#@E6ZF?pwX zF|oWrF6-=!kB{HPc@oWKB4@k%)owX-e;>=f2eeB_PAM@BT(+O9^=MbAsP- z2lJpTBlZv3tbFo$(lSIbyHd|eDM$P<{=2mIC1dzHydX6(wOC_fY6=23(Hw@Ip_F{5 zMZ8_E{_hxx2*PDaTQ62@Uv+hID=M0IcOkz;aMIJ!9X0x3h0r7Y$#^hSmMzD}rRnL9 zFtvPt@lAE+C;*dolnw4A=_cQVY`2(~B_UW>`7i<%%_r5U)4v@>8$ySlDr{++%fc$na-m zvnrM+n}^dt2s#$S-y6ogC9q@JRLzV3@Zl^uWytIH;z`~{#ZZDqm10M+xfVLLvhvN} zd2vQ_n8GQykTyaad>6%0K5D1TVmO_TDl?~#^swY9<3=QAv zM)DMZRa$H-V=4!;P0vL4WG(4Uct)78Uj%WDxCS^bE-_nuF( zxA(rUZ;7k>69TdjmPb;KE+?Y#(&}obi@%fR736JMi01)gu8G9PHhvI3G+tymHJzW< zgiXQ(N*5wgs0DeyF0#7f(U0$cG4kx1nvMtb%o{5or)uF@RyqrdQc!46%khx1mCchB z(J9LkRaPy2EjgQ$(y1y?(%r>|V5eK9#svD`?U1;)bfq$^U!wX~)guOFe&*Dt<{pfe zFlQ(xKK`~>QoNz+CHe1<{W!OU>k%Ka6fd-tSi@e#sA;f@T}9pcbnS*`$sNK6DXSY? z(cSSh_mJe$lEL(YERdfE*e(x1a;ro|G6^&1OB3GycIrArqy8(;XZBzSoV(UfK?8yh zK?6hgpN!uBu_H5k1D5-Wgtlyd13uvJ~)(Pct(={;J-()W0}o0ghAE z>&K6ix3{-}mhV$$#slJV^`gO~WNWv9{>RqPjXc>Sk&dzDJ320E^KjX!#j_2wsf(IQQ)v6A`_;wP`>-})n9#-QgQB7$JpMwR#m>co znq6a`w*)n}ol8FUHTqza?6S+5oAcw9KwjUy#bfn|oV@bL{qyS|@>yK5M3($CZ>C07 zsB3@}9#2Y6PEvE~H0|EI)Qto#LCQcGHLv|w|Hj6~bSY$!`PecgB_)!_YH)R&K{}2C zd_u(c?{RT)ll3o}8k_M1_9GY)&kt_k@k0p%{24hpjb|JEP*X4nY33Jr?Q4B~VYxmP zXkd`UM{{%kz`%QWh|$gRW|)+|{tbdD_oL)DUi}ZT3=J2H0QG2%|DsjA&~mU|AN|4% zlOQl5A#ud2x;r*}Oe zY|;`6!BKqEiyR$&2?nVzm5Xa|%zPL)1gEDG@>z-nBLsbQ?tCNpnskxx|EMW|n!NI? zi&eKdKhDZga!()Pwia4i3^|_hs<5^74F$AU0&QQgjfQ)=obMQe#c01a7@%YRU4s?T z^<-{t4wUbK<702lYR8(BrFFpcVFnzUm&FtjS)5Evyrd;~5T%?~OiWB1@@cZL*zToQ zb{JMRw#Ca!$fs+}l`i@f6M!=$r|3s~lh4kPv2&TX=dHD3m6B3}+VWB!9FU|W>-n=T zcZWZ@V`|n5U*8xj>7Fg*;o+gD&#sp2Q}CIY{bDd#d9PRC$SeSMdIsWRnCh)pHghit z2u`%16#HF1BrIyt-xscQ0PDTzPm?&y+4x2$kzboTlBK|)r#~<8o;Z%*1*rH$B#1)S zgR)@|TG0NzRo`byJ~#gWJeK3;IItfrQHEc-jTU!7+A@Z7d1(oEYlWSjHq(9U>Z6s_ z4ole!26lc62f2@|EyATgVKwNpuXG#P>FGfc zJUcr(#6f9gWi961U;^1ZkBEtJINnauakAa4ex%IYwK-OEIBwPu{Sh;H>+Eool#8)8 zEp4?4Z`xxaoPmR**vz!&wmvK5F6xIi!QiNhkhzM7gapu)Hyy*;A`%i>7DHgrqA_#J z3W+4y>>+W$i-(A}vc6~)C}io;5}I6DVzKJAT^u1qGcVZ zf0kxuXB#DHz#77Zb!b}Kc!ETr$am|L=2GgYAOJc_R~6vpb!hbUsL+faC7s= z@12ak_oTRU6yKb5R(de6?>KKb21$=B*pHv)&_i(e+mX+#Q54etQea{50&0vAr<`2^=<1Hx}&|xnvVeaz0@klDRL1*Qt z7>w+6SLb38y+Tme;>vU}gk5LG>hQIm9v7&xB`8i>UAv}$fIe?vJXLzoSX~c)XFweE zbWn4Wot9AbZ@t^4$M+@B=}kYf4#b9B|49xq%Kv^A$<1Rt#7{RmN39M~k$XFf83MWU z*Sqoiy&NLTr~x+r1}{HNo9)vir5tVeCY!crt;uSCq|?-g#{0l%LQa%#qHA349MuD1 z?IZFgq%!~Jh4Z5(c7=={iWjDmD$$kY#!@QFhKh>Epvb=%Ce^?&B{wi zL~Q9;ogY?s0zLC;cyiLYg|1d%Ust`xV-LB!zP^sBt2O|7lU2=*lEcARcD4d+9}TxE zP%E2LW74)FW>IS|F126n*O4J2C!Zc3CfrbkJof%l=lMtIR#o+y_Ph#F)^@+#UyV@8 zDLdmZG`xet2g0PDxw!_S;c}5rB+<}#*?hXL+o(iIk)qnUh+9A)MccI+myV8(fq~)3 zs=JVVM;Py7p@YHm;^&R9Kaum!U%yl>@cA1q*!cPR-6ktLfTF*#v0+_3 zwYZqZiiq%!Vs^0^EhOmW-*W>>ET(O6Ft{Zs@%_8szv8QB&~WN~TtXnYHR^|5>^jxM z7XjiRAR`ljIZfBDAnMOI_j>DjJue#}#;9txVZ%T{KKIkX&5B+eY;1{l@BG8Ugj`EE z)Uv0{MS~f+9));B-p!OeSfJjD_ z>5#G7W`zvcN3^Lt&trg49nb5~$k)#P>*7dAUahL2)LuqZ)X_KtZ2g-PD925vEX7lh zE8}zAXg}E-@-W`2i=KXZI)mqVe8IPMej#+FN`rqF1Q1ZItzB(9M%`8Vm*;TWZ`+Gq z2v%*XeSKLT#+)IFhYuf;Wyv!_yN&kZUO6EUh|>$P>yR7b)_Xu_-dH{B45fG_s}8>m zd^os98_85vY9BVS2^7=r``j-~0@_#Y2S>Dn0|RMx6M1Sia`BdWue2;IHi5Cw8_&13 znt|Xy+Zb?gZ~*oOYZE&OYaAPE-9Z}Sa_e{p3zI6n&8$|+{c&hTQ5^qakREj_?Ez=D0nEHyVYsX1^zn1K+K+ul%iYM7k@Zk2761*eWaVv-)U9e&HQl4sb+*t`Wo0D0 zdtx{FtgfRW14l^xf4g{E@A+2GF+P2omXx#}X;kZ`{2a-bCjY959bJ+1U+X7atq2ix z@J30ge9Dps$+lHdR#paz<|*rA!Ti>(#3xL~bg;m>+H>)fxQXnXI_#})! zJUm=e!H%2M?%M12TSuuJ!5&Mylge^-SAHn`ExbpRnj_MRew+54m z!Mtv#Vc)zCB2Oh_la`3LNs2iU&2!dJ+iK0XNxv(;Pq&;)+mwNbO-pgY*_Nkll58-A zD!avaFvy6Biilh-@zhJm^x_A#(9t;s!5aJfZ@4GS8L`_MgiF#3S?lmLXF#QT=-7rI=+-PP^;WUtc*}%h>Exicz^hIpkYH!2Sa?UhDl8 z^+Y%(4EPZr7@^OYIuB0EGZH;6;nUTg1L+PRl;yv&J$nIEtW0^bRhC7k=k*F2dFAEh zS0{ZJdqTZ=s(UTFixMHCO(%OV*lM=>V_&RiNFfh5N_x3?c#iTeyI(UC z%OnaS`COHhlwzYZU3NRPk&^TqlMoyokIT5h|43_;xSjA)ouTSd(-uw2%4p1Ar-*Zs z;%MG3xbW&xWct9?#i~*H)nBv9UX`tDcv$JM_*nqVwB**s*C&weg8%S-k87mw(mE$H ze=59IIW`k&`TuHz^CmnAkEfEpkZVI8Xa3EZ{2!ACf44aQDk2Z=cDZifJE?6e20IBj z*21V<%lSg=-Mlzu^KG(*ff3L>=jnZwOn0$Uo!JSu5PcNFH zDEeITW3g)5+Wb-Dj?(b_&gdp_e16Aj>4CBogw**fkMfM|t5~F&@YF5H&--|71Aq8m zvT=e->-I)(bBrPs0j0m1YV1E6x1h^ThuQ zT>d@q@bA8nH~W9in(6Yk!MgRRnwlw) zzD75r{)NEnM*K%TN29G5+FgTM+V979`xexDz*m0nxVSt}&UYJ|CYn|a@=s1CH2&r6 z?XOvVD243icfu&l&T?$20pSuqpm{P7X28y6g+BjmHO86)XBe-AnK4b4gZZn|TK54WJRirRu$M3lNtmQ`o_!)#=U5#Nk~k+P3b<6~-~9Qh z!Zh+hji}8oPhZ;}-hi_0Q}yG%P}%v3;Zc?z+AN!Bp;kq7iu`JT7;hJed~@OQ0kp zv#LOXQRaFP&-T=%s-KsW?5kSAGqXN*Ic@X_%+7W% zF}||Dy8NK4`z<+{4<&S~e?W}*;grkLuZI170yT?+Bw;gnY^-iH2nl7R;~qIrX2?)y z<_i+(1dfl-H~Go*bf6ZuXC1C-eE{q9_+`S9wF)KfGXe+4a%~%3kfljU3GqLmnlmOf zHOrBa!LwgUKT|VxaMahAg2M$6&+_sRYU=C$f76Jqc{eBZ{*sSvHc!@yT_ID?>`l@$ zvfSzDjVh;ldZa1;XnL;h`XFr70#I5mrq25PdrQ|-f1nhThIFIfM2I_qC)cbNWYDv< zW5AcQ+w;lHgn=;QFfbI*g*4FGI#v3h;NW086a|)uB+NU|ALq8v7(G!S$mXP_Ep{1% z3rNMs^71oe9RG^(0^Y!_TNNT=IX5I5im6hHUOa#PWIG27as!Y0jHd$4o9I72u?`|i z1(n;)0oVcW3aqkoYibyp|H>fM)QB7RP;4x!5sJg??e#tkfzzdSU`k(;mL+-EO_N+) zeDLDMBDw)xfT?(UBvlEGku>30trZf5!GMg`7Ph4WPLqm;*03-b;}8rDD^c*4vT}3B z`1i!U`XSqlOXc>gB2a}{>iwf+dAfJBwumlqSa&yxyu59iyoi12()o$)^eT>T`sgTa z8b^Ln5j_o!6l)l8z>P`-SuJG|tv!iCj~=G={Gu&V(JLaokftO-bQUx7L?;Mxa65jL z9vK}V0y4+y-24Ek?`%0P#FEf$p&O*hr06e#$%L2~qg`fxda4c!!`>$O`+GOCW5f!Y z{OTWZn_urLo-`qq*4IDSo>d?u?1#rDcd0#+;wIpfl#kRK zw&-sz?Ks1rNk?rP8+}w^ZE%W$s#AO?H6Ol+W_BC`2bU5mK{s35-I);bD>;>y3QQje z!Hc~GeJ|eeFo-TBWP0_t&PQB41c(@cjb>==*UHBbuuPCnp7jczoUMfJd{jDEpI;w$ z??S8}%+5Qj!C;C8263{C-#Un3sUtiOkodNGJ$$i6Fp&{wGoVI>m#Ct|yV5FMXh`PV7I|fH+#lE0NcOBm1XMMn$AhsN!Tb z>K(1L1i75DQ>SHxbuZZ2%FC8OI5_b$ygjYFC#1Z-wT zB@G7shDM>bD2mNZhq?hWoNidElCh&>xs!bqR(ZP&N?9L&rpo9==z94fwWcv#|d z22jz~+zD^|{L~y=)IZ(2T0LPHNP9 z4(yKE!_P0v%Gz#Sr#bjl9i_r7{_fq#YN@Mxigd<{N9xxXq;zLDVmPUh{3jk;{<4f% zg|0x`2R_m><~Yh9Z)K49pOn;D@b97qSg=G&Tl#G
  • -(VDofVQ0NBH!N#UEbc5Y; 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 literal 0 HcmV?d00001 diff --git a/screenshots/screenshot_tray.png b/screenshots/screenshot_tray.png new file mode 100644 index 0000000000000000000000000000000000000000..830ae604e03bf5052f0cce7f6cae55688e487c19 GIT binary patch literal 28672 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 K$&^YN`TqxT$y}EJ literal 0 HcmV?d00001 diff --git a/shared.py b/shared.py new file mode 100644 index 0000000..fa2ea35 --- /dev/null +++ b/shared.py @@ -0,0 +1,101 @@ +""" +Shared utilities for Claude Usage Widget. +Colors, utilization parsing, and time formatting. +""" + +from datetime import datetime, timezone + +# ── Colors ────────────────────────────────────────────────────────────────── + +COLOR_GREEN = "#22c55e" +COLOR_YELLOW = "#eab308" +COLOR_RED = "#ef4444" +COLOR_GRAY = "#6b7280" + +DEFAULT_THRESHOLDS = {"warn": 60, "critical": 85} + + +def get_color_for_pct(pct: float, thresholds: dict | None = None) -> str: + """Return color based on percentage (0-100 scale).""" + warn = (thresholds or DEFAULT_THRESHOLDS).get("warn", 60) + critical = (thresholds or DEFAULT_THRESHOLDS).get("critical", 85) + if pct < warn: + return COLOR_GREEN + elif pct < critical: + return COLOR_YELLOW + else: + return COLOR_RED + + +def hex_to_rgb(hex_color: str) -> tuple: + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i+2], 16) / 255.0 for i in (0, 2, 4)) + + +def parse_utilization(raw: float) -> tuple[int, float]: + """Return (percentage_int, decimal_0_to_1) from API utilization value.""" + if raw >= 1: # Already a percentage (API returns 0-100 scale; 1.0 means 1%) + return int(raw), raw / 100 + return int(raw * 100), raw + + +def compute_burn_rate(seven: dict) -> float | None: + """Return the 7d burn rate multiplier, or None if the window is too new.""" + resets_at_str = seven.get("resets_at") + if not resets_at_str: + return None + pct7, _ = parse_utilization(seven.get("utilization", 0)) + try: + resets_at = datetime.fromisoformat(resets_at_str.replace("Z", "+00:00")) + now = datetime.now(timezone.utc) + window_secs = 7 * 24 * 3600 + elapsed_secs = window_secs - (resets_at - now).total_seconds() + if elapsed_secs < 0.05 * window_secs: # ignore first ~8h + return None + return (pct7 / 100) / (elapsed_secs / window_secs) + except Exception: + return None + + +def format_reset_clock(iso_str: str | None) -> str: + """Format reset time as a clock time, e.g. '9:00P'. Used when auto-poll is off.""" + if not iso_str: + return "unknown" + try: + reset_dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00")).astimezone() + return reset_dt.strftime("%-I:%M") + ("A" if reset_dt.hour < 12 else "P") + except Exception: + return "unknown" + + +def format_reset_clock_7d(iso_str: str | None) -> str: + """Format reset time as day + clock, e.g. 'Th 7:00P'. Used for 7d window when auto-poll is off.""" + if not iso_str: + return "unknown" + try: + reset_dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00")).astimezone() + day = reset_dt.strftime("%a")[:2] # "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" + time_str = reset_dt.strftime("%-I:%M") + ("A" if reset_dt.hour < 12 else "P") + return f"{day} {time_str}" + except Exception: + return "unknown" + + +def format_reset_time(iso_str: str | None) -> str: + if not iso_str: + return "unknown" + try: + reset_dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00")) + total_sec = int((reset_dt - datetime.now(timezone.utc)).total_seconds()) + if total_sec <= 0: + return "any moment" + days, remainder = divmod(total_sec, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, _ = divmod(remainder, 60) + if days > 0: + return f"{days}d {hours}h" + if hours > 0: + return f"{hours}h {minutes}m" + return f"{minutes}m" + except Exception: + return iso_str diff --git a/uninstall.sh b/uninstall.sh index 2f08deb..df99f5c 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -3,25 +3,54 @@ set -euo pipefail APP_ID="claude-usage-widget" INSTALL_DIR="$HOME/.local/share/$APP_ID" -BIN_LINK="$HOME/.local/bin/claude-usage-widget" -echo "▸ Removing Claude Usage Widget…" +echo "╔══════════════════════════════════════════╗" +echo "║ Claude AI Usage Widget — Uninstaller ║" +echo "╚══════════════════════════════════════════╝" +echo "" -# Kill running instance -pkill -f "claude_usage_widget.py" 2>/dev/null || true +# Stop running instance +echo "▸ Stopping widget if running…" +pkill -f "claude_usage_widget.py" 2>/dev/null && echo " ✓ Stopped" || echo " (not running)" +# App files + venv +echo "" +echo "▸ Removing installed files…" rm -rf "$INSTALL_DIR" -rm -f "$BIN_LINK" -rm -f "$HOME/.local/bin/claude-widget-start" -rm -f "$HOME/.local/bin/claude-widget-stop" -rm -f "$HOME/.config/autostart/$APP_ID.desktop" -rm -f "$HOME/.local/share/applications/$APP_ID.desktop" - -# Optionally remove config (preserves token) -read -rp " Remove config (~/.config/$APP_ID)? [y/N] " yn +echo " ✓ $INSTALL_DIR" + +# Bin scripts + symlink +for f in \ + "$HOME/.local/bin/claude-usage-widget" \ + "$HOME/.local/bin/claude-widget-start" \ + "$HOME/.local/bin/claude-widget-stop" +do + rm -f "$f" && echo " ✓ $f" +done + +# Desktop entries +rm -f "$HOME/.config/autostart/$APP_ID.desktop" && echo " ✓ autostart entry" +rm -f "$HOME/.local/share/applications/$APP_ID.desktop" && echo " ✓ app launcher entry" + +# Temp files +rm -f /tmp/claude-widget.log +rm -rf "/tmp/$APP_ID" +echo " ✓ temp files" + +# Config — ask, since the user may want to keep their account setup +echo "" +read -rp "▸ Remove config + account setup (~/.config/$APP_ID)? [y/N] " yn case "${yn,,}" in - y|yes) rm -rf "$HOME/.config/$APP_ID"; echo " ✓ Config removed" ;; - *) echo " ✓ Config preserved" ;; + y|yes) + rm -rf "$HOME/.config/$APP_ID" + echo " ✓ Config removed" + ;; + *) + echo " Config preserved at ~/.config/$APP_ID" + ;; esac -echo " ✓ Uninstalled" +echo "" +echo "╔══════════════════════════════════════════╗" +echo "║ ✓ Uninstall complete ║" +echo "╚══════════════════════════════════════════╝" diff --git a/usage_popup.py b/usage_popup.py new file mode 100644 index 0000000..fc321ec --- /dev/null +++ b/usage_popup.py @@ -0,0 +1,215 @@ +""" +Detail popup window for Claude Usage Widget. +Shows per-account usage breakdown with progress bars and reset timers. +""" + +import gi +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk, Gdk + +from shared import get_color_for_pct, parse_utilization, format_reset_time, format_reset_clock, format_reset_clock_7d, compute_burn_rate + + +class UsageDetailWindow(Gtk.Window): + """Popup window showing detailed usage info for all accounts.""" + + def __init__(self, accounts_data: list[dict], last_updated: str, + thresholds: dict, burn_rate_cfg: dict, version: str, on_refresh): + super().__init__(title="Claude AI Usage") + self.set_default_size(400, -1) + self.set_resizable(False) + self.set_position(Gtk.WindowPosition.MOUSE) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + self.set_keep_above(True) + self.set_decorated(True) + self.connect("focus-out-event", lambda *_: self.destroy()) + + self._apply_css() + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + vbox.set_margin_top(16) + vbox.set_margin_bottom(16) + vbox.set_margin_start(20) + vbox.set_margin_end(20) + + # Header + title = Gtk.Label(label="Claude Usage") + title.get_style_context().add_class("title-label") + title.set_halign(Gtk.Align.START) + vbox.pack_start(title, False, False, 0) + + # Render each account + for acct in accounts_data: + self._add_account_section(vbox, acct, thresholds, burn_rate_cfg) + + # Footer + sep = Gtk.Separator() + sep.get_style_context().add_class("separator") + vbox.pack_start(sep, False, False, 4) + + footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + updated = Gtk.Label(label=f"Updated: {last_updated}") + updated.get_style_context().add_class("metric-sub") + footer.pack_start(updated, False, False, 0) + + refresh_btn = Gtk.Button(label="Refresh") + refresh_btn.set_relief(Gtk.ReliefStyle.NONE) + btn_css = Gtk.CssProvider() + btn_css.load_from_data(b""" + button { color: #8888aa; background: transparent; border: none; padding: 2px 8px; } + button:hover { color: #e0e0ff; } + """) + refresh_btn.get_style_context().add_provider(btn_css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + refresh_btn.connect("clicked", lambda _: (self.destroy(), on_refresh())) + footer.pack_end(refresh_btn, False, False, 0) + vbox.pack_start(footer, False, False, 0) + + ver = Gtk.Label(label=f"v{version}") + ver.get_style_context().add_class("reset-label") + ver.set_halign(Gtk.Align.CENTER) + vbox.pack_start(ver, False, False, 0) + + self.add(vbox) + self.show_all() + + def _apply_css(self): + css = Gtk.CssProvider() + css.load_from_data(b""" + window { background-color: #1a1a2e; } + .title-label { color: #e0e0ff; font-size: 16px; font-weight: bold; } + .account-label { color: #c0c0ff; font-size: 14px; font-weight: bold; } + .section-label { color: #a0a0c0; font-size: 11px; font-weight: bold; letter-spacing: 2px; } + .col-header { color: #a0a0c0; font-size: 12px; font-weight: bold; letter-spacing: 1px; border-bottom: 1px solid #2a2a4a; } + .metric-sub { color: #8888aa; font-size: 11px; } + .reset-label { color: #6b7280; font-size: 11px; } + .status-ok { color: #22c55e; font-size: 11px; } + .status-err { color: #ef4444; font-size: 11px; } + .separator { background-color: #2a2a4a; } + """) + Gtk.StyleContext.add_provider_for_screen( + Gdk.Screen.get_default(), css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) + + def _add_account_section(self, vbox: Gtk.Box, acct: dict, thresholds: dict, + burn_rate_cfg: dict): + """Add one account's usage section to the popup.""" + label = acct["label"] + usage = acct.get("usage_data") + error = acct.get("error") + + sep = Gtk.Separator() + sep.get_style_context().add_class("separator") + vbox.pack_start(sep, False, False, 4) + + # Account header row + header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + name = Gtk.Label(label=f"Account: {label}") + name.get_style_context().add_class("account-label") + header.pack_start(name, False, False, 0) + + if error: + st = Gtk.Label(label=f" {error}") + st.get_style_context().add_class("status-err") + header.pack_end(st, False, False, 0) + elif usage: + st = Gtk.Label(label=" Connected") + st.get_style_context().add_class("status-ok") + header.pack_end(st, False, False, 0) + vbox.pack_start(header, False, False, 0) + + # Subscription info + sub_info = acct.get("subscription_info") + if sub_info and sub_info.get("subscription_type"): + sub_lbl = Gtk.Label(label=f"Plan: {sub_info['subscription_type']}") + sub_lbl.get_style_context().add_class("metric-sub") + sub_lbl.set_halign(Gtk.Align.START) + vbox.pack_start(sub_lbl, False, False, 0) + + if not usage: + if not error: + err_lbl = Gtk.Label(label="No data available") + err_lbl.get_style_context().add_class("status-err") + err_lbl.set_halign(Gtk.Align.START) + vbox.pack_start(err_lbl, False, False, 2) + return + + # Two-column table layout: one column per usage window + grid = Gtk.Grid() + grid.set_column_spacing(24) + grid.set_row_spacing(4) + grid.set_column_homogeneous(True) + + windows = [("five_hour", "5h"), ("seven_day", "7d")] + for col, (key, window_label) in enumerate(windows): + bucket = usage.get(key) + + # Row 0: column header + hdr = Gtk.Label(label=window_label) + hdr.get_style_context().add_class("col-header") + hdr.set_halign(Gtk.Align.CENTER) + grid.attach(hdr, col, 0, 1, 1) + + if not bucket: + placeholder = Gtk.Label(label="—") + placeholder.get_style_context().add_class("reset-label") + placeholder.set_halign(Gtk.Align.CENTER) + grid.attach(placeholder, col, 1, 1, 1) + continue + + pct, decimal = parse_utilization(bucket.get("utilization", 0)) + color = get_color_for_pct(pct, thresholds) + resets_at = bucket.get("resets_at") + if acct.get("disable_polling", False): + reset_str = format_reset_clock_7d(resets_at) if key == "seven_day" else format_reset_clock(resets_at) + else: + reset_str = format_reset_time(resets_at) + + # Row 1: progress bar + bar = Gtk.LevelBar() + bar.set_min_value(0) + bar.set_max_value(1.0) + bar.set_value(min(decimal, 1.0)) + bar.set_size_request(-1, 8) + bar.remove_offset_value("low") + bar.remove_offset_value("high") + bar.remove_offset_value("full") + bar_css = Gtk.CssProvider() + bar_css.load_from_data(f""" + levelbar trough {{ background-color: #2a2a4a; border-radius: 4px; min-height: 8px; }} + levelbar trough block.filled {{ background-color: {color}; border-radius: 4px; min-height: 8px; }} + """.encode()) + bar.get_style_context().add_provider(bar_css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + grid.attach(bar, col, 1, 1, 1) + + # Row 2: "72% — ↺ 2h 15m" inline + val = Gtk.Label() + val.set_markup( + f'{pct}%' + f' \u2014 \u21ba {reset_str}' + ) + val.set_halign(Gtk.Align.CENTER) + grid.attach(val, col, 2, 1, 1) + + # Row 3: burn rate — only shown for 7d column + if key == "seven_day": + burn_rate = compute_burn_rate(bucket) + if burn_rate is not None: + arrow = "\u2191" if burn_rate >= 1.0 else "\u2193" + multiplier = burn_rate_cfg.get("multiplier", 1.5) + # Colour: red if over multiplier, green if under 1×, muted otherwise + if burn_rate >= multiplier: + pace_color = "#ef4444" + elif burn_rate < 1.0: + pace_color = "#22c55e" + else: + pace_color = "#8888aa" + pace_lbl = Gtk.Label() + pace_lbl.set_markup( + f'' + f'pace {arrow}{burn_rate:.1f}\u00d7' + ) + pace_lbl.set_halign(Gtk.Align.CENTER) + grid.attach(pace_lbl, col, 3, 1, 1) + + vbox.pack_start(grid, False, False, 4)