claude-usage-widget 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_usage_widget-0.2.0/LICENSE +21 -0
- claude_usage_widget-0.2.0/MANIFEST.in +6 -0
- claude_usage_widget-0.2.0/PKG-INFO +238 -0
- claude_usage_widget-0.2.0/README.md +209 -0
- claude_usage_widget-0.2.0/claude_usage/__init__.py +3 -0
- claude_usage_widget-0.2.0/claude_usage/analytics.py +149 -0
- claude_usage_widget-0.2.0/claude_usage/api_server.py +86 -0
- claude_usage_widget-0.2.0/claude_usage/cli.py +90 -0
- claude_usage_widget-0.2.0/claude_usage/collector.py +613 -0
- claude_usage_widget-0.2.0/claude_usage/config.py +93 -0
- claude_usage_widget-0.2.0/claude_usage/exporter.py +86 -0
- claude_usage_widget-0.2.0/claude_usage/forecast.py +155 -0
- claude_usage_widget-0.2.0/claude_usage/history.py +95 -0
- claude_usage_widget-0.2.0/claude_usage/icons/claude-tray.svg +17 -0
- claude_usage_widget-0.2.0/claude_usage/notifier.py +78 -0
- claude_usage_widget-0.2.0/claude_usage/overlay.py +556 -0
- claude_usage_widget-0.2.0/claude_usage/overlay_macos.py +673 -0
- claude_usage_widget-0.2.0/claude_usage/pricing.py +175 -0
- claude_usage_widget-0.2.0/claude_usage/py.typed +0 -0
- claude_usage_widget-0.2.0/claude_usage/themes.py +146 -0
- claude_usage_widget-0.2.0/claude_usage/trends.py +106 -0
- claude_usage_widget-0.2.0/claude_usage/updater.py +52 -0
- claude_usage_widget-0.2.0/claude_usage/webhooks.py +67 -0
- claude_usage_widget-0.2.0/claude_usage/widget.py +899 -0
- claude_usage_widget-0.2.0/claude_usage/widget_macos.py +1223 -0
- claude_usage_widget-0.2.0/claude_usage_widget.egg-info/PKG-INFO +238 -0
- claude_usage_widget-0.2.0/claude_usage_widget.egg-info/SOURCES.txt +54 -0
- claude_usage_widget-0.2.0/claude_usage_widget.egg-info/dependency_links.txt +1 -0
- claude_usage_widget-0.2.0/claude_usage_widget.egg-info/entry_points.txt +2 -0
- claude_usage_widget-0.2.0/claude_usage_widget.egg-info/requires.txt +4 -0
- claude_usage_widget-0.2.0/claude_usage_widget.egg-info/top_level.txt +1 -0
- claude_usage_widget-0.2.0/config.json.example +12 -0
- claude_usage_widget-0.2.0/docs/integrations/README.md +16 -0
- claude_usage_widget-0.2.0/docs/integrations/polybar-module.ini +6 -0
- claude_usage_widget-0.2.0/docs/integrations/starship.toml.snippet +7 -0
- claude_usage_widget-0.2.0/docs/integrations/tmux.conf.snippet +6 -0
- claude_usage_widget-0.2.0/docs/integrations/waybar-module.json +9 -0
- claude_usage_widget-0.2.0/docs/integrations/waybar-style.css +4 -0
- claude_usage_widget-0.2.0/docs/integrations/zsh-prompt.zsh +21 -0
- claude_usage_widget-0.2.0/pyproject.toml +52 -0
- claude_usage_widget-0.2.0/requirements-macos.txt +2 -0
- claude_usage_widget-0.2.0/setup.cfg +4 -0
- claude_usage_widget-0.2.0/tests/test_analytics.py +84 -0
- claude_usage_widget-0.2.0/tests/test_api_server.py +67 -0
- claude_usage_widget-0.2.0/tests/test_cli.py +90 -0
- claude_usage_widget-0.2.0/tests/test_collector.py +928 -0
- claude_usage_widget-0.2.0/tests/test_config.py +285 -0
- claude_usage_widget-0.2.0/tests/test_exporter.py +78 -0
- claude_usage_widget-0.2.0/tests/test_forecast.py +180 -0
- claude_usage_widget-0.2.0/tests/test_history.py +102 -0
- claude_usage_widget-0.2.0/tests/test_notifier.py +91 -0
- claude_usage_widget-0.2.0/tests/test_pricing.py +236 -0
- claude_usage_widget-0.2.0/tests/test_themes.py +96 -0
- claude_usage_widget-0.2.0/tests/test_trends.py +82 -0
- claude_usage_widget-0.2.0/tests/test_updater.py +58 -0
- claude_usage_widget-0.2.0/tests/test_webhooks.py +61 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Burak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claude-usage-widget
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Desktop widget and CLI that shows real-time Claude Code usage limits and cost.
|
|
5
|
+
Author: Burak
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bozdemir/claude-usage-widget
|
|
8
|
+
Project-URL: Issues, https://github.com/bozdemir/claude-usage-widget/issues
|
|
9
|
+
Keywords: claude,anthropic,usage,rate-limit,widget
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: X11 Applications :: GTK
|
|
12
|
+
Classifier: Environment :: MacOS X
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: System :: Monitoring
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: macos
|
|
26
|
+
Requires-Dist: rumps>=0.4.0; extra == "macos"
|
|
27
|
+
Requires-Dist: pyobjc-framework-Cocoa>=9.0; extra == "macos"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# Claude Usage Widget
|
|
31
|
+
|
|
32
|
+
A desktop widget that displays your Claude Code usage limits in real time. Shows session and weekly utilization percentages fetched from the Anthropic API, with an always-on-top OSD overlay and a system tray / menu bar icon.
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+

