claude-code-usage 1.0.3__tar.gz → 2.0.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 (25) hide show
  1. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/PKG-INFO +17 -7
  2. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/claude_code_usage.egg-info/PKG-INFO +17 -7
  3. claude_code_usage-2.0.0/claude_code_usage.egg-info/entry_points.txt +5 -0
  4. claude_code_usage-2.0.0/claude_code_usage.egg-info/requires.txt +9 -0
  5. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/pyproject.toml +24 -6
  6. claude_code_usage-2.0.0/src/autostart.py +353 -0
  7. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/src/config.py +34 -16
  8. claude_code_usage-2.0.0/src/icon_generator.py +120 -0
  9. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/src/notifier.py +95 -82
  10. claude_code_usage-2.0.0/src/tray.py +231 -0
  11. claude_code_usage-1.0.3/claude_code_usage.egg-info/entry_points.txt +0 -2
  12. claude_code_usage-1.0.3/claude_code_usage.egg-info/requires.txt +0 -2
  13. claude_code_usage-1.0.3/src/autostart.py +0 -119
  14. claude_code_usage-1.0.3/src/icon_generator.py +0 -112
  15. claude_code_usage-1.0.3/src/tray.py +0 -302
  16. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/LICENSE +0 -0
  17. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/README.md +0 -0
  18. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/claude_code_usage.egg-info/SOURCES.txt +0 -0
  19. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/claude_code_usage.egg-info/dependency_links.txt +0 -0
  20. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/claude_code_usage.egg-info/top_level.txt +0 -0
  21. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/setup.cfg +0 -0
  22. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/src/__init__.py +0 -0
  23. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/src/api.py +0 -0
  24. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/src/main.py +0 -0
  25. {claude_code_usage-1.0.3 → claude_code_usage-2.0.0}/src/utils.py +0 -0
@@ -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
@@ -21,8 +26,13 @@ Requires-Python: >=3.11
21
26
  Description-Content-Type: text/markdown
22
27
  License-File: LICENSE
23
28
  Requires-Dist: requests>=2.28.0
24
- Requires-Dist: PyGObject>=3.42.0
25
- Dynamic: license-file
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
 
@@ -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
@@ -21,8 +26,13 @@ Requires-Python: >=3.11
21
26
  Description-Content-Type: text/markdown
22
27
  License-File: LICENSE
23
28
  Requires-Dist: requests>=2.28.0
24
- Requires-Dist: PyGObject>=3.42.0
25
- Dynamic: license-file
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,5 @@
1
+ [console_scripts]
2
+ claude-usage = src.main:main
3
+
4
+ [gui_scripts]
5
+ claude-usage-gui = src.main:main
@@ -0,0 +1,9 @@
1
+ requests>=2.28.0
2
+ pystray>=0.19.0
3
+ Pillow>=9.0.0
4
+ desktop-notifier>=3.5.0
5
+ platformdirs>=3.0.0
6
+ humanize>=4.0.0
7
+
8
+ [linux]
9
+ PyGObject>=3.42.0
@@ -1,22 +1,27 @@
1
1
  [build-system]
