synclayer 0.1.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.
synclayer/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from ._version import __version__
2
+ APP_NAME = "Synclayer"
3
+
4
+ _LAZY = ("Remote", "Watcher", "connect", "RemoteError", "ApiError", "SyncResult")
5
+ __all__ = ["__version__", "APP_NAME", *_LAZY]
6
+
7
+
8
+ def __getattr__(name): # keep `import synclayer` light; load the client library on first use
9
+ if name in _LAZY:
10
+ from . import remote
11
+
12
+ return getattr(remote, name)
13
+ raise AttributeError(f"module 'synclayer' has no attribute {name!r}")
synclayer/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
synclayer/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
synclayer/autostart.py ADDED
@@ -0,0 +1,175 @@
1
+ """Launch-on-boot / login for the tray app (target 'tray') or the server (target 'server').
2
+
3
+ Windows : tray -> HKCU Run key; server -> Task Scheduler ONSTART (needs an elevated shell)
4
+ Linux : tray -> ~/.config/autostart .desktop; server -> systemd *user* unit
5
+ (for a boot-time system service use scripts/install-service.sh)
6
+ macOS : LaunchAgent plist
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import plistlib
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ NAMES = {"tray": "Synclayer", "server": "SynclayerServer"}
17
+
18
+
19
+ def _python_exe(target: str) -> str:
20
+ """pythonw.exe for the tray on Windows so no console window flashes at login."""
21
+ exe = Path(sys.executable)
22
+ if target == "tray" and sys.platform.startswith("win"):
23
+ pyw = exe.with_name("pythonw.exe")
24
+ if pyw.is_file():
25
+ return str(pyw)
26
+ return str(exe)
27
+
28
+
29
+ def _command(target: str) -> list[str]:
30
+ base = [sys.executable] if getattr(sys, "frozen", False) else [_python_exe(target), "-m", "synclayer"]
31
+ return base + (["tray", "--minimized"] if target == "tray" else ["server", "serve"])
32
+
33
+
34
+ def _env_file() -> str | None:
35
+ f = os.environ.get("SL_ENV_FILE")
36
+ p = Path(f) if f else Path(".env")
37
+ return str(p.resolve()) if p.is_file() else None
38
+
39
+
40
+ # ---------------- Windows
41
+ def _win_enable(target):
42
+ cmd = subprocess.list2cmdline(_command(target))
43
+ if target == "tray":
44
+ import winreg
45
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run",
46
+ 0, winreg.KEY_SET_VALUE) as k:
47
+ winreg.SetValueEx(k, NAMES[target], 0, winreg.REG_SZ, cmd)
48
+ return "Tray app will start at login."
49
+ env = _env_file()
50
+ tr = f'cmd /c "set SL_ENV_FILE={env}&& {cmd}"' if env else cmd
51
+ r = subprocess.run(["schtasks", "/Create", "/TN", NAMES[target], "/TR", tr, "/SC", "ONSTART",
52
+ "/RU", "SYSTEM", "/RL", "HIGHEST", "/F"], capture_output=True, text=True)
53
+ if r.returncode:
54
+ raise SystemExit("Could not create scheduled task (run as Administrator): " + r.stderr.strip())
55
+ return "Server scheduled to start at boot."
56
+
57
+
58
+ def _win_disable(target):
59
+ if target == "tray":
60
+ import winreg
61
+ try:
62
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run",
63
+ 0, winreg.KEY_SET_VALUE) as k:
64
+ winreg.DeleteValue(k, NAMES[target])
65
+ except FileNotFoundError:
66
+ pass
67
+ else:
68
+ subprocess.run(["schtasks", "/Delete", "/TN", NAMES[target], "/F"], capture_output=True)
69
+ return "Disabled."
70
+
71
+
72
+ def _win_enabled(target):
73
+ if target == "tray":
74
+ import winreg
75
+ try:
76
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run") as k:
77
+ winreg.QueryValueEx(k, NAMES[target])
78
+ return True
79
+ except FileNotFoundError:
80
+ return False
81
+ return subprocess.run(["schtasks", "/Query", "/TN", NAMES[target]], capture_output=True).returncode == 0
82
+
83
+
84
+ # ---------------- Linux
85
+ def _desktop_path() -> Path:
86
+ return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "autostart" / "synclayer.desktop"
87
+
88
+
89
+ def _unit_path() -> Path:
90
+ return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "systemd" / "user" / "synclayer-server.service"
91
+
92
+
93
+ def _quote(cmd):
94
+ return " ".join('"' + c.replace('"', '\\"') + '"' if " " in c else c for c in cmd)
95
+
96
+
97
+ def _linux_enable(target):
98
+ if target == "tray":
99
+ p = _desktop_path()
100
+ p.parent.mkdir(parents=True, exist_ok=True)
101
+ p.write_text("[Desktop Entry]\nType=Application\nName=Synclayer\n"
102
+ f"Exec={_quote(_command('tray'))}\nX-GNOME-Autostart-enabled=true\nTerminal=false\n"
103
+ "Comment=Synclayer sync and file manager\n")
104
+ return f"Autostart entry written: {p}"
105
+ u = _unit_path()
106
+ u.parent.mkdir(parents=True, exist_ok=True)
107
+ env = _env_file()
108
+ u.write_text("[Unit]\nDescription=Synclayer server\nAfter=network-online.target\n\n[Service]\n"
109
+ f"ExecStart={_quote(_command('server'))}\nRestart=on-failure\n"
110
+ + (f"Environment=SL_ENV_FILE={env}\n" if env else "") +
111
+ "\n[Install]\nWantedBy=default.target\n")
112
+ subprocess.run(["systemctl", "--user", "daemon-reload"])
113
+ subprocess.run(["systemctl", "--user", "enable", "--now", "synclayer-server.service"])
114
+ return ("User service enabled. To start at BOOT without logging in run: "
115
+ "sudo loginctl enable-linger $USER (or use scripts/install-service.sh)")
116
+
117
+
118
+ def _linux_disable(target):
119
+ if target == "tray":
120
+ _desktop_path().unlink(missing_ok=True)
121
+ else:
122
+ subprocess.run(["systemctl", "--user", "disable", "--now", "synclayer-server.service"])
123
+ _unit_path().unlink(missing_ok=True)
124
+ return "Disabled."
125
+
126
+
127
+ def _linux_enabled(target):
128
+ return _desktop_path().exists() if target == "tray" else _unit_path().exists()
129
+
130
+
131
+ # ---------------- macOS
132
+ def _plist_path(target):
133
+ return Path.home() / "Library" / "LaunchAgents" / f"com.synclayer.{target}.plist"
134
+
135
+
136
+ def _mac_enable(target):
137
+ p = _plist_path(target)
138
+ p.parent.mkdir(parents=True, exist_ok=True)
139
+ data = {"Label": f"com.synclayer.{target}", "ProgramArguments": _command(target), "RunAtLoad": True,
140
+ "KeepAlive": target == "server"}
141
+ if _env_file():
142
+ data["EnvironmentVariables"] = {"SL_ENV_FILE": _env_file()}
143
+ logs = Path.home() / "Library" / "Logs" / "Synclayer"
144
+ logs.mkdir(parents=True, exist_ok=True)
145
+ data["StandardOutPath"] = str(logs / f"{target}.log")
146
+ data["StandardErrorPath"] = str(logs / f"{target}.err.log")
147
+ p.write_bytes(plistlib.dumps(data))
148
+ subprocess.run(["launchctl", "load", "-w", str(p)])
149
+ return f"LaunchAgent installed: {p} (logs: ~/Library/Logs/Synclayer)"
150
+
151
+
152
+ def _mac_disable(target):
153
+ p = _plist_path(target)
154
+ subprocess.run(["launchctl", "unload", "-w", str(p)], capture_output=True)
155
+ p.unlink(missing_ok=True)
156
+ return "Disabled."
157
+
158
+
159
+ # ---------------- public API
160
+ def enable(target: str = "tray") -> str:
161
+ if sys.platform.startswith("win"):
162
+ return _win_enable(target)
163
+ return _mac_enable(target) if sys.platform == "darwin" else _linux_enable(target)
164
+
165
+
166
+ def disable(target: str = "tray") -> str:
167
+ if sys.platform.startswith("win"):
168
+ return _win_disable(target)
169
+ return _mac_disable(target) if sys.platform == "darwin" else _linux_disable(target)
170
+
171
+
172
+ def is_enabled(target: str = "tray") -> bool:
173
+ if sys.platform.startswith("win"):
174
+ return _win_enabled(target)
175
+ return _plist_path(target).exists() if sys.platform == "darwin" else _linux_enabled(target)