claude-code-usage 1.0.3__py3-none-any.whl → 2.0.0__py3-none-any.whl

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.
@@ -1,17 +1,22 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: claude-code-usage
3
- Version: 1.0.3
4
- Summary: System tray monitor for Claude Code token usage
3
+ Version: 2.0.0
4
+ Summary: Cross-platform system tray monitor for Claude Code token usage
5
5
  Author: Max
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/Maex-z9/CC_Usage
8
8
  Project-URL: Repository, https://github.com/Maex-z9/CC_Usage
9
9
  Project-URL: Issues, https://github.com/Maex-z9/CC_Usage/issues
10
- Keywords: claude,anthropic,usage,monitor,tray,gnome
10
+ Keywords: claude,anthropic,usage,monitor,tray,cross-platform
11
11
  Classifier: Development Status :: 4 - Beta
12
- Classifier: Environment :: X11 Applications :: GTK
12
+ Classifier: Environment :: MacOS X
13
+ Classifier: Environment :: Win32 (MS Windows)
14
+ Classifier: Environment :: X11 Applications
13
15
  Classifier: Intended Audience :: Developers
14
16
  Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Operating System :: Microsoft :: Windows
19
+ Classifier: Operating System :: MacOS
15
20
  Classifier: Operating System :: POSIX :: Linux
16
21
  Classifier: Programming Language :: Python :: 3
17
22
  Classifier: Programming Language :: Python :: 3.11
@@ -20,9 +25,14 @@ Classifier: Topic :: Utilities
20
25
  Requires-Python: >=3.11
21
26
  Description-Content-Type: text/markdown
22
27
  License-File: LICENSE
23
- Requires-Dist: requests>=2.28.0
24
- Requires-Dist: PyGObject>=3.42.0
25
- Dynamic: license-file
28
+ Requires-Dist: requests >=2.28.0
29
+ Requires-Dist: pystray >=0.19.0
30
+ Requires-Dist: Pillow >=9.0.0
31
+ Requires-Dist: desktop-notifier >=3.5.0
32
+ Requires-Dist: platformdirs >=3.0.0
33
+ Requires-Dist: humanize >=4.0.0
34
+ Provides-Extra: linux
35
+ Requires-Dist: PyGObject >=3.42.0 ; extra == 'linux'
26
36
 
27
37
  # Claude Code Usage Overlay
28
38
 
