claude-usage-widget 0.2.0__tar.gz → 0.2.2__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 (58) hide show
  1. {claude_usage_widget-0.2.0/claude_usage_widget.egg-info → claude_usage_widget-0.2.2}/PKG-INFO +8 -1
  2. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/README.md +7 -0
  3. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/__init__.py +1 -1
  4. claude_usage_widget-0.2.2/claude_usage/__main__.py +10 -0
  5. claude_usage_widget-0.2.2/claude_usage/cli.py +227 -0
  6. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2/claude_usage_widget.egg-info}/PKG-INFO +8 -1
  7. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage_widget.egg-info/SOURCES.txt +1 -0
  8. claude_usage_widget-0.2.0/claude_usage/cli.py +0 -90
  9. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/LICENSE +0 -0
  10. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/MANIFEST.in +0 -0
  11. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/analytics.py +0 -0
  12. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/api_server.py +0 -0
  13. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/collector.py +0 -0
  14. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/config.py +0 -0
  15. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/exporter.py +0 -0
  16. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/forecast.py +0 -0
  17. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/history.py +0 -0
  18. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/icons/claude-tray.svg +0 -0
  19. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/notifier.py +0 -0
  20. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/overlay.py +0 -0
  21. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/overlay_macos.py +0 -0
  22. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/pricing.py +0 -0
  23. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/py.typed +0 -0
  24. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/themes.py +0 -0
  25. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/trends.py +0 -0
  26. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/updater.py +0 -0
  27. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/webhooks.py +0 -0
  28. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/widget.py +0 -0
  29. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage/widget_macos.py +0 -0
  30. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage_widget.egg-info/dependency_links.txt +0 -0
  31. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage_widget.egg-info/entry_points.txt +0 -0
  32. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage_widget.egg-info/requires.txt +0 -0
  33. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/claude_usage_widget.egg-info/top_level.txt +0 -0
  34. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/config.json.example +0 -0
  35. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/docs/integrations/README.md +0 -0
  36. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/docs/integrations/polybar-module.ini +0 -0
  37. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/docs/integrations/starship.toml.snippet +0 -0
  38. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/docs/integrations/tmux.conf.snippet +0 -0
  39. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/docs/integrations/waybar-module.json +0 -0
  40. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/docs/integrations/waybar-style.css +0 -0
  41. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/docs/integrations/zsh-prompt.zsh +0 -0
  42. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/pyproject.toml +0 -0
  43. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/requirements-macos.txt +0 -0
  44. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/setup.cfg +0 -0
  45. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_analytics.py +0 -0
  46. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_api_server.py +0 -0
  47. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_cli.py +0 -0
  48. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_collector.py +0 -0
  49. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_config.py +0 -0
  50. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_exporter.py +0 -0
  51. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_forecast.py +0 -0
  52. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_history.py +0 -0
  53. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_notifier.py +0 -0
  54. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_pricing.py +0 -0
  55. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_themes.py +0 -0
  56. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_trends.py +0 -0
  57. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_updater.py +0 -0
  58. {claude_usage_widget-0.2.0 → claude_usage_widget-0.2.2}/tests/test_webhooks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-usage-widget
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Desktop widget and CLI that shows real-time Claude Code usage limits and cost.
5
5
  Author: Burak
6
6
  License: MIT
@@ -214,6 +214,13 @@ The OSD is a transparent, borderless window rendered entirely via 2D drawing pri
214
214
  ### Linux: Cairo errors (`Couldn't find foreign struct converter`)
215
215
  - Install `python3-gi-cairo`: `sudo apt install python3-gi-cairo`
216
216
 
217
+ ### Linux: `ImportError: cannot import name '_gi' from partially initialized module 'gi'`
218
+ This usually means a broken user-level PyGObject install is shadowing the system package. Fix:
219
+ ```bash
220
+ pip uninstall -y PyGObject pycairo
221
+ sudo apt install --reinstall python3-gi python3-gi-cairo gir1.2-ayatanaappindicator3-0.1
222
+ ```
223
+
217
224
  ### macOS: no menu bar icon
