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.
Files changed (56) hide show
  1. claude_usage_widget-0.2.0/LICENSE +21 -0
  2. claude_usage_widget-0.2.0/MANIFEST.in +6 -0
  3. claude_usage_widget-0.2.0/PKG-INFO +238 -0
  4. claude_usage_widget-0.2.0/README.md +209 -0
  5. claude_usage_widget-0.2.0/claude_usage/__init__.py +3 -0
  6. claude_usage_widget-0.2.0/claude_usage/analytics.py +149 -0
  7. claude_usage_widget-0.2.0/claude_usage/api_server.py +86 -0
  8. claude_usage_widget-0.2.0/claude_usage/cli.py +90 -0
  9. claude_usage_widget-0.2.0/claude_usage/collector.py +613 -0
  10. claude_usage_widget-0.2.0/claude_usage/config.py +93 -0
  11. claude_usage_widget-0.2.0/claude_usage/exporter.py +86 -0
  12. claude_usage_widget-0.2.0/claude_usage/forecast.py +155 -0
  13. claude_usage_widget-0.2.0/claude_usage/history.py +95 -0
  14. claude_usage_widget-0.2.0/claude_usage/icons/claude-tray.svg +17 -0
  15. claude_usage_widget-0.2.0/claude_usage/notifier.py +78 -0
  16. claude_usage_widget-0.2.0/claude_usage/overlay.py +556 -0
  17. claude_usage_widget-0.2.0/claude_usage/overlay_macos.py +673 -0
  18. claude_usage_widget-0.2.0/claude_usage/pricing.py +175 -0
  19. claude_usage_widget-0.2.0/claude_usage/py.typed +0 -0
  20. claude_usage_widget-0.2.0/claude_usage/themes.py +146 -0
  21. claude_usage_widget-0.2.0/claude_usage/trends.py +106 -0
  22. claude_usage_widget-0.2.0/claude_usage/updater.py +52 -0
  23. claude_usage_widget-0.2.0/claude_usage/webhooks.py +67 -0
  24. claude_usage_widget-0.2.0/claude_usage/widget.py +899 -0
  25. claude_usage_widget-0.2.0/claude_usage/widget_macos.py +1223 -0
  26. claude_usage_widget-0.2.0/claude_usage_widget.egg-info/PKG-INFO +238 -0
  27. claude_usage_widget-0.2.0/claude_usage_widget.egg-info/SOURCES.txt +54 -0
  28. claude_usage_widget-0.2.0/claude_usage_widget.egg-info/dependency_links.txt +1 -0
  29. claude_usage_widget-0.2.0/claude_usage_widget.egg-info/entry_points.txt +2 -0
  30. claude_usage_widget-0.2.0/claude_usage_widget.egg-info/requires.txt +4 -0
  31. claude_usage_widget-0.2.0/claude_usage_widget.egg-info/top_level.txt +1 -0
  32. claude_usage_widget-0.2.0/config.json.example +12 -0
  33. claude_usage_widget-0.2.0/docs/integrations/README.md +16 -0
  34. claude_usage_widget-0.2.0/docs/integrations/polybar-module.ini +6 -0
  35. claude_usage_widget-0.2.0/docs/integrations/starship.toml.snippet +7 -0
  36. claude_usage_widget-0.2.0/docs/integrations/tmux.conf.snippet +6 -0
  37. claude_usage_widget-0.2.0/docs/integrations/waybar-module.json +9 -0
  38. claude_usage_widget-0.2.0/docs/integrations/waybar-style.css +4 -0
  39. claude_usage_widget-0.2.0/docs/integrations/zsh-prompt.zsh +21 -0
  40. claude_usage_widget-0.2.0/pyproject.toml +52 -0
  41. claude_usage_widget-0.2.0/requirements-macos.txt +2 -0
  42. claude_usage_widget-0.2.0/setup.cfg +4 -0
  43. claude_usage_widget-0.2.0/tests/test_analytics.py +84 -0
  44. claude_usage_widget-0.2.0/tests/test_api_server.py +67 -0
  45. claude_usage_widget-0.2.0/tests/test_cli.py +90 -0
  46. claude_usage_widget-0.2.0/tests/test_collector.py +928 -0
  47. claude_usage_widget-0.2.0/tests/test_config.py +285 -0
  48. claude_usage_widget-0.2.0/tests/test_exporter.py +78 -0
  49. claude_usage_widget-0.2.0/tests/test_forecast.py +180 -0
  50. claude_usage_widget-0.2.0/tests/test_history.py +102 -0
  51. claude_usage_widget-0.2.0/tests/test_notifier.py +91 -0
  52. claude_usage_widget-0.2.0/tests/test_pricing.py +236 -0
  53. claude_usage_widget-0.2.0/tests/test_themes.py +96 -0
  54. claude_usage_widget-0.2.0/tests/test_trends.py +82 -0
  55. claude_usage_widget-0.2.0/tests/test_updater.py +58 -0
  56. 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,6 @@
1
+ include README.md
2
+ include LICENSE
3
+ include config.json.example
4
+ recursive-include claude_usage/icons *.svg
5
+ recursive-include docs/integrations *
6
+ include requirements-macos.txt
@@ -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
+ ![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS-blue)
35
+ ![Python](https://img.shields.io/badge/python-3.10+-green)
36
+ ![License](https://img.shields.io/badge/license-MIT-lightgrey)
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
+ ![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS-blue)
6
+ ![Python](https://img.shields.io/badge/python-3.10+-green)
7
+ ![License](https://img.shields.io/badge/license-MIT-lightgrey)
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,3 @@
1
+ """Claude Usage Widget — desktop usage tracker for Claude Code."""
2
+
3
+ __version__ = "0.2.0"
@@ -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]