@@ -0,0 +1,15 @@
1
+ src/__init__.py,sha256=rzeXYIMbNbWPF6gpjv7luGXLZA67r_5GU5ZGUNb8pKA,27
2
+ src/api.py,sha256=IymlJFR7_Ou5I_Asij8oyR2b7bx9WurBtREwcysbiJQ,6168
3
+ src/autostart.py,sha256=DJ6r0P-fuanS9bc82I3pVF1mXaXbQC3rY_JKrDs5nEY,9239
4
+ src/config.py,sha256=fOqNm6cDA3eLigTWbLjWPpneuS3PiZekL_mITRVvrT4,5972
5
+ src/icon_generator.py,sha256=o9Wm7MAsScarRkrskHajgLQkmmAiBcZZ3AWgxA_95LE,3955
6
+ src/main.py,sha256=A7Ej5kqbtOYWUGSuZxx6bM-7WNimjjAs--04_kvvta0,506
7
+ src/notifier.py,sha256=cSGlZcx8eF7VNmRVWgF156PB8bJhxflu6hbUSVhuk94,10900
8
+ src/tray.py,sha256=IG3uH2AYBMIUdSfavCR5lMSNB0IVe7nuIcTkvPg3JSk,8403
9
+ src/utils.py,sha256=ZqGvw-oyzHompxRMUeYT5FdJFaQ6uvoAthhu-3bPGJk,893
10
+ claude_code_usage-2.0.0.dist-info/LICENSE,sha256=alXcUJ2-k8eqG_Amt8y9Hhlb8fT1nABafZOqWDmdzUw,1060
11
+ claude_code_usage-2.0.0.dist-info/METADATA,sha256=5IDglmOvUTFxnPkAX-6NAA9s-94QUmnSJmEGMEY_QH4,5995
12
+ claude_code_usage-2.0.0.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
13
+ claude_code_usage-2.0.0.dist-info/entry_points.txt,sha256=sRUb89gemjebLTvCQVv6TJWunjYrD-i_it3A3E9bJVc,95
14
+ claude_code_usage-2.0.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
15
+ claude_code_usage-2.0.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.2)
2
+ Generator: setuptools (73.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ claude-usage = src.main:main
3
+
4
+ [gui_scripts]
5
+ claude-usage-gui = src.main:main
src/autostart.py CHANGED
@@ -1,13 +1,24 @@
1
- """Autostart management for Claude Usage Overlay."""
1
+ """Autostart management for Claude Usage Overlay.
2
+
3
+ Cross-platform: supports Linux (.desktop), Windows (Registry), and macOS (LaunchAgent).
4
+ """
2
5
 
3
6
  import os
4
7
  import shutil
8
+ import sys
5
9
  from configparser import ConfigParser
6
10
  from pathlib import Path
7
11
 
12
+ # Detect platform
13
+ IS_WINDOWS = sys.platform == 'win32'
14
+ IS_MACOS = sys.platform == 'darwin'
15
+ IS_LINUX = sys.platform.startswith('linux')
8
16
 
9
- def get_autostart_path() -> Path:
10
- """Get XDG-compliant autostart directory path.
17
+
18
+ # --- Linux .desktop file support ---
19
+
20
+ def _get_linux_autostart_path() -> Path:
21
+ """Get XDG-compliant autostart directory path for Linux.
11
22
 
12
23
  Returns:
13
24
  Path: Path to autostart directory
@@ -24,49 +35,52 @@ def get_autostart_path() -> Path:
24
35
  return autostart_dir
25
36
 
26
37
 
27
- def get_desktop_file_path() -> Path:
28
- """Get path to the .desktop file.
38
+ def _get_linux_desktop_file_path() -> Path:
39
+ """Get path to the Linux .desktop file.
29
40
 
30
41
  Returns:
31
42
  Path: Path to claude-usage-overlay.desktop
32
43
  """
33
- return get_autostart_path() / 'claude-usage-overlay.desktop'
44
+ return _get_linux_autostart_path() / 'claude-usage-overlay.desktop'
34
45
 
35
46
 
36
47
  def _get_exec_command() -> str:
37
- """Determine the best Exec command for the .desktop file.
48
+ """Determine the best Exec command for autostart.
38
49
 
39
50
  Returns:
40
- Command string for Exec= line, preferring installed entry point
51
+ Command string, preferring installed entry point.
52
+ On Windows, paths are quoted to handle spaces.
41
53
  """
42
54
  # Prefer the installed 'claude-usage' command if available
43
55
  claude_usage_path = shutil.which("claude-usage")
44
56
  if claude_usage_path:
57
+ # Quote path on Windows in case of spaces
58
+ if IS_WINDOWS and ' ' in claude_usage_path:
59
+ return f'"{claude_usage_path}"'
45
60
  return claude_usage_path
46
61
 
47
- # Fallback to running module directly with python
48
- project_root = Path(__file__).parent.parent.absolute()
49
- main_py = project_root / "src" / "main.py"
50
- if main_py.exists():
51
- # Use sys.executable equivalent for current python
52
- return f"python3 -m src.main"
62
+ # For PyInstaller frozen executable
63
+ if getattr(sys, 'frozen', False):
64
+ if IS_WINDOWS and ' ' in sys.executable:
65
+ return f'"{sys.executable}"'
66
+ return sys.executable
53
67
 
54
- # Last resort: assume it's installed as module
55
- return "python3 -m src.main"
68
+ # Fallback to running module directly with python
69
+ if IS_WINDOWS:
70
+ # Quote Python path on Windows (often in "Program Files")
71
+ return f'"{sys.executable}" -m src.main'
72
+ return f"{sys.executable} -m src.main"
56
73
 
57
74
 
58
- def create_autostart_entry(enable: bool = True):
59
- """Create or update autostart .desktop file.
75
+ def _create_linux_autostart(enable: bool = True):
76
+ """Create or update autostart .desktop file on Linux.
60
77
 
61
78
  Args:
62
79
  enable: If True, enable autostart. If False, disable (set Hidden=true).
63
80
  """
64
- desktop_file = get_desktop_file_path()
65
-
66
- # Get best exec command (prefer installed entry point)
81
+ desktop_file = _get_linux_desktop_file_path()
67
82
  exec_cmd = _get_exec_command()
68
83
 
69
- # Desktop entry content
70
84
  hidden_value = "false" if enable else "true"
71
85
  content = f"""[Desktop Entry]
72
86
  Type=Application
@@ -82,38 +96,258 @@ X-GNOME-Autostart-enabled=true
82
96
  Hidden={hidden_value}
83
97
  """
84
98
 
85
- # Write .desktop file with secure permissions (rw-r--r--)
86
- # .desktop files don't need execute bit - they're parsed by desktop environment
87
99
  desktop_file.write_text(content)
88
100
  desktop_file.chmod(0o644)
89
101
 
90
102
 
91
- def remove_autostart_entry():
92
- """Remove autostart .desktop file."""
93
- desktop_file = get_desktop_file_path()
103
+ def _remove_linux_autostart():
104
+ """Remove autostart .desktop file on Linux."""
105
+ desktop_file = _get_linux_desktop_file_path()
94
106
  if desktop_file.exists():
95
107
  desktop_file.unlink()
96
108
 
97
109
 
98
- def is_autostart_enabled() -> bool:
99
- """Check if autostart is enabled.
110
+ def _is_linux_autostart_enabled() -> bool:
111
+ """Check if autostart is enabled on Linux.
100
112
 
101
113
  Returns:
102
114
  bool: True if autostart is enabled (file exists and Hidden!=true)
103
115
  """
104
- desktop_file = get_desktop_file_path()
116
+ desktop_file = _get_linux_desktop_file_path()
105
117
 
106
118
  if not desktop_file.exists():
107
119
  return False
108
120
 
109
- # Use configparser for proper INI parsing
110
121
  parser = ConfigParser(interpolation=None)
111
122
  try:
112
123
  parser.read(desktop_file)
113
- # Desktop files use 'Desktop Entry' section
114
- # Hidden=true means disabled; absent or false means enabled
115
124
  hidden = parser.get('Desktop Entry', 'Hidden', fallback='false')
116
125
  return hidden.lower() != 'true'
117
126
  except Exception:
118
- # If parsing fails, assume enabled if file exists
119
127
  return True
128
+
129
+
130
+ # --- Windows Registry support ---
131
+
132
+ WINDOWS_RUN_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
133
+ WINDOWS_APP_NAME = "ClaudeUsageOverlay"
134
+
135
+
136
+ def _create_windows_autostart(enable: bool = True):
137
+ """Create or remove autostart Registry entry on Windows.
138
+
139
+ Args:
140
+ enable: If True, enable autostart. If False, remove entry.
141
+ """
142
+ if not IS_WINDOWS:
143
+ return
144
+
145
+ import winreg
146
+
147
+ exec_cmd = _get_exec_command()
148
+
149
+ try:
150
+ key = winreg.OpenKey(
151
+ winreg.HKEY_CURRENT_USER,
152
+ WINDOWS_RUN_KEY,
153
+ 0,
154
+ winreg.KEY_SET_VALUE
155
+ )
156
+ try:
157
+ if enable:
158
+ winreg.SetValueEx(key, WINDOWS_APP_NAME, 0, winreg.REG_SZ, exec_cmd)
159
+ else:
160
+ try:
161
+ winreg.DeleteValue(key, WINDOWS_APP_NAME)
162
+ except FileNotFoundError:
163
+ pass # Already removed
164
+ finally:
165
+ winreg.CloseKey(key)
166
+ except OSError as e:
167
+ print(f"Failed to modify Windows autostart: {e}", file=sys.stderr)
168
+
169
+
170
+ def _remove_windows_autostart():
171
+ """Remove autostart Registry entry on Windows."""
172
+ _create_windows_autostart(enable=False)
173
+
174
+
175
+ def _is_windows_autostart_enabled() -> bool:
176
+ """Check if autostart is enabled on Windows.
177
+
178
+ Returns:
179
+ bool: True if Registry entry exists
180
+ """
181
+ if not IS_WINDOWS:
182
+ return False
183
+
184
+ import winreg
185
+
186
+ try:
187
+ key = winreg.OpenKey(
188
+ winreg.HKEY_CURRENT_USER,
189
+ WINDOWS_RUN_KEY,
190
+ 0,
191
+ winreg.KEY_READ
192
+ )
193
+ try:
194
+ winreg.QueryValueEx(key, WINDOWS_APP_NAME)
195
+ return True
196
+ except FileNotFoundError:
197
+ return False
198
+ finally:
199
+ winreg.CloseKey(key)
200
+ except OSError:
201
+ return False
202
+
203
+
204
+ # --- macOS LaunchAgent support ---
205
+
206
+ def _get_macos_launch_agent_path() -> Path:
207
+ """Get path to macOS LaunchAgent plist file.
208
+
209
+ Returns:
210
+ Path: Path to com.claude-usage.overlay.plist
211
+ """
212
+ launch_agents_dir = Path.home() / "Library" / "LaunchAgents"
213
+ launch_agents_dir.mkdir(parents=True, exist_ok=True)
214
+ return launch_agents_dir / "com.claude-usage.overlay.plist"
215
+
216
+
217
+ def _create_macos_autostart(enable: bool = True):
218
+ """Create or update LaunchAgent plist on macOS.
219
+
220
+ Args:
221
+ enable: If True, enable autostart. If False, remove plist.
222
+ """
223
+ if not IS_MACOS:
224
+ return
225
+
226
+ plist_path = _get_macos_launch_agent_path()
227
+
228
+ if not enable:
229
+ if plist_path.exists():
230
+ plist_path.unlink()
231
+ return
232
+
233
+ exec_cmd = _get_exec_command()
234
+
235
+ # For module execution, split into program and arguments
236
+ if " -m " in exec_cmd:
237
+ parts = exec_cmd.split()
238
+ program = parts[0]
239
+ args = parts[1:]
240
+ args_xml = "\n".join(f" <string>{arg}</string>" for arg in args)
241
+ program_args = f""" <key>ProgramArguments</key>
242
+ <array>
243
+ <string>{program}</string>
244
+ {args_xml}
245
+ </array>"""
246
+ else:
247
+ program_args = f""" <key>ProgramArguments</key>
248
+ <array>
249
+ <string>{exec_cmd}</string>
250
+ </array>"""
251
+
252
+ plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
253
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
254
+ <plist version="1.0">
255
+ <dict>
256
+ <key>Label</key>
257
+ <string>com.claude-usage.overlay</string>
258
+ {program_args}
259
+ <key>RunAtLoad</key>
260
+ <true/>
261
+ <key>KeepAlive</key>
262
+ <false/>
263
+ </dict>
264
+ </plist>
265
+ """
266
+
267
+ plist_path.write_text(plist_content)
268
+
269
+
270
+ def _remove_macos_autostart():
271
+ """Remove LaunchAgent plist on macOS."""
272
+ _create_macos_autostart(enable=False)
273
+
274
+
275
+ def _is_macos_autostart_enabled() -> bool:
276
+ """Check if autostart is enabled on macOS.
277
+
278
+ Returns:
279
+ bool: True if LaunchAgent plist exists
280
+ """
281
+ if not IS_MACOS:
282
+ return False
283
+
284
+ return _get_macos_launch_agent_path().exists()
285
+
286
+
287
+ # --- Cross-platform public API ---
288
+
289
+ def get_autostart_path() -> Path:
290
+ """Get platform-specific autostart path.
291
+
292
+ Returns:
293
+ Path: Path to autostart configuration location
294
+ """
295
+ if IS_WINDOWS:
296
+ # Return a conceptual path for documentation purposes
297
+ return Path("HKCU") / WINDOWS_RUN_KEY / WINDOWS_APP_NAME
298
+ elif IS_MACOS:
299
+ return _get_macos_launch_agent_path()
300
+ else:
301
+ return _get_linux_autostart_path()
302
+
303
+
304
+ def get_desktop_file_path() -> Path:
305
+ """Get path to the autostart configuration file.
306
+
307
+ Returns:
308
+ Path: Platform-specific autostart file path
309
+ """
310
+ if IS_WINDOWS:
311
+ return Path("HKCU") / WINDOWS_RUN_KEY / WINDOWS_APP_NAME
312
+ elif IS_MACOS:
313
+ return _get_macos_launch_agent_path()
314
+ else:
315
+ return _get_linux_desktop_file_path()
316
+
317
+
318
+ def create_autostart_entry(enable: bool = True):
319
+ """Create or update autostart entry for current platform.
320
+
321
+ Args:
322
+ enable: If True, enable autostart. If False, disable.
323
+ """
324
+ if IS_WINDOWS:
325
+ _create_windows_autostart(enable)
326
+ elif IS_MACOS:
327
+ _create_macos_autostart(enable)
328
+ else:
329
+ _create_linux_autostart(enable)
330
+
331
+
332
+ def remove_autostart_entry():
333
+ """Remove autostart entry for current platform."""
334
+ if IS_WINDOWS:
335
+ _remove_windows_autostart()
336
+ elif IS_MACOS:
337
+ _remove_macos_autostart()
338
+ else:
339
+ _remove_linux_autostart()
340
+
341
+
342
+ def is_autostart_enabled() -> bool:
343
+ """Check if autostart is enabled for current platform.
344
+
345
+ Returns:
346
+ bool: True if autostart is enabled
347
+ """
348
+ if IS_WINDOWS:
349
+ return _is_windows_autostart_enabled()
350
+ elif IS_MACOS:
351
+ return _is_macos_autostart_enabled()
352
+ else:
353
+ return _is_linux_autostart_enabled()
src/config.py CHANGED
@@ -1,4 +1,7 @@
1
- """Configuration module for loading Claude OAuth credentials."""
1
+ """Configuration module for loading Claude OAuth credentials.
2
+
3
+ Cross-platform: uses platformdirs for config directory resolution.
4
+ """
2
5
 
3
6
  import json
4
7
  import os
@@ -6,9 +9,15 @@ import time
6
9
  from dataclasses import asdict, dataclass
7
10
  from pathlib import Path
8
11
 
12
+ import platformdirs
13
+
9
14
 
10
15
  CREDENTIALS_PATH = Path.home() / ".claude" / ".credentials.json"
11
16
 
17
+ # Application info for platformdirs
18
+ APP_NAME = "claude-usage-overlay"
19
+ APP_AUTHOR = "claude-usage" # Used on Windows
20
+
12
21
 
13
22
  def load_credentials() -> dict:
14
23
  """Load Claude OAuth credentials from ~/.claude/.credentials.json.
@@ -74,22 +83,27 @@ def get_access_token() -> str:
74
83
  return credentials["accessToken"]
75
84
 
76
85
 
77
- def get_config_path() -> Path:
78
- """Get XDG-compliant config file path.
86
+ def get_config_dir() -> Path:
87
+ """Get cross-platform config directory.
79
88
 
80
89
  Returns:
81
- Path: Path to config.json in XDG config directory
90
+ Path: Config directory path
91
+ - Linux: ~/.config/claude-usage-overlay/
92
+ - Windows: %APPDATA%/claude-usage/claude-usage-overlay/
93
+ - macOS: ~/Library/Application Support/claude-usage-overlay/
82
94
  """
83
- config_home = os.environ.get('XDG_CONFIG_HOME')
84
- if config_home:
85
- base = Path(config_home)
86
- else:
87
- base = Path.home() / '.config'
88
-
89
- config_dir = base / 'claude-usage-overlay'
95
+ config_dir = Path(platformdirs.user_config_dir(APP_NAME, APP_AUTHOR))
90
96
  config_dir.mkdir(parents=True, exist_ok=True)
97
+ return config_dir
91
98
 
92
- return config_dir / 'config.json'
99
+
100
+ def get_config_path() -> Path:
101
+ """Get cross-platform config file path.
102
+
103
+ Returns:
104
+ Path: Path to config.json in platform-specific config directory
105
+ """
106
+ return get_config_dir() / 'config.json'
93
107
 
94
108
 
95
109
  @dataclass
@@ -140,7 +154,7 @@ class UserConfig:
140
154
 
141
155
  @classmethod
142
156
  def load(cls) -> 'UserConfig':
143
- """Load configuration from XDG config file.
157
+ """Load configuration from config file.
144
158
 
145
159
  Returns:
146
160
  UserConfig: Configuration object with values from file or defaults
@@ -165,12 +179,16 @@ class UserConfig:
165
179
  raise ValueError(f"Invalid config file structure: {e}")
166
180
 
167
181
  def save(self):
168
- """Save configuration to XDG config file."""
182
+ """Save configuration to config file."""
169
183
  config_path = get_config_path()
170
184
  config_path.parent.mkdir(parents=True, exist_ok=True)
171
185
 
172
186
  with open(config_path, 'w') as f:
173
187
  json.dump(asdict(self), f, indent=2)
174
188
 
175
- # Set secure permissions (owner read/write only)
176
- os.chmod(config_path, 0o600)
189
+ # Set secure permissions (owner read/write only) on Unix
190
+ try:
191
+ os.chmod(config_path, 0o600)
192
+ except (OSError, AttributeError):
193
+ # Windows doesn't support chmod the same way, skip
194
+ pass