2
- requires = ["setuptools>=61.0"]
2
+ requires = ["setuptools>=61.0,<74"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "claude-code-usage"
7
- version = "1.0.3"
8
- description = "System tray monitor for Claude Code token usage"
7
+ version = "2.0.0"
8
+ description = "Cross-platform system tray monitor for Claude Code token usage"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
11
11
  authors = [
12
12
  {name = "Max"}
13
13
  ]
14
- keywords = ["claude", "anthropic", "usage", "monitor", "tray", "gnome"]
14
+ keywords = ["claude", "anthropic", "usage", "monitor", "tray", "cross-platform"]
15
15
  classifiers = [
16
16
  "Development Status :: 4 - Beta",
17
- "Environment :: X11 Applications :: GTK",
17
+ "Environment :: MacOS X",
18
+ "Environment :: Win32 (MS Windows)",
19
+ "Environment :: X11 Applications",
18
20
  "Intended Audience :: Developers",
19
21
  "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Operating System :: Microsoft :: Windows",
24
+ "Operating System :: MacOS",
20
25
  "Operating System :: POSIX :: Linux",
21
26
  "Programming Language :: Python :: 3",
22
27
  "Programming Language :: Python :: 3.11",
@@ -26,9 +31,19 @@ classifiers = [
26
31
  requires-python = ">=3.11"
27
32
  dependencies = [
28
33
  "requests>=2.28.0",
29
- "PyGObject>=3.42.0",
34
+ "pystray>=0.19.0",
35
+ "Pillow>=9.0.0",
36
+ "desktop-notifier>=3.5.0",
37
+ "platformdirs>=3.0.0",
38
+ "humanize>=4.0.0",
30
39
  ]
31
40
 
41
+ [project.optional-dependencies]
42
+ # Linux/GNOME: pystray needs PyGObject for AppIndicator menu support
43
+ # Install system packages: sudo apt install python3-gi gir1.2-ayatanaappindicator3-0.1
44
+ # Then use: pip install -e . --system-site-packages
45
+ linux = ["PyGObject>=3.42.0"]
46
+
32
47
  [project.urls]
33
48
  Homepage = "https://github.com/Maex-z9/CC_Usage"
34
49
  Repository = "https://github.com/Maex-z9/CC_Usage"
@@ -37,6 +52,9 @@ Issues = "https://github.com/Maex-z9/CC_Usage/issues"
37
52
  [project.scripts]
38
53
  claude-usage = "src.main:main"
39
54
 
55
+ [project.gui-scripts]
56
+ claude-usage-gui = "src.main:main"
57
+
40
58
  [tool.setuptools.packages.find]
41
59
  where = ["."]
42
60
  include = ["src*"]
@@ -0,0 +1,353 @@
1
+ """Autostart management for Claude Usage Overlay.
2
+
3
+ Cross-platform: supports Linux (.desktop), Windows (Registry), and macOS (LaunchAgent).
4
+ """
5
+
6
+ import os
7
+ import shutil
8
+ import sys
9
+ from configparser import ConfigParser
10
+ from pathlib import Path
11
+
12
+ # Detect platform
13
+ IS_WINDOWS = sys.platform == 'win32'
14
+ IS_MACOS = sys.platform == 'darwin'
15
+ IS_LINUX = sys.platform.startswith('linux')
16
+
17
+
18
+ # --- Linux .desktop file support ---
19
+
20
+ def _get_linux_autostart_path() -> Path:
21
+ """Get XDG-compliant autostart directory path for Linux.
22
+
23
+ Returns:
24
+ Path: Path to autostart directory
25
+ """
26
+ config_home = os.environ.get('XDG_CONFIG_HOME')
27
+ if config_home:
28
+ base = Path(config_home)
29
+ else:
30
+ base = Path.home() / '.config'
31
+
32
+ autostart_dir = base / 'autostart'
33
+ autostart_dir.mkdir(parents=True, exist_ok=True)
34
+
35
+ return autostart_dir
36
+
37
+
38
+ def _get_linux_desktop_file_path() -> Path:
39
+ """Get path to the Linux .desktop file.
40
+
41
+ Returns:
42
+ Path: Path to claude-usage-overlay.desktop
43
+ """
44
+ return _get_linux_autostart_path() / 'claude-usage-overlay.desktop'
45
+
46
+
47
+ def _get_exec_command() -> str:
48
+ """Determine the best Exec command for autostart.
49
+
50
+ Returns:
51
+ Command string, preferring installed entry point.
52
+ On Windows, paths are quoted to handle spaces.
53
+ """
54
+ # Prefer the installed 'claude-usage' command if available
55
+ claude_usage_path = shutil.which("claude-usage")
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}"'
60
+ return claude_usage_path
61
+
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
67
+
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"
73
+
74
+
75
+ def _create_linux_autostart(enable: bool = True):
76
+ """Create or update autostart .desktop file on Linux.
77
+
78
+ Args:
79
+ enable: If True, enable autostart. If False, disable (set Hidden=true).
80
+ """
81
+ desktop_file = _get_linux_desktop_file_path()
82
+ exec_cmd = _get_exec_command()
83
+
84
+ hidden_value = "false" if enable else "true"
85
+ content = f"""[Desktop Entry]
86
+ Type=Application
87
+ Version=1.0
88
+ Name=Claude Code Usage Monitor
89
+ Comment=System tray monitor for Claude Code token usage
90
+ Exec={exec_cmd}
91
+ Icon=dialog-information
92
+ Terminal=false
93
+ Categories=Utility;Monitor;
94
+ StartupNotify=false
95
+ X-GNOME-Autostart-enabled=true
96
+ Hidden={hidden_value}
97
+ """
98
+
99
+ desktop_file.write_text(content)
100
+ desktop_file.chmod(0o644)
101
+
102
+
103
+ def _remove_linux_autostart():
104
+ """Remove autostart .desktop file on Linux."""
105
+ desktop_file = _get_linux_desktop_file_path()
106
+ if desktop_file.exists():
107
+ desktop_file.unlink()
108
+
109
+
110
+ def _is_linux_autostart_enabled() -> bool:
111
+ """Check if autostart is enabled on Linux.
112
+
113
+ Returns:
114
+ bool: True if autostart is enabled (file exists and Hidden!=true)
115
+ """
116
+ desktop_file = _get_linux_desktop_file_path()
117
+
118
+ if not desktop_file.exists():
119
+ return False
120
+
121
+ parser = ConfigParser(interpolation=None)
122
+ try:
123
+ parser.read(desktop_file)
124
+ hidden = parser.get('Desktop Entry', 'Hidden', fallback='false')
125
+ return hidden.lower() != 'true'
126
+ except Exception:
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()
@@ -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