|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Real API data** -- fetches rate-limit utilization from `anthropic-ratelimit-unified-*` response headers
|
|
41
|
+
- **OSD overlay** -- transparent, borderless widget in the corner of your screen showing session and weekly usage bars with reset countdowns
|
|
42
|
+
- **System tray / menu bar** -- quick-glance usage info and a detailed popup window
|
|
43
|
+
- **Cross-platform** -- native GTK3 on Linux, native AppKit/rumps on macOS
|
|
44
|
+
- **Auto-refresh** -- updates every 30 seconds (configurable)
|
|
45
|
+
- **Resizable** -- scroll wheel to scale the OSD up or down (0.6x--2.0x)
|
|
46
|
+
- **Opacity control** -- adjustable OSD transparency via the tray / menu bar
|
|
47
|
+
- **Draggable** -- left-click and drag to reposition the OSD
|
|
48
|
+
- **Minimizable** -- right-click the OSD to collapse it to a thin progress bar
|
|
49
|
+
- **Active sessions** -- shows running Claude Code sessions with project paths and durations
|
|
50
|
+
- **Cost estimation** -- widget shows estimated $ spent today and cache savings using model pricing
|
|
51
|
+
- **Usage forecasting** -- burn rate calculation shows "at this rate you'll hit the limit in Xh Ym"
|
|
52
|
+
- **Per-project breakdown** -- top 5 projects by token usage today
|
|
53
|
+
- **Themes** -- 5 built-in themes (default, catppuccin-mocha, dracula, nord, gruvbox-dark), set via `"theme": "dracula"` in `config.json`
|
|
54
|
+
|
|
55
|
+
## Requirements
|
|
56
|
+
|
|
57
|
+
- Python 3.10+
|
|
58
|
+
- Claude Code CLI installed and authenticated (OAuth)
|
|
59
|
+
|
|
60
|
+
### Linux
|
|
61
|
+
|
|
62
|
+
- GTK3, python3-gi, python3-gi-cairo, python3-cairo
|
|
63
|
+
- `gir1.2-ayatanaappindicator3-0.1` (system tray)
|
|
64
|
+
|
|
65
|
+
### macOS
|
|
66
|
+
|
|
67
|
+
- `rumps` and `pyobjc-framework-Cocoa` (installed automatically via `requirements-macos.txt`)
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
### Linux
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# 1. Install system dependencies (Ubuntu/Debian)
|
|
75
|
+
sudo apt install python3-gi python3-gi-cairo python3-cairo gir1.2-ayatanaappindicator3-0.1
|
|
76
|
+
|
|
77
|
+
# Fedora
|
|
78
|
+
sudo dnf install python3-gobject python3-gobject-cairo python3-cairo gtk3
|
|
79
|
+
|
|
80
|
+
# Arch
|
|
81
|
+
sudo pacman -S python-gobject python-cairo gtk3 libappindicator-gtk3
|
|
82
|
+
|
|
83
|
+
# 2. Clone and run
|
|
84
|
+
git clone https://github.com/bozdemir/claude-usage-widget.git
|
|
85
|
+
cd claude-usage-widget
|
|
86
|
+
python3 main.py &
|
|
87
|
+
|
|
88
|
+
# 3. Autostart on login (optional)
|
|
89
|
+
./install.sh
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### macOS
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# 1. Clone
|
|
96
|
+
git clone https://github.com/bozdemir/claude-usage-widget.git
|
|
97
|
+
cd claude-usage-widget
|
|
98
|
+
|
|
99
|
+
# 2. Install Python dependencies
|
|
100
|
+
pip3 install -r requirements-macos.txt
|
|
101
|
+
|
|
102
|
+
# 3. Run
|
|
103
|
+
python3 main.py
|
|
104
|
+
|
|
105
|
+
# 4. Autostart on login (optional) -- installs a Launch Agent
|
|
106
|
+
./install-macos.sh
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Usage
|
|
110
|
+
|
|
111
|
+
### OSD Overlay Controls
|
|
112
|
+
|
|
113
|
+
| Action | Effect |
|
|
114
|
+
|--------|--------|
|
|
115
|
+
| **Scroll up / down** | Resize (0.6x--2.0x) |
|
|
116
|
+
| **Left-click drag** | Move the OSD |
|
|
117
|
+
| **Right-click** | Minimize / restore |
|
|
118
|
+
|
|
119
|
+
### System Tray / Menu Bar
|
|
120
|
+
|
|
121
|
+
- **Session / Weekly** -- current usage percentages
|
|
122
|
+
- **Details...** -- opens a detailed popup with usage bars and active sessions
|
|
123
|
+
- **Refresh** -- force an immediate data refresh
|
|
124
|
+
- **OSD Overlay** -- toggle the OSD on or off
|
|
125
|
+
- **OSD Opacity** -- set OSD transparency (100% / 75% / 50% / 25%)
|
|
126
|
+
- **Quit** -- exit the widget
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
All settings are optional. Copy `config.json.example` to `config.json` and edit the values you want to change:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
cp config.json.example config.json
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"daily_message_limit": 200,
|
|
139
|
+
"weekly_message_limit": 1000,
|
|
140
|
+
"daily_token_limit": 5000000,
|
|
141
|
+
"weekly_token_limit": 25000000,
|
|
142
|
+
"refresh_seconds": 30,
|
|
143
|
+
"osd_opacity": 0.75,
|
|
144
|
+
"osd_scale": 1.0
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Setting | Default | Description |
|
|
149
|
+
|---------|---------|-------------|
|
|
150
|
+
| `refresh_seconds` | `30` | How often to fetch new data from the API (seconds) |
|
|
151
|
+
| `osd_opacity` | `0.75` | OSD background opacity (0.15--1.0) |
|
|
152
|
+
| `osd_scale` | `1.0` | OSD scale factor (0.6--2.0) |
|
|
153
|
+
| `daily_message_limit` | `200` | Daily message limit for local tracking in the popup |
|
|
154
|
+
| `weekly_message_limit` | `1000` | Weekly message limit for local tracking in the popup |
|
|
155
|
+
| `daily_token_limit` | `5000000` | Daily token limit for local tracking |
|
|
156
|
+
| `weekly_token_limit` | `25000000` | Weekly token limit for local tracking |
|
|
157
|
+
| `claude_dir` | `~/.claude` | Path to the Claude Code data directory |
|
|
158
|
+
| `theme` | `default` | Color theme for the OSD and popup. One of `default`, `catppuccin-mocha`, `dracula`, `nord`, `gruvbox-dark` |
|
|
159
|
+
|
|
160
|
+
Keys omitted from `config.json` fall back to built-in defaults. `claude_dir` is not included in the example file because the default is correct for most setups.
|
|
161
|
+
|
|
162
|
+
## Themes
|
|
163
|
+
|
|
164
|
+
The widget ships with 5 built-in color themes. Select one by adding `"theme": "<name>"` to your `config.json`:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"theme": "dracula"
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Available themes:
|
|
173
|
+
|
|
174
|
+
- **default** -- the original widget palette _(screenshots welcome)_
|
|
175
|
+
- **catppuccin-mocha** -- soft pastel dark theme _(screenshots welcome)_
|
|
176
|
+
- **dracula** -- classic purple-and-pink dark theme _(screenshots welcome)_
|
|
177
|
+
- **nord** -- cool arctic blue palette _(screenshots welcome)_
|
|
178
|
+
- **gruvbox-dark** -- warm retro-style dark theme _(screenshots welcome)_
|
|
179
|
+
|
|
180
|
+
## How It Works
|
|
181
|
+
|
|
182
|
+
The widget reads your Claude Code OAuth credentials from `~/.claude/.credentials.json` (Linux) or the macOS Keychain and makes a minimal API call (`max_tokens=1` to `claude-haiku-4-5-20251001`) to read the rate-limit response headers:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
anthropic-ratelimit-unified-5h-utilization: 0.58
|
|
186
|
+
anthropic-ratelimit-unified-5h-reset: 1776186000
|
|
187
|
+
anthropic-ratelimit-unified-7d-utilization: 0.10
|
|
188
|
+
anthropic-ratelimit-unified-7d-reset: 1776690000
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
These are the same values shown on the [claude.ai usage page](https://claude.ai/settings/usage). The widget also reads local data from `~/.claude/` for message counts, token usage per model, and active session tracking.
|
|
192
|
+
|
|
193
|
+
### How the OSD Works
|
|
194
|
+
|
|
195
|
+
The OSD is a transparent, borderless window rendered entirely via 2D drawing primitives:
|
|
196
|
+
|
|
197
|
+
- **Linux** -- a `Gtk.Window` with the `NOTIFICATION` type hint uses Cairo to draw rounded rectangles, progress bars, and text onto an RGBA surface. The compositor handles transparency; `set_accept_focus(False)` prevents the overlay from stealing keyboard focus.
|
|
198
|
+
- **macOS** -- an `NSWindow` at `NSFloatingWindowLevel` with `NSWindowStyleMaskBorderless` and a transparent `NSView` subclass does the equivalent drawing via AppKit (`NSBezierPath`, `NSColor`, `NSAttributedString`).
|
|
199
|
+
|
|
200
|
+
**Scale and opacity** -- Both platforms store a float `scale` (0.6--2.0, default 1.0) and `opacity` (0.15--1.0, default 0.75) that are applied at draw time. Scale multiplies every pixel dimension (padding, font size, bar height, window size) before drawing, so the entire widget resizes proportionally without re-layout. Opacity is used as the alpha channel of the background fill; bar and text elements are drawn at full alpha on top so they remain legible at low opacity.
|
|
201
|
+
|
|
202
|
+
**Refresh cycle** -- A background thread wakes every `refresh_seconds` (default 30), makes an API call, and posts the result back to the main thread via `GLib.idle_add` (Linux) or a `rumps.Timer`-drained queue (macOS). The main thread then invalidates the OSD window, triggering a synchronous redraw. User interactions (scroll to resize, drag to move, right-click to minimize) update scale and position in place and queue an immediate redraw without waiting for the next refresh tick.
|
|
203
|
+
|
|
204
|
+
## Troubleshooting
|
|
205
|
+
|
|
206
|
+
### Linux: OSD not visible
|
|
207
|
+
- Ensure XWayland is available. The widget forces `GDK_BACKEND=x11` for reliable borderless windows.
|
|
208
|
+
- Check if the process is running: `ps aux | grep main.py`
|
|
209
|
+
|
|
210
|
+
### Linux: no system tray icon
|
|
211
|
+
- Install `gir1.2-ayatanaappindicator3-0.1`.
|
|
212
|
+
- On GNOME, you may need the [AppIndicator extension](https://extensions.gnome.org/extension/615/appindicator-support/).
|
|
213
|
+
|
|
214
|
+
### Linux: Cairo errors (`Couldn't find foreign struct converter`)
|
|
215
|
+
- Install `python3-gi-cairo`: `sudo apt install python3-gi-cairo`
|
|
216
|
+
|
|
217
|
+
### macOS: no menu bar icon
|
|
218
|
+
- Make sure `rumps` is installed: `pip3 install rumps`
|
|
219
|
+
- If using a virtualenv, ensure `pyobjc-framework-Cocoa` is also installed.
|
|
220
|
+
|
|
221
|
+
### API authentication fails
|
|
222
|
+
- Make sure the Claude Code CLI is installed and you are logged in (the `claude` command should work).
|
|
223
|
+
- Linux: the OAuth token is read from `~/.claude/.credentials.json`.
|
|
224
|
+
- macOS: the OAuth token is read from the Keychain, with a fallback to `~/.claude/.credentials.json`.
|
|
225
|
+
|
|
226
|
+
## Contributing
|
|
227
|
+
|
|
228
|
+
Contributions are welcome. A few guidelines:
|
|
229
|
+
|
|
230
|
+
- **Bug reports** -- open an issue with your OS, Python version, and the full error output. If the OSD is invisible, include `xrandr` or `system_profiler SPDisplaysDataType` output.
|
|
231
|
+
- **Pull requests** -- keep changes focused. One fix or feature per PR. Run the widget manually on the target platform before submitting.
|
|
232
|
+
- **Platform parity** -- features that affect the OSD or tray should work on both Linux and macOS, or be clearly gated behind a platform check.
|
|
233
|
+
- **No new dependencies** -- avoid adding Python packages beyond those already in `requirements-macos.txt` and the listed GTK stack. If a dependency is truly necessary, discuss it in an issue first.
|
|
234
|
+
- **Code style** -- follow the existing conventions. No formatter is enforced; just match the surrounding code.
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Claude Usage Widget
|
|
2
|
+
|
|
3
|
+
A desktop widget that displays your Claude Code usage limits in real time. Shows session and weekly utilization percentages fetched from the Anthropic API, with an always-on-top OSD overlay and a system tray / menu bar icon.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Real API data** -- fetches rate-limit utilization from `anthropic-ratelimit-unified-*` response headers
|
|
12
|
+
- **OSD overlay** -- transparent, borderless widget in the corner of your screen showing session and weekly usage bars with reset countdowns
|
|
13
|
+
- **System tray / menu bar** -- quick-glance usage info and a detailed popup window
|
|
14
|
+
- **Cross-platform** -- native GTK3 on Linux, native AppKit/rumps on macOS
|
|
15
|
+
- **Auto-refresh** -- updates every 30 seconds (configurable)
|
|
16
|
+
- **Resizable** -- scroll wheel to scale the OSD up or down (0.6x--2.0x)
|
|
17
|
+
- **Opacity control** -- adjustable OSD transparency via the tray / menu bar
|
|
18
|
+
- **Draggable** -- left-click and drag to reposition the OSD
|
|
19
|
+
- **Minimizable** -- right-click the OSD to collapse it to a thin progress bar
|
|
20
|
+
- **Active sessions** -- shows running Claude Code sessions with project paths and durations
|
|
21
|
+
- **Cost estimation** -- widget shows estimated $ spent today and cache savings using model pricing
|
|
22
|
+
- **Usage forecasting** -- burn rate calculation shows "at this rate you'll hit the limit in Xh Ym"
|
|
23
|
+
- **Per-project breakdown** -- top 5 projects by token usage today
|
|
24
|
+
- **Themes** -- 5 built-in themes (default, catppuccin-mocha, dracula, nord, gruvbox-dark), set via `"theme": "dracula"` in `config.json`
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Python 3.10+
|
|
29
|
+
- Claude Code CLI installed and authenticated (OAuth)
|
|
30
|
+
|
|
31
|
+
### Linux
|
|
32
|
+
|
|
33
|
+
- GTK3, python3-gi, python3-gi-cairo, python3-cairo
|
|
34
|
+
- `gir1.2-ayatanaappindicator3-0.1` (system tray)
|
|
35
|
+
|
|
36
|
+
### macOS
|
|
37
|
+
|
|
38
|
+
- `rumps` and `pyobjc-framework-Cocoa` (installed automatically via `requirements-macos.txt`)
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
### Linux
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 1. Install system dependencies (Ubuntu/Debian)
|
|
46
|
+
sudo apt install python3-gi python3-gi-cairo python3-cairo gir1.2-ayatanaappindicator3-0.1
|
|
47
|
+
|
|
48
|
+
# Fedora
|
|
49
|
+
sudo dnf install python3-gobject python3-gobject-cairo python3-cairo gtk3
|
|
50
|
+
|
|
51
|
+
# Arch
|
|
52
|
+
sudo pacman -S python-gobject python-cairo gtk3 libappindicator-gtk3
|
|
53
|
+
|
|
54
|
+
# 2. Clone and run
|
|
55
|
+
git clone https://github.com/bozdemir/claude-usage-widget.git
|
|
56
|
+
cd claude-usage-widget
|
|
57
|
+
python3 main.py &
|
|
58
|
+
|
|
59
|
+
# 3. Autostart on login (optional)
|
|
60
|
+
./install.sh
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### macOS
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 1. Clone
|
|
67
|
+
git clone https://github.com/bozdemir/claude-usage-widget.git
|
|
68
|
+
cd claude-usage-widget
|
|
69
|
+
|
|
70
|
+
# 2. Install Python dependencies
|
|
71
|
+
pip3 install -r requirements-macos.txt
|
|
72
|
+
|
|
73
|
+
# 3. Run
|
|
74
|
+
python3 main.py
|
|
75
|
+
|
|
76
|
+
# 4. Autostart on login (optional) -- installs a Launch Agent
|
|
77
|
+
./install-macos.sh
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
### OSD Overlay Controls
|
|
83
|
+
|
|
84
|
+
| Action | Effect |
|
|
85
|
+
|--------|--------|
|
|
86
|
+
| **Scroll up / down** | Resize (0.6x--2.0x) |
|
|
87
|
+
| **Left-click drag** | Move the OSD |
|
|
88
|
+
| **Right-click** | Minimize / restore |
|
|
89
|
+
|
|
90
|
+
### System Tray / Menu Bar
|
|
91
|
+
|
|
92
|
+
- **Session / Weekly** -- current usage percentages
|
|
93
|
+
- **Details...** -- opens a detailed popup with usage bars and active sessions
|
|
94
|
+
- **Refresh** -- force an immediate data refresh
|
|
95
|
+
- **OSD Overlay** -- toggle the OSD on or off
|
|
96
|
+
- **OSD Opacity** -- set OSD transparency (100% / 75% / 50% / 25%)
|
|
97
|
+
- **Quit** -- exit the widget
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
All settings are optional. Copy `config.json.example` to `config.json` and edit the values you want to change:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cp config.json.example config.json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"daily_message_limit": 200,
|
|
110
|
+
"weekly_message_limit": 1000,
|
|
111
|
+
"daily_token_limit": 5000000,
|
|
112
|
+
"weekly_token_limit": 25000000,
|
|
113
|
+
"refresh_seconds": 30,
|
|
114
|
+
"osd_opacity": 0.75,
|
|
115
|
+
"osd_scale": 1.0
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
| Setting | Default | Description |
|
|
120
|
+
|---------|---------|-------------|
|
|
121
|
+
| `refresh_seconds` | `30` | How often to fetch new data from the API (seconds) |
|
|
122
|
+
| `osd_opacity` | `0.75` | OSD background opacity (0.15--1.0) |
|
|
123
|
+
| `osd_scale` | `1.0` | OSD scale factor (0.6--2.0) |
|
|
124
|
+
| `daily_message_limit` | `200` | Daily message limit for local tracking in the popup |
|
|
125
|
+
| `weekly_message_limit` | `1000` | Weekly message limit for local tracking in the popup |
|
|
126
|
+
| `daily_token_limit` | `5000000` | Daily token limit for local tracking |
|
|
127
|
+
| `weekly_token_limit` | `25000000` | Weekly token limit for local tracking |
|
|
128
|
+
| `claude_dir` | `~/.claude` | Path to the Claude Code data directory |
|
|
129
|
+
| `theme` | `default` | Color theme for the OSD and popup. One of `default`, `catppuccin-mocha`, `dracula`, `nord`, `gruvbox-dark` |
|
|
130
|
+
|
|
131
|
+
Keys omitted from `config.json` fall back to built-in defaults. `claude_dir` is not included in the example file because the default is correct for most setups.
|
|
132
|
+
|
|
133
|
+
## Themes
|
|
134
|
+
|
|
135
|
+
The widget ships with 5 built-in color themes. Select one by adding `"theme": "<name>"` to your `config.json`:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"theme": "dracula"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Available themes:
|
|
144
|
+
|
|
145
|
+
- **default** -- the original widget palette _(screenshots welcome)_
|
|
146
|
+
- **catppuccin-mocha** -- soft pastel dark theme _(screenshots welcome)_
|
|
147
|
+
- **dracula** -- classic purple-and-pink dark theme _(screenshots welcome)_
|
|
148
|
+
- **nord** -- cool arctic blue palette _(screenshots welcome)_
|
|
149
|
+
- **gruvbox-dark** -- warm retro-style dark theme _(screenshots welcome)_
|
|
150
|
+
|
|
151
|
+
## How It Works
|
|
152
|
+
|
|
153
|
+
The widget reads your Claude Code OAuth credentials from `~/.claude/.credentials.json` (Linux) or the macOS Keychain and makes a minimal API call (`max_tokens=1` to `claude-haiku-4-5-20251001`) to read the rate-limit response headers:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
anthropic-ratelimit-unified-5h-utilization: 0.58
|
|
157
|
+
anthropic-ratelimit-unified-5h-reset: 1776186000
|
|
158
|
+
anthropic-ratelimit-unified-7d-utilization: 0.10
|
|
159
|
+
anthropic-ratelimit-unified-7d-reset: 1776690000
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
These are the same values shown on the [claude.ai usage page](https://claude.ai/settings/usage). The widget also reads local data from `~/.claude/` for message counts, token usage per model, and active session tracking.
|
|
163
|
+
|
|
164
|
+
### How the OSD Works
|
|
165
|
+
|
|
166
|
+
The OSD is a transparent, borderless window rendered entirely via 2D drawing primitives:
|
|
167
|
+
|
|
168
|
+
- **Linux** -- a `Gtk.Window` with the `NOTIFICATION` type hint uses Cairo to draw rounded rectangles, progress bars, and text onto an RGBA surface. The compositor handles transparency; `set_accept_focus(False)` prevents the overlay from stealing keyboard focus.
|
|
169
|
+
- **macOS** -- an `NSWindow` at `NSFloatingWindowLevel` with `NSWindowStyleMaskBorderless` and a transparent `NSView` subclass does the equivalent drawing via AppKit (`NSBezierPath`, `NSColor`, `NSAttributedString`).
|
|
170
|
+
|
|
171
|
+
**Scale and opacity** -- Both platforms store a float `scale` (0.6--2.0, default 1.0) and `opacity` (0.15--1.0, default 0.75) that are applied at draw time. Scale multiplies every pixel dimension (padding, font size, bar height, window size) before drawing, so the entire widget resizes proportionally without re-layout. Opacity is used as the alpha channel of the background fill; bar and text elements are drawn at full alpha on top so they remain legible at low opacity.
|
|
172
|
+
|
|
173
|
+
**Refresh cycle** -- A background thread wakes every `refresh_seconds` (default 30), makes an API call, and posts the result back to the main thread via `GLib.idle_add` (Linux) or a `rumps.Timer`-drained queue (macOS). The main thread then invalidates the OSD window, triggering a synchronous redraw. User interactions (scroll to resize, drag to move, right-click to minimize) update scale and position in place and queue an immediate redraw without waiting for the next refresh tick.
|
|
174
|
+
|
|
175
|
+
## Troubleshooting
|
|
176
|
+
|
|
177
|
+
### Linux: OSD not visible
|
|
178
|
+
- Ensure XWayland is available. The widget forces `GDK_BACKEND=x11` for reliable borderless windows.
|
|
179
|
+
- Check if the process is running: `ps aux | grep main.py`
|
|
180
|
+
|
|
181
|
+
### Linux: no system tray icon
|
|
182
|
+
- Install `gir1.2-ayatanaappindicator3-0.1`.
|
|
183
|
+
- On GNOME, you may need the [AppIndicator extension](https://extensions.gnome.org/extension/615/appindicator-support/).
|
|
184
|
+
|
|
185
|
+
### Linux: Cairo errors (`Couldn't find foreign struct converter`)
|
|
186
|
+
- Install `python3-gi-cairo`: `sudo apt install python3-gi-cairo`
|
|
187
|
+
|
|
188
|
+
### macOS: no menu bar icon
|
|
189
|
+
- Make sure `rumps` is installed: `pip3 install rumps`
|
|
190
|
+
- If using a virtualenv, ensure `pyobjc-framework-Cocoa` is also installed.
|
|
191
|
+
|
|
192
|
+
### API authentication fails
|
|
193
|
+
- Make sure the Claude Code CLI is installed and you are logged in (the `claude` command should work).
|
|
194
|
+
- Linux: the OAuth token is read from `~/.claude/.credentials.json`.
|
|
195
|
+
- macOS: the OAuth token is read from the Keychain, with a fallback to `~/.claude/.credentials.json`.
|
|
196
|
+
|
|
197
|
+
## Contributing
|
|
198
|
+
|
|
199
|
+
Contributions are welcome. A few guidelines:
|
|
200
|
+
|
|
201
|
+
- **Bug reports** -- open an issue with your OS, Python version, and the full error output. If the OSD is invisible, include `xrandr` or `system_profiler SPDisplaysDataType` output.
|
|
202
|
+
- **Pull requests** -- keep changes focused. One fix or feature per PR. Run the widget manually on the target platform before submitting.
|
|
203
|
+
- **Platform parity** -- features that affect the OSD or tray should work on both Linux and macOS, or be clearly gated behind a platform check.
|
|
204
|
+
- **No new dependencies** -- avoid adding Python packages beyond those already in `requirements-macos.txt` and the listed GTK stack. If a dependency is truly necessary, discuss it in an issue first.
|
|
205
|
+
- **Code style** -- follow the existing conventions. No formatter is enforced; just match the surrounding code.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Anomaly detection and cost-optimisation analysis over usage history.
|
|
2
|
+
|
|
3
|
+
Pure module — no I/O, no GUI, no network. Given a list of sample dicts from
|
|
4
|
+
history.py, produces structured reports the widget can render in the popup.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import statistics
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
MIN_SAMPLES = 7 # Need at least a week of data before we flag anomalies
|
|
14
|
+
Z_THRESHOLD = 2.0 # Standard deviations above the mean
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AnomalyReport:
|
|
19
|
+
"""Summary of a single-day anomaly check."""
|
|
20
|
+
|
|
21
|
+
is_anomaly: bool = False
|
|
22
|
+
today_usage: float = 0.0
|
|
23
|
+
baseline: float = 0.0
|
|
24
|
+
std_dev: float = 0.0
|
|
25
|
+
z_score: float = 0.0
|
|
26
|
+
ratio: float = 0.0
|
|
27
|
+
reason: str = ""
|
|
28
|
+
message: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _daily_peaks(samples: list[dict], key: str = "session") -> list[float]:
|
|
32
|
+
"""Reduce a sample stream into one max value per calendar day."""
|
|
33
|
+
by_day: dict[int, float] = {}
|
|
34
|
+
for s in samples:
|
|
35
|
+
ts = float(s.get("ts", 0))
|
|
36
|
+
if ts <= 0:
|
|
37
|
+
continue
|
|
38
|
+
day = int(ts // 86400)
|
|
39
|
+
val = float(s.get(key, 0))
|
|
40
|
+
if val > by_day.get(day, 0.0):
|
|
41
|
+
by_day[day] = val
|
|
42
|
+
return [by_day[d] for d in sorted(by_day)]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def detect_anomaly(
|
|
46
|
+
samples: list[dict],
|
|
47
|
+
today_usage: float,
|
|
48
|
+
key: str = "session",
|
|
49
|
+
) -> AnomalyReport:
|
|
50
|
+
"""Return an AnomalyReport for today_usage against historical peaks."""
|
|
51
|
+
rep = AnomalyReport(today_usage=today_usage)
|
|
52
|
+
|
|
53
|
+
peaks = _daily_peaks(samples, key=key)
|
|
54
|
+
history = peaks[:-1] if peaks else []
|
|
55
|
+
|
|
56
|
+
if len(history) < MIN_SAMPLES:
|
|
57
|
+
rep.reason = f"insufficient history ({len(history)} days < {MIN_SAMPLES})"
|
|
58
|
+
return rep
|
|
59
|
+
|
|
60
|
+
rep.baseline = statistics.fmean(history)
|
|
61
|
+
rep.std_dev = statistics.pstdev(history) if len(history) > 1 else 0.0
|
|
62
|
+
if rep.baseline > 0:
|
|
63
|
+
rep.ratio = today_usage / rep.baseline
|
|
64
|
+
if rep.std_dev > 0:
|
|
65
|
+
rep.z_score = (today_usage - rep.baseline) / rep.std_dev
|
|
66
|
+
|
|
67
|
+
# Flag anomaly: either z-score exceeds threshold, OR history is flat
|
|
68
|
+
# (std_dev == 0) but today is >= 1.5x the baseline.
|
|
69
|
+
is_spike = today_usage > rep.baseline and (
|
|
70
|
+
rep.z_score >= Z_THRESHOLD
|
|
71
|
+
or (rep.std_dev == 0 and rep.ratio >= 1.5)
|
|
72
|
+
)
|
|
73
|
+
if is_spike:
|
|
74
|
+
rep.is_anomaly = True
|
|
75
|
+
rep.message = (
|
|
76
|
+
f"Today is {rep.ratio:.1f}x your {len(history)}-day average — "
|
|
77
|
+
f"{int(today_usage * 100)}% vs {int(rep.baseline * 100)}% typical."
|
|
78
|
+
)
|
|
79
|
+
return rep
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Cost optimisation tips
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
LOW_CACHE_HIT_RATE = 0.60 # below this, suggest improving caching
|
|
87
|
+
OPUS_HEAVY_THRESHOLD = 0.80 # above this share of output from opus, suggest sonnet
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _cache_hit_rate(counts: dict) -> float:
|
|
91
|
+
"""Return cache_read / (cache_read + input) for one model's counts."""
|
|
92
|
+
cr = float(counts.get("cache_read", 0) or 0)
|
|
93
|
+
in_t = float(counts.get("input", 0) or 0)
|
|
94
|
+
denom = cr + in_t
|
|
95
|
+
return cr / denom if denom > 0 else 0.0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def generate_tips(
|
|
99
|
+
by_model: dict,
|
|
100
|
+
week_cost: float,
|
|
101
|
+
cache_savings: float,
|
|
102
|
+
) -> list[str]:
|
|
103
|
+
"""Return 0-3 short actionable tips based on the week's usage profile."""
|
|
104
|
+
tips: list[str] = []
|
|
105
|
+
if not by_model:
|
|
106
|
+
return tips
|
|
107
|
+
|
|
108
|
+
total_output = sum(
|
|
109
|
+
float(c.get("output", 0) or 0) for c in by_model.values()
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Tip 1: cache hit rate
|
|
113
|
+
hit_rates = [
|
|
114
|
+
_cache_hit_rate(c) for c in by_model.values()
|
|
115
|
+
if float(c.get("input", 0) or 0) + float(c.get("cache_read", 0) or 0) > 10_000
|
|
116
|
+
]
|
|
117
|
+
if hit_rates:
|
|
118
|
+
avg_hit = sum(hit_rates) / len(hit_rates)
|
|
119
|
+
if avg_hit < LOW_CACHE_HIT_RATE and week_cost > 0:
|
|
120
|
+
potential = week_cost * (0.85 - avg_hit) * 0.9
|
|
121
|
+
if potential >= 1.0:
|
|
122
|
+
tips.append(
|
|
123
|
+
f"Cache hit rate is {int(avg_hit * 100)}%. "
|
|
124
|
+
f"Raising to ~85% could save ~${potential:.0f}/week."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Tip 2: model mix
|
|
128
|
+
if total_output > 100_000:
|
|
129
|
+
opus_output = sum(
|
|
130
|
+
float(c.get("output", 0) or 0)
|
|
131
|
+
for m, c in by_model.items() if "opus" in m
|
|
132
|
+
)
|
|
133
|
+
opus_share = opus_output / total_output if total_output else 0.0
|
|
134
|
+
if opus_share >= OPUS_HEAVY_THRESHOLD and week_cost > 20.0:
|
|
135
|
+
potential = week_cost * 0.70 * (opus_share - 0.5) * 0.4
|
|
136
|
+
if potential >= 1.0:
|
|
137
|
+
tips.append(
|
|
138
|
+
f"Opus handles {int(opus_share * 100)}% of your output. "
|
|
139
|
+
f"Shifting easy tasks to Sonnet could save ~${potential:.0f}/week."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Tip 3: celebrate savings
|
|
143
|
+
if cache_savings > 0 and week_cost > 0 and cache_savings >= week_cost * 2:
|
|
144
|
+
tips.append(
|
|
145
|
+
f"Cache already saves ${cache_savings:.0f}/week — "
|
|
146
|
+
f"{cache_savings / max(week_cost, 1):.1f}x your bill. Keep it up."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return tips[:3]
|