tool-tray 0.3.8__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.
tool_tray/__init__.py ADDED
@@ -0,0 +1,368 @@
1
+ __version__ = "0.3.8"
2
+
3
+
4
+ def main() -> None:
5
+ import sys
6
+
7
+ args = sys.argv[1:]
8
+
9
+ if not args:
10
+ # Default: run tray app
11
+ from tool_tray.tray import run_tray
12
+
13
+ run_tray()
14
+ return
15
+
16
+ command = args[0]
17
+
18
+ if command == "encode":
19
+ _cmd_encode(args[1:])
20
+ elif command == "setup":
21
+ _cmd_setup(args[1:])
22
+ elif command == "reset":
23
+ _cmd_reset()
24
+ elif command == "init":
25
+ _cmd_init()
26
+ elif command == "autostart":
27
+ _cmd_autostart(args[1:])
28
+ elif command == "logs":
29
+ _cmd_logs(args[1:])
30
+ elif command == "cleanup":
31
+ _cmd_cleanup(args[1:])
32
+ elif command in ("-h", "--help", "help"):
33
+ _cmd_help()
34
+ elif command in ("-v", "--version", "version"):
35
+ print(f"tooltray {__version__}")
36
+ else:
37
+ print(f"Unknown command: {command}")
38
+ print("Run 'tooltray --help' for usage")
39
+ sys.exit(1)
40
+
41
+
42
+ def _cmd_help() -> None:
43
+ print("""Tool Tray - System tray tool manager
44
+
45
+ Usage:
46
+ tooltray Run system tray app
47
+ tooltray setup Configure via GUI dialog
48
+ tooltray reset Remove config and start fresh
49
+ tooltray init Create tooltray.toml in current directory
50
+ tooltray encode Generate config code for sharing
51
+ tooltray autostart Manage system autostart
52
+ tooltray logs View log file
53
+ tooltray cleanup Remove orphaned desktop icons
54
+
55
+ Setup options:
56
+ --code CODE Config code (skip GUI dialog)
57
+
58
+ Encode options:
59
+ --token TOKEN GitHub PAT (required)
60
+ --repo ORG/REPO Repository to include (can be repeated)
61
+ --prefix PREFIX Code prefix for branding (default: TB)
62
+
63
+ Autostart options:
64
+ --enable Add tooltray to system startup
65
+ --disable Remove from system startup
66
+ --status Check if autostart is enabled
67
+
68
+ Logs options:
69
+ -f, --follow Tail log file (like tail -f)
70
+ --path Print log file path
71
+
72
+ Cleanup options:
73
+ --dry-run Show what would be removed
74
+ --force Remove without confirmation
75
+
76
+ Examples:
77
+ tooltray setup
78
+ tooltray setup --code "TB-eyJ0b2tlbi..."
79
+ tooltray encode --token ghp_xxx --repo myorg/myapp --repo myorg/cli
80
+ tooltray autostart --enable
81
+ tooltray cleanup --dry-run
82
+ """)
83
+
84
+
85
+ def _cmd_setup(args: list[str]) -> None:
86
+ from tool_tray.config import decode_config, save_config
87
+
88
+ # Check for --code flag
89
+ code: str | None = None
90
+ i = 0
91
+ while i < len(args):
92
+ if args[i] == "--code" and i + 1 < len(args):
93
+ code = args[i + 1]
94
+ break
95
+ i += 1
96
+
97
+ if code:
98
+ # Direct CLI mode
99
+ try:
100
+ config = decode_config(code)
101
+ save_config(config)
102
+ print("Configuration saved successfully!")
103
+ except ValueError as e:
104
+ print(f"Error: {e}")
105
+ else:
106
+ # GUI dialog mode
107
+ from tool_tray.setup_dialog import show_setup_dialog
108
+
109
+ if show_setup_dialog():
110
+ print("Configuration saved successfully!")
111
+ else:
112
+ print("Setup cancelled")
113
+
114
+
115
+ def _cmd_reset() -> None:
116
+ from tool_tray.config import get_config_path
117
+
118
+ path = get_config_path()
119
+ if not path.exists():
120
+ print("No config found")
121
+ return
122
+
123
+ print(f"Config file: {path}")
124
+ try:
125
+ confirm = input("Remove config? [y/N] ").strip().lower()
126
+ except (EOFError, KeyboardInterrupt):
127
+ print()
128
+ return
129
+
130
+ if confirm == "y":
131
+ path.unlink()
132
+ print("Config removed")
133
+ else:
134
+ print("Cancelled")
135
+
136
+
137
+ def _cmd_init() -> None:
138
+ from pathlib import Path
139
+
140
+ manifest_path = Path("tooltray.toml")
141
+ if manifest_path.exists():
142
+ print(f"Already exists: {manifest_path}")
143
+ return
144
+
145
+ template = """name = "" # Display name in tray menu
146
+ type = "uv" # uv | git
147
+ launch = "" # Command to run when clicked
148
+ """
149
+
150
+ manifest_path.write_text(template)
151
+
152
+ print("""tooltray.toml created!
153
+
154
+ Tool Tray is a system tray app that manages tools from private GitHub repos.
155
+ Users get a config code with repo list + token, tooltray fetches manifests.
156
+
157
+ Edit tooltray.toml:
158
+ name - Display name in the tray menu
159
+ type - "uv" for Python tools, "git" for clone+build
160
+ launch - Command name to run when clicked (usually same as name)
161
+
162
+ Optional fields:
163
+ build - Build command for git type (e.g. "npm install")
164
+ desktop_icon - Set to true to create desktop shortcut
165
+ autostart - Set to true to launch on system startup
166
+ icon - Path to icon file in repo
167
+
168
+ Once configured, commit tooltray.toml to your repo.
169
+ """)
170
+
171
+
172
+ def _cmd_autostart(args: list[str]) -> None:
173
+ import sys
174
+
175
+ from tool_tray.autostart import (
176
+ disable_autostart,
177
+ enable_autostart,
178
+ is_autostart_enabled,
179
+ )
180
+
181
+ if not args:
182
+ print("Usage: tooltray autostart [--enable|--disable|--status]")
183
+ sys.exit(1)
184
+
185
+ option = args[0]
186
+ if option == "--enable":
187
+ if enable_autostart():
188
+ print("Autostart enabled")
189
+ else:
190
+ sys.exit(1)
191
+ elif option == "--disable":
192
+ if disable_autostart():
193
+ print("Autostart disabled")
194
+ else:
195
+ sys.exit(1)
196
+ elif option == "--status":
197
+ if is_autostart_enabled():
198
+ print("Autostart: enabled")
199
+ else:
200
+ print("Autostart: disabled")
201
+ else:
202
+ print(f"Unknown option: {option}")
203
+ print("Usage: tooltray autostart [--enable|--disable|--status]")
204
+ sys.exit(1)
205
+
206
+
207
+ def _cmd_logs(args: list[str]) -> None:
208
+ import time
209
+
210
+ from tool_tray.logging import get_log_dir
211
+
212
+ log_file = get_log_dir() / "tooltray.log"
213
+
214
+ if "--path" in args:
215
+ print(log_file)
216
+ return
217
+
218
+ if not log_file.exists():
219
+ print(f"No log file yet: {log_file}")
220
+ return
221
+
222
+ if "-f" in args or "--follow" in args:
223
+ try:
224
+ with open(log_file) as f:
225
+ f.seek(0, 2)
226
+ while True:
227
+ line = f.readline()
228
+ if line:
229
+ print(line, end="")
230
+ else:
231
+ time.sleep(0.5)
232
+ except KeyboardInterrupt:
233
+ pass
234
+ else:
235
+ lines = log_file.read_text().splitlines()
236
+ for line in lines[-50:]:
237
+ print(line)
238
+
239
+
240
+ def _cmd_cleanup(args: list[str]) -> None:
241
+ from pathlib import Path
242
+
243
+ from tool_tray.config import load_config
244
+ from tool_tray.desktop import remove_desktop_icon
245
+ from tool_tray.manifest import fetch_manifest
246
+ from tool_tray.state import load_state, remove_icon_record
247
+
248
+ dry_run = "--dry-run" in args
249
+ force = "--force" in args
250
+
251
+ # Load config to get active repos
252
+ config = load_config()
253
+ if not config:
254
+ print("No config found. Run 'tooltray setup' first.")
255
+ return
256
+
257
+ token = config.get("token", "")
258
+ repos = config.get("repos", [])
259
+ active_repos = set(repos)
260
+
261
+ # Build manifest lookup for active repos
262
+ manifest_by_repo: dict[str, bool] = {} # repo -> desktop_icon enabled
263
+ for repo in repos:
264
+ manifest = fetch_manifest(repo, token)
265
+ if manifest:
266
+ manifest_by_repo[repo] = manifest.desktop_icon
267
+
268
+ # Find orphaned icons
269
+ state = load_state()
270
+ orphans: list[tuple[str, str, str]] = [] # (tool_name, path, reason)
271
+
272
+ for tool_name, record in state.desktop_icons.items():
273
+ icon_path = Path(record.path)
274
+
275
+ if not icon_path.exists():
276
+ orphans.append((tool_name, record.path, "file missing"))
277
+ elif record.repo not in active_repos:
278
+ orphans.append((tool_name, record.path, "repo removed"))
279
+ elif record.repo in manifest_by_repo and not manifest_by_repo[record.repo]:
280
+ orphans.append((tool_name, record.path, "desktop_icon disabled"))
281
+
282
+ if not orphans:
283
+ print("No orphaned icons found.")
284
+ return
285
+
286
+ # Display orphans
287
+ print(f"Found {len(orphans)} orphaned icon(s):\n")
288
+ for tool_name, path, reason in orphans:
289
+ print(f" {tool_name}")
290
+ print(f" Path: {path}")
291
+ print(f" Reason: {reason}\n")
292
+
293
+ if dry_run:
294
+ print("Dry run - no changes made.")
295
+ return
296
+
297
+ # Confirm unless --force
298
+ if not force:
299
+ try:
300
+ confirm = input("Remove these icons? [y/N] ").strip().lower()
301
+ except (EOFError, KeyboardInterrupt):
302
+ print()
303
+ return
304
+
305
+ if confirm != "y":
306
+ print("Cancelled")
307
+ return
308
+
309
+ # Remove orphans
310
+ removed = 0
311
+ for tool_name, path, reason in orphans:
312
+ if reason == "file missing":
313
+ remove_icon_record(tool_name)
314
+ removed += 1
315
+ print(f"Removed record: {tool_name}")
316
+ else:
317
+ if remove_desktop_icon(tool_name):
318
+ remove_icon_record(tool_name)
319
+ removed += 1
320
+ print(f"Removed: {tool_name}")
321
+ else:
322
+ print(f"Failed to remove: {tool_name}")
323
+
324
+ print(f"\nCleaned up {removed} icon(s).")
325
+
326
+
327
+ def _cmd_encode(args: list[str]) -> None:
328
+ import sys
329
+
330
+ from tool_tray.config import encode_config
331
+
332
+ token = ""
333
+ prefix = "TB"
334
+ repos: list[str] = []
335
+
336
+ i = 0
337
+ while i < len(args):
338
+ arg = args[i]
339
+ if arg == "--token" and i + 1 < len(args):
340
+ token = args[i + 1]
341
+ i += 2
342
+ elif arg == "--prefix" and i + 1 < len(args):
343
+ prefix = args[i + 1]
344
+ i += 2
345
+ elif arg == "--repo" and i + 1 < len(args):
346
+ from urllib.parse import unquote
347
+
348
+ repo = unquote(args[i + 1]).strip().strip("'\"")
349
+ if "/" not in repo:
350
+ print(f"Invalid repo format: {repo}")
351
+ print("Expected: ORG/REPO (e.g., myorg/myapp)")
352
+ sys.exit(1)
353
+ repos.append(repo)
354
+ i += 2
355
+ else:
356
+ print(f"Unknown option: {arg}")
357
+ sys.exit(1)
358
+
359
+ if not token:
360
+ print("Error: --token is required")
361
+ sys.exit(1)
362
+
363
+ if not repos:
364
+ print("Error: at least one --repo is required")
365
+ sys.exit(1)
366
+
367
+ code = encode_config(token, repos, prefix)
368
+ print(code)
tool_tray/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from tool_tray import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
tool_tray/autostart.py ADDED
@@ -0,0 +1,248 @@
1
+ import subprocess
2
+ import sys
3
+ from pathlib import Path
4
+
5
+
6
+ def _get_tooltray_path() -> str:
7
+ """Get the path to tooltray executable."""
8
+ try:
9
+ result = subprocess.run(
10
+ ["uv", "tool", "list", "--show-paths"],
11
+ capture_output=True,
12
+ text=True,
13
+ check=True,
14
+ )
15
+ for line in result.stdout.splitlines():
16
+ if line.startswith("- tooltray ") or line.startswith("- tool-tray "):
17
+ start = line.find("(")
18
+ end = line.find(")")
19
+ if start != -1 and end != -1:
20
+ return line[start + 1 : end]
21
+ except subprocess.CalledProcessError:
22
+ pass
23
+ return "tooltray"
24
+
25
+
26
+ def _linux_autostart_enable() -> bool:
27
+ """Add tooltray to Linux autostart."""
28
+ from tool_tray.logging import log_error, log_info
29
+
30
+ desktop_file = Path.home() / ".config/autostart/tooltray.desktop"
31
+ desktop_file.parent.mkdir(parents=True, exist_ok=True)
32
+ exe = _get_tooltray_path()
33
+ try:
34
+ desktop_file.write_text(f"""[Desktop Entry]
35
+ Type=Application
36
+ Name=Tool Tray
37
+ Comment=System tray tool manager
38
+ Exec={exe}
39
+ Hidden=false
40
+ NoDisplay=false
41
+ X-GNOME-Autostart-enabled=true
42
+ """)
43
+ log_info(f"Autostart enabled: {desktop_file}")
44
+ print(f"Autostart enabled: {desktop_file}")
45
+ return True
46
+ except OSError as e:
47
+ log_error(f"Failed to enable autostart: {desktop_file}", e)
48
+ print(f"Failed to enable autostart: {e}")
49
+ return False
50
+
51
+
52
+ def _linux_autostart_disable() -> bool:
53
+ """Remove tooltray from Linux autostart."""
54
+ from tool_tray.logging import log_info
55
+
56
+ desktop_file = Path.home() / ".config/autostart/tooltray.desktop"
57
+ if desktop_file.exists():
58
+ desktop_file.unlink()
59
+ log_info(f"Autostart disabled: {desktop_file}")
60
+ print(f"Autostart disabled: {desktop_file}")
61
+ return True
62
+ print("Autostart was not enabled")
63
+ return False
64
+
65
+
66
+ def _macos_autostart_enable() -> bool:
67
+ """Add tooltray to macOS autostart via LaunchAgent."""
68
+ import shutil
69
+
70
+ from tool_tray.logging import log_error, log_info
71
+
72
+ plist = Path.home() / "Library/LaunchAgents/com.tooltray.plist"
73
+ plist.parent.mkdir(parents=True, exist_ok=True)
74
+
75
+ # Use sys.executable to run with same Python interpreter
76
+ python_exe = sys.executable
77
+
78
+ # Find uv binary path for PATH env (needed for get_installed_version)
79
+ uv_bin = shutil.which("uv")
80
+ if not uv_bin:
81
+ log_error("Cannot enable autostart: uv not found in PATH")
82
+ print("Error: uv not found in PATH. Install uv first.")
83
+ return False
84
+ uv_dir = str(Path(uv_bin).parent)
85
+
86
+ log_dir = Path.home() / "Library/Logs/tooltray"
87
+ log_dir.mkdir(parents=True, exist_ok=True)
88
+ try:
89
+ plist.write_text(f"""<?xml version="1.0" encoding="UTF-8"?>
90
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
91
+ <plist version="1.0">
92
+ <dict>
93
+ <key>Label</key>
94
+ <string>com.tooltray</string>
95
+ <key>ProgramArguments</key>
96
+ <array>
97
+ <string>{python_exe}</string>
98
+ <string>-m</string>
99
+ <string>tool_tray</string>
100
+ </array>
101
+ <key>RunAtLoad</key>
102
+ <true/>
103
+ <key>KeepAlive</key>
104
+ <false/>
105
+ <key>EnvironmentVariables</key>
106
+ <dict>
107
+ <key>PATH</key>
108
+ <string>{uv_dir}:/usr/bin:/bin</string>
109
+ </dict>
110
+ <key>StandardOutPath</key>
111
+ <string>{log_dir}/stdout.log</string>
112
+ <key>StandardErrorPath</key>
113
+ <string>{log_dir}/stderr.log</string>
114
+ </dict>
115
+ </plist>""")
116
+ subprocess.run(["launchctl", "load", str(plist)], check=False)
117
+ log_info(f"Autostart enabled: {plist}")
118
+ print(f"Autostart enabled: {plist}")
119
+ return True
120
+ except OSError as e:
121
+ log_error(f"Failed to enable autostart: {plist}", e)
122
+ print(f"Failed to enable autostart: {e}")
123
+ return False
124
+
125
+
126
+ def _macos_autostart_disable() -> bool:
127
+ """Remove tooltray from macOS autostart."""
128
+ from tool_tray.logging import log_info
129
+
130
+ plist = Path.home() / "Library/LaunchAgents/com.tooltray.plist"
131
+ if plist.exists():
132
+ subprocess.run(["launchctl", "unload", str(plist)], check=False)
133
+ plist.unlink()
134
+ log_info(f"Autostart disabled: {plist}")
135
+ print(f"Autostart disabled: {plist}")
136
+ return True
137
+ print("Autostart was not enabled")
138
+ return False
139
+
140
+
141
+ def _windows_autostart_enable() -> bool:
142
+ """Add tooltray to Windows autostart via registry."""
143
+ from tool_tray.logging import log_error, log_info
144
+
145
+ try:
146
+ import winreg
147
+ except ImportError:
148
+ print("winreg not available (not Windows)")
149
+ return False
150
+
151
+ exe = _get_tooltray_path()
152
+ try:
153
+ key = winreg.OpenKey(
154
+ winreg.HKEY_CURRENT_USER,
155
+ r"Software\Microsoft\Windows\CurrentVersion\Run",
156
+ 0,
157
+ winreg.KEY_SET_VALUE,
158
+ )
159
+ winreg.SetValueEx(key, "ToolTray", 0, winreg.REG_SZ, exe)
160
+ winreg.CloseKey(key)
161
+ log_info("Autostart enabled via registry")
162
+ print("Autostart enabled via registry")
163
+ return True
164
+ except OSError as e:
165
+ log_error("Failed to enable autostart via registry", e)
166
+ print(f"Failed to enable autostart: {e}")
167
+ return False
168
+
169
+
170
+ def _windows_autostart_disable() -> bool:
171
+ """Remove tooltray from Windows autostart."""
172
+ from tool_tray.logging import log_info
173
+
174
+ try:
175
+ import winreg
176
+ except ImportError:
177
+ print("winreg not available (not Windows)")
178
+ return False
179
+
180
+ try:
181
+ key = winreg.OpenKey(
182
+ winreg.HKEY_CURRENT_USER,
183
+ r"Software\Microsoft\Windows\CurrentVersion\Run",
184
+ 0,
185
+ winreg.KEY_SET_VALUE,
186
+ )
187
+ winreg.DeleteValue(key, "ToolTray")
188
+ winreg.CloseKey(key)
189
+ log_info("Autostart disabled via registry")
190
+ print("Autostart disabled")
191
+ return True
192
+ except FileNotFoundError:
193
+ print("Autostart was not enabled")
194
+ return False
195
+ except OSError as e:
196
+ print(f"Failed to disable autostart: {e}")
197
+ return False
198
+
199
+
200
+ def enable_autostart() -> bool:
201
+ """Add tooltray to system autostart."""
202
+ from tool_tray.logging import log_debug
203
+
204
+ log_debug(f"Enabling autostart on {sys.platform}")
205
+ if sys.platform == "darwin":
206
+ return _macos_autostart_enable()
207
+ elif sys.platform == "win32":
208
+ return _windows_autostart_enable()
209
+ else:
210
+ return _linux_autostart_enable()
211
+
212
+
213
+ def disable_autostart() -> bool:
214
+ """Remove tooltray from system autostart."""
215
+ from tool_tray.logging import log_debug
216
+
217
+ log_debug(f"Disabling autostart on {sys.platform}")
218
+ if sys.platform == "darwin":
219
+ return _macos_autostart_disable()
220
+ elif sys.platform == "win32":
221
+ return _windows_autostart_disable()
222
+ else:
223
+ return _linux_autostart_disable()
224
+
225
+
226
+ def is_autostart_enabled() -> bool:
227
+ """Check if autostart is currently enabled."""
228
+ if sys.platform == "darwin":
229
+ plist = Path.home() / "Library/LaunchAgents/com.tooltray.plist"
230
+ return plist.exists()
231
+ elif sys.platform == "win32":
232
+ try:
233
+ import winreg
234
+
235
+ key = winreg.OpenKey(
236
+ winreg.HKEY_CURRENT_USER,
237
+ r"Software\Microsoft\Windows\CurrentVersion\Run",
238
+ 0,
239
+ winreg.KEY_READ,
240
+ )
241
+ winreg.QueryValueEx(key, "ToolTray")
242
+ winreg.CloseKey(key)
243
+ return True
244
+ except (ImportError, FileNotFoundError, OSError):
245
+ return False
246
+ else:
247
+ desktop_file = Path.home() / ".config/autostart/tooltray.desktop"
248
+ return desktop_file.exists()