218
225
  - Make sure `rumps` is installed: `pip3 install rumps`
219
226
  - If using a virtualenv, ensure `pyobjc-framework-Cocoa` is also installed.
@@ -185,6 +185,13 @@ The OSD is a transparent, borderless window rendered entirely via 2D drawing pri
185
185
  ### Linux: Cairo errors (`Couldn't find foreign struct converter`)
186
186
  - Install `python3-gi-cairo`: `sudo apt install python3-gi-cairo`
187
187
 
188
+ ### Linux: `ImportError: cannot import name '_gi' from partially initialized module 'gi'`
189
+ This usually means a broken user-level PyGObject install is shadowing the system package. Fix:
190
+ ```bash
191
+ pip uninstall -y PyGObject pycairo
192
+ sudo apt install --reinstall python3-gi python3-gi-cairo gir1.2-ayatanaappindicator3-0.1
193
+ ```
194
+
188
195
  ### macOS: no menu bar icon
189
196
  - Make sure `rumps` is installed: `pip3 install rumps`
190
197
  - If using a virtualenv, ensure `pyobjc-framework-Cocoa` is also installed.
@@ -1,3 +1,3 @@
1
1
  """Claude Usage Widget — desktop usage tracker for Claude Code."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
@@ -0,0 +1,10 @@
1
+ """Enable `python -m claude_usage` as an equivalent entry point to the CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from claude_usage.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
@@ -0,0 +1,227 @@
1
+ """Command-line interface for headless / scripted access to usage stats."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ from dataclasses import asdict, is_dataclass
10
+ from typing import Sequence
11
+
12
+ from claude_usage import __version__
13
+ from claude_usage.collector import UsageStats, collect_all
14
+ from claude_usage.config import load_config
15
+
16
+
17
+ def build_parser() -> argparse.ArgumentParser:
18
+ """Return the argparse parser used by the CLI dispatcher."""
19
+ p = argparse.ArgumentParser(
20
+ prog="claude-usage",
21
+ description="Claude Code usage tracker — GUI by default, CLI on demand.",
22
+ )
23
+ p.add_argument("--version", action="store_true", help="Print version and exit.")
24
+ p.add_argument("--json", action="store_true", help="Emit full stats as JSON.")
25
+ p.add_argument("--once", action="store_true", help="Collect once and print JSON.")
26
+ p.add_argument("--field", metavar="NAME", default=None,
27
+ help="Print a single UsageStats field by name.")
28
+ p.add_argument("--export", choices=("csv", "json"), default=None,
29
+ help="Export history as CSV or JSON to stdout.")
30
+ p.add_argument("--days", type=int, default=30,
31
+ help="Look-back window for --export (default: 30).")
32
+ return p
33
+
34
+
35
+ def _usage_stats_to_dict(stats: UsageStats) -> dict:
36
+ """Convert a UsageStats dataclass to a JSON-serialisable dict."""
37
+ return asdict(stats) if is_dataclass(stats) else dict(stats)
38
+
39
+
40
+ def _default_config_path() -> str:
41
+ base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
42
+ cfg = os.path.join(base_dir, "config.json")
43
+ if not os.path.isfile(cfg):
44
+ cfg = os.path.join(base_dir, "config.json.example")
45
+ return cfg
46
+
47
+
48
+ def run_cli(argv: Sequence[str]) -> int:
49
+ """Dispatch a single CLI invocation. Returns a process exit code."""
50
+ args = build_parser().parse_args(list(argv))
51
+
52
+ if args.version:
53
+ print(__version__)
54
+ return 0
55
+
56
+ if args.export:
57
+ from claude_usage.exporter import export_history
58
+ config = load_config(_default_config_path())
59
+ history_path = os.path.join(config["claude_dir"], "usage-history.jsonl")
60
+ count = export_history(history_path, fmt=args.export, days=args.days, out=sys.stdout)
61
+ print(f"# exported {count} samples", file=sys.stderr)
62
+ return 0
63
+
64
+ if args.json or args.once or args.field:
65
+ config = load_config(_default_config_path())
66
+ stats = collect_all(config)
67
+ data = _usage_stats_to_dict(stats)
68
+
69
+ if args.field is not None:
70
+ if args.field not in data:
71
+ print(f"error: unknown field {args.field!r}", file=sys.stderr)
72
+ return 2
73
+ print(data[args.field])
74
+ return 0
75
+
76
+ json.dump(data, sys.stdout, default=str, indent=2, sort_keys=True)
77
+ print()
78
+ return 0
79
+
80
+ # No CLI flag — caller should fall through to GUI.
81
+ return -1
82
+
83
+
84
+ def _launch_gui() -> None:
85
+ """Launch the platform-appropriate GUI (GTK3 on Linux, AppKit on macOS)."""
86
+ import signal
87
+
88
+ # CLI entry is usually invoked by a console script shim (`claude-usage`),
89
+ # so restore the default SIGINT handler so Ctrl-C kills the GUI cleanly.
90
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
91
+
92
+ if sys.platform == "darwin":
93
+ from claude_usage.widget_macos import ClaudeUsageTray # noqa: WPS433
94
+ config = load_config(_default_config_path())
95
+ app = ClaudeUsageTray(config)
96
+ app.run()
97
+ return
98
+
99
+ if sys.platform.startswith("linux"):
100
+ # Force XWayland; native Wayland doesn't support the override-redirect
101
+ # tricks needed for our borderless OSD overlay.
102
+ os.environ.setdefault("GDK_BACKEND", "x11")
103
+
104
+ # Guarded gi import — if the system ``python3-gi`` is broken or
105
+ # shadowed by a partial user-local install, bail out with a clear
106
+ # message instead of a confusing internal traceback.
107
+ try:
108
+ import gi # type: ignore[import-not-found]
109
+ except ImportError as exc:
110
+ _print_linux_install_instructions(exc)
111
+ sys.exit(1)
112
+
113
+ _ensure_gi_cairo_linux()
114
+
115
+ try:
116
+ gi.require_version("Gtk", "3.0")
117
+ from gi.repository import Gtk # type: ignore[attr-defined]
118
+ gi.require_version("AyatanaAppIndicator3", "0.1")
119
+ from gi.repository import AyatanaAppIndicator3 # noqa: F401
120
+ except (ValueError, ImportError) as exc:
121
+ _print_linux_install_instructions(exc)
122
+ sys.exit(1)
123
+
124
+ from claude_usage.widget import ClaudeUsageTray
125
+
126
+ config = load_config(_default_config_path())
127
+ _tray = ClaudeUsageTray(config) # noqa: F841 — tray owns its lifecycle
128
+ Gtk.main()
129
+ return
130
+
131
+ print(f"ERROR: Unsupported platform: {sys.platform}", file=sys.stderr)
132
+ sys.exit(1)
133
+
134
+
135
+ def _print_linux_install_instructions(exc: Exception) -> None:
136
+ """Print actionable install / repair instructions for GTK-related failures."""
137
+ print(
138
+ "\nERROR: GTK / GObject Introspection stack is missing or broken.\n"
139
+ f" ({exc.__class__.__name__}: {exc})\n"
140
+ "\n"
141
+ "Required system packages:\n"
142
+ " Ubuntu/Debian:\n"
143
+ " sudo apt install python3-gi python3-gi-cairo python3-cairo \\\n"
144
+ " gir1.2-ayatanaappindicator3-0.1 gir1.2-notify-0.7\n"
145
+ " Fedora:\n"
146
+ " sudo dnf install python3-gobject python3-gobject-cairo \\\n"
147
+ " libappindicator-gtk3 libnotify\n"
148
+ " Arch:\n"
149
+ " sudo pacman -S python-gobject python-cairo \\\n"
150
+ " libappindicator-gtk3 libnotify\n"
151
+ "\n"
152
+ "If 'circular import' appears, there may be a broken user-local install:\n"
153
+ " pip uninstall -y PyGObject pycairo\n"
154
+ " sudo apt install --reinstall python3-gi # or your distro equivalent\n"
155
+ "\n"
156
+ "GNOME users: the tray icon also needs the AppIndicator extension:\n"
157
+ " https://extensions.gnome.org/extension/615/appindicator-support/\n",
158
+ file=sys.stderr,
159
+ )
160
+
161
+
162
+ def _ensure_gi_cairo_linux() -> None:
163
+ """Try system gi-cairo first, then GNOME snap fallback, else warn."""
164
+ import gi # noqa: WPS433
165
+
166
+ try:
167
+ gi.require_foreign("cairo")
168
+ return
169
+ except Exception:
170
+ pass
171
+
172
+ import glob as _glob
173
+ import importlib.util
174
+
175
+ ver = f"{sys.version_info.major}{sys.version_info.minor}"
176
+ snap_so_list = sorted(
177
+ _glob.glob(
178
+ f"/snap/gnome-*/*/usr/lib/python3/dist-packages/gi/"
179
+ f"_gi_cairo.cpython-{ver}*.so"
180
+ ),
181
+ reverse=True,
182
+ )
183
+ for snap_so in snap_so_list:
184
+ try:
185
+ spec = importlib.util.spec_from_file_location("gi._gi_cairo", snap_so)
186
+ if spec is None or spec.loader is None:
187
+ continue
188
+ mod = importlib.util.module_from_spec(spec)
189
+ sys.modules["gi._gi_cairo"] = mod
190
+ spec.loader.exec_module(mod)
191
+ return
192
+ except Exception:
193
+ continue
194
+
195
+ print(
196
+ "WARNING: python3-gi-cairo not found. OSD overlay may not render.\n"
197
+ " Ubuntu/Debian: sudo apt install python3-gi-cairo\n"
198
+ " Fedora: sudo dnf install python3-gobject-cairo\n"
199
+ " Arch: sudo pacman -S python-gobject\n",
200
+ file=sys.stderr,
201
+ )
202
+
203
+
204
+ def main() -> int:
205
+ """Entry point for the ``claude-usage`` console script.
206
+
207
+ Dispatches CLI flags first; if none were given, launches the GUI and
208
+ returns once the GUI exits.
209
+ """
210
+ if sys.version_info < (3, 10):
211
+ print(
212
+ "ERROR: Python 3.10+ is required (collector.py uses str|None syntax).",
213
+ file=sys.stderr,
214
+ )
215
+ return 1
216
+
217
+ rc = run_cli(sys.argv[1:])
218
+ if rc >= 0:
219
+ return rc
220
+
221
+ # No CLI flag — fall through to the GUI.
222
+ _launch_gui()
223
+ return 0
224
+
225
+
226
+ if __name__ == "__main__":
227
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-usage-widget
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Desktop widget and CLI that shows real-time Claude Code usage limits and cost.
5
5
  Author: Burak
6
6
  License: MIT
@@ -214,6 +214,13 @@ The OSD is a transparent, borderless window rendered entirely via 2D drawing pri
214
214
  ### Linux: Cairo errors (`Couldn't find foreign struct converter`)
215
215
  - Install `python3-gi-cairo`: `sudo apt install python3-gi-cairo`
216
216
 
217
+ ### Linux: `ImportError: cannot import name '_gi' from partially initialized module 'gi'`
218
+ This usually means a broken user-level PyGObject install is shadowing the system package. Fix:
219
+ ```bash
220
+ pip uninstall -y PyGObject pycairo
221
+ sudo apt install --reinstall python3-gi python3-gi-cairo gir1.2-ayatanaappindicator3-0.1
222
+ ```
223
+
217
224
  ### macOS: no menu bar icon
218
225
  - Make sure `rumps` is installed: `pip3 install rumps`
219
226
  - If using a virtualenv, ensure `pyobjc-framework-Cocoa` is also installed.
@@ -5,6 +5,7 @@ config.json.example
5
5
  pyproject.toml
6
6
  requirements-macos.txt
7
7
  claude_usage/__init__.py
8
+ claude_usage/__main__.py
8
9
  claude_usage/analytics.py
9
10
  claude_usage/api_server.py
10
11
  claude_usage/cli.py
@@ -1,90 +0,0 @@
1
- """Command-line interface for headless / scripted access to usage stats."""
2
-
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import json
7
- import os
8
- import sys
9
- from dataclasses import asdict, is_dataclass
10
- from typing import Sequence
11
-
12
- from claude_usage import __version__
13
- from claude_usage.collector import UsageStats, collect_all
14
- from claude_usage.config import load_config
15
-
16
-
17
- def build_parser() -> argparse.ArgumentParser:
18
- """Return the argparse parser used by the CLI dispatcher."""
19
- p = argparse.ArgumentParser(
20
- prog="claude-usage",
21
- description="Claude Code usage tracker — GUI by default, CLI on demand.",
22
- )
23
- p.add_argument("--version", action="store_true", help="Print version and exit.")
24
- p.add_argument("--json", action="store_true", help="Emit full stats as JSON.")
25
- p.add_argument("--once", action="store_true", help="Collect once and print JSON.")
26
- p.add_argument("--field", metavar="NAME", default=None,
27
- help="Print a single UsageStats field by name.")
28
- p.add_argument("--export", choices=("csv", "json"), default=None,
29
- help="Export history as CSV or JSON to stdout.")
30
- p.add_argument("--days", type=int, default=30,
31
- help="Look-back window for --export (default: 30).")
32
- return p
33
-
34
-
35
- def _usage_stats_to_dict(stats: UsageStats) -> dict:
36
- """Convert a UsageStats dataclass to a JSON-serialisable dict."""
37
- return asdict(stats) if is_dataclass(stats) else dict(stats)
38
-
39
-
40
- def _default_config_path() -> str:
41
- base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
42
- cfg = os.path.join(base_dir, "config.json")
43
- if not os.path.isfile(cfg):
44
- cfg = os.path.join(base_dir, "config.json.example")
45
- return cfg
46
-
47
-
48
- def run_cli(argv: Sequence[str]) -> int:
49
- """Dispatch a single CLI invocation. Returns a process exit code."""
50
- args = build_parser().parse_args(list(argv))
51
-
52
- if args.version:
53
- print(__version__)
54
- return 0
55
-
56
- if args.export:
57
- from claude_usage.exporter import export_history
58
- config = load_config(_default_config_path())
59
- history_path = os.path.join(config["claude_dir"], "usage-history.jsonl")
60
- count = export_history(history_path, fmt=args.export, days=args.days, out=sys.stdout)
61
- print(f"# exported {count} samples", file=sys.stderr)
62
- return 0
63
-
64
- if args.json or args.once or args.field:
65
- config = load_config(_default_config_path())
66
- stats = collect_all(config)
67
- data = _usage_stats_to_dict(stats)
68
-
69
- if args.field is not None:
70
- if args.field not in data:
71
- print(f"error: unknown field {args.field!r}", file=sys.stderr)
72
- return 2
73
- print(data[args.field])
74
- return 0
75
-
76
- json.dump(data, sys.stdout, default=str, indent=2, sort_keys=True)
77
- print()
78
- return 0
79
-
80
- # No CLI flag — caller should fall through to GUI.
81
- return -1
82
-
83
-
84
- def main() -> int:
85
- """Entry point for the ``claude-usage`` console script."""
86
- return run_cli(sys.argv[1:])
87
-
88
-
89
- if __name__ == "__main__":
90
- sys.exit(main())