Skip to content

Commit 4bffdbf

Browse files
committed
Add autostart support and broaden runtime theme support
1 parent 8e9c7a4 commit 4bffdbf

File tree

3 files changed

+212
-35
lines changed

3 files changed

+212
-35
lines changed

configure.py

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
MAIN_DIRECTORY = str(Path(__file__).parent.resolve()) + "/"
134134
THEMES_DIR = MAIN_DIRECTORY + 'res/themes'
135135
SMARTMONITOR_THEMES_DIR = MAIN_DIRECTORY + 'res/smartmonitor/themes'
136+
AUTOSTART_SERVICE_NAME = "turing-smart-screen-python.service"
136137
DEFAULT_SMARTMONITOR_VENDOR_THEME_ROOT = (
137138
os.path.join(MAIN_DIRECTORY, "vendor", "themefor3.5")
138139
if os.path.isdir(os.path.join(MAIN_DIRECTORY, "vendor", "themefor3.5"))
@@ -337,6 +338,81 @@ def sanitize_smartmonitor_theme_name(name: str) -> str:
337338
return "".join(safe).strip()
338339

339340

341+
def autostart_supported() -> bool:
342+
return platform.system() == "Linux"
343+
344+
345+
def autostart_service_dir() -> Path:
346+
return Path.home() / ".config" / "systemd" / "user"
347+
348+
349+
def autostart_service_path() -> Path:
350+
return autostart_service_dir() / AUTOSTART_SERVICE_NAME
351+
352+
353+
def render_autostart_service() -> str:
354+
python_exec = Path(sys.executable).resolve()
355+
main_script = Path(MAIN_DIRECTORY) / "main.py"
356+
working_dir = Path(MAIN_DIRECTORY).resolve()
357+
return "\n".join([
358+
"[Unit]",
359+
"Description=Turing Smart Screen Python HIDdev",
360+
"After=graphical-session.target network-online.target",
361+
"Wants=graphical-session.target network-online.target",
362+
"",
363+
"[Service]",
364+
"Type=simple",
365+
f"WorkingDirectory={working_dir}",
366+
f"ExecStart={python_exec} {main_script}",
367+
"Restart=on-failure",
368+
"RestartSec=3",
369+
"Environment=PYTHONUNBUFFERED=1",
370+
"",
371+
"[Install]",
372+
"WantedBy=default.target",
373+
"",
374+
])
375+
376+
377+
def write_autostart_service_file() -> Path:
378+
service_dir = autostart_service_dir()
379+
service_dir.mkdir(parents=True, exist_ok=True)
380+
service_path = autostart_service_path()
381+
service_path.write_text(render_autostart_service(), encoding="utf-8")
382+
return service_path
383+
384+
385+
def is_autostart_enabled() -> bool:
386+
if not autostart_supported():
387+
return False
388+
try:
389+
result = subprocess.run(
390+
["systemctl", "--user", "is-enabled", AUTOSTART_SERVICE_NAME],
391+
stdout=subprocess.PIPE,
392+
stderr=subprocess.PIPE,
393+
text=True,
394+
check=False,
395+
)
396+
return result.returncode == 0 and result.stdout.strip() == "enabled"
397+
except Exception:
398+
return autostart_service_path().is_file()
399+
400+
401+
def enable_autostart_service():
402+
service_path = write_autostart_service_file()
403+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
404+
subprocess.run(["systemctl", "--user", "enable", "--now", AUTOSTART_SERVICE_NAME], check=True)
405+
return service_path
406+
407+
408+
def disable_autostart_service():
409+
subprocess.run(["systemctl", "--user", "disable", "--now", AUTOSTART_SERVICE_NAME], check=False)
410+
service_path = autostart_service_path()
411+
if service_path.exists():
412+
service_path.unlink()
413+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=False)
414+
415+
340416
def smartmonitor_theme_dir(name: str) -> str:
341417
return os.path.join(SMARTMONITOR_THEMES_DIR, name)
342418

@@ -751,11 +827,25 @@ def __init__(self):
751827
)
752828
self.new_ui_btn.place(x=570, y=520, height=50, width=100)
753829

830+
self.autostart_enable_btn = ttk.Button(
831+
self.window,
832+
text="Enable\nautostart",
833+
command=lambda: self.on_enable_autostart_click(),
834+
)
835+
self.autostart_enable_btn.place(x=680, y=520, height=50, width=100)
836+
837+
self.autostart_disable_btn = ttk.Button(
838+
self.window,
839+
text="Disable\nautostart",
840+
command=lambda: self.on_disable_autostart_click(),
841+
)
842+
self.autostart_disable_btn.place(x=790, y=520, height=50, width=100)
843+
754844
self.save_btn = ttk.Button(self.window, text="Save settings", command=lambda: self.on_save_click())
755-
self.save_btn.place(x=680, y=520, height=50, width=110)
845+
self.save_btn.place(x=900, y=520, height=50, width=110)
756846

757847
self.save_run_btn = ttk.Button(self.window, text="Save and run", command=lambda: self.on_saverun_click())
758-
self.save_run_btn.place(x=800, y=520, height=50, width=110)
848+
self.save_run_btn.place(x=1020, y=520, height=50, width=110)
759849

760850
self.config = None
761851
self.load_config_values()
@@ -790,6 +880,7 @@ def refresh_theme_selector(self):
790880
self.theme_cb.set("")
791881
elif self.theme_cb.get() not in themes:
792882
self.theme_cb.set(themes[0])
883+
self.refresh_autostart_buttons()
793884

794885
def load_theme_preview(self):
795886
if self.is_smartmonitor_model():
@@ -1039,6 +1130,66 @@ def on_edit_ui_click(self):
10391130
def on_save_click(self):
10401131
self.save_config_values()
10411132

1133+
def refresh_autostart_buttons(self):
1134+
if not autostart_supported():
1135+
self.autostart_enable_btn.state(["disabled"])
1136+
self.autostart_disable_btn.state(["disabled"])
1137+
return
1138+
1139+
enabled = is_autostart_enabled()
1140+
if enabled:
1141+
self.autostart_enable_btn.state(["disabled"])
1142+
self.autostart_disable_btn.state(["!disabled"])
1143+
else:
1144+
self.autostart_enable_btn.state(["!disabled"])
1145+
self.autostart_disable_btn.state(["disabled"])
1146+
1147+
def on_enable_autostart_click(self):
1148+
if not autostart_supported():
1149+
messagebox.showinfo("Autostart", "Autostart via systemd --user is available only on Linux.", parent=self.window)
1150+
return
1151+
1152+
try:
1153+
service_path = enable_autostart_service()
1154+
except subprocess.CalledProcessError as exc:
1155+
messagebox.showerror(
1156+
"Autostart failed",
1157+
"Could not enable autostart via systemd --user.\n\n"
1158+
f"Service file:\n{autostart_service_path()}\n\n"
1159+
f"Error: {exc}",
1160+
parent=self.window,
1161+
)
1162+
return
1163+
except Exception as exc:
1164+
messagebox.showerror("Autostart failed", str(exc), parent=self.window)
1165+
return
1166+
1167+
self.refresh_autostart_buttons()
1168+
messagebox.showinfo(
1169+
"Autostart enabled",
1170+
"Autostart has been enabled for the current user.\n\n"
1171+
f"Service file:\n{service_path}",
1172+
parent=self.window,
1173+
)
1174+
1175+
def on_disable_autostart_click(self):
1176+
if not autostart_supported():
1177+
messagebox.showinfo("Autostart", "Autostart via systemd --user is available only on Linux.", parent=self.window)
1178+
return
1179+
1180+
try:
1181+
disable_autostart_service()
1182+
except Exception as exc:
1183+
messagebox.showerror("Autostart disable failed", str(exc), parent=self.window)
1184+
return
1185+
1186+
self.refresh_autostart_buttons()
1187+
messagebox.showinfo(
1188+
"Autostart disabled",
1189+
"Autostart has been disabled for the current user.",
1190+
parent=self.window,
1191+
)
1192+
10421193
def stop_running_main_instances(self):
10431194
main_scripts = {
10441195
os.path.abspath(os.path.join(MAIN_DIRECTORY, script_name))

library/smartmonitor_runtime.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,44 @@ def _theme_metadata(theme_stem: str) -> dict | None:
8484

8585

8686
@lru_cache(maxsize=32)
87-
def _theme_bundle(theme_stem: str):
87+
def _resolve_theme_source_ui_path(theme_stem: str) -> Path | None:
8888
metadata = _theme_metadata(theme_stem)
8989
if not isinstance(metadata, dict):
9090
return None
91+
9192
source_ui = metadata.get("source_ui")
92-
if not source_ui:
93-
return None
94-
source_ui_path = Path(source_ui)
95-
if not source_ui_path.is_file():
93+
if source_ui:
94+
source_ui_path = Path(source_ui).expanduser()
95+
if source_ui_path.is_file():
96+
return source_ui_path
97+
98+
vendor_theme = metadata.get("source_vendor_theme")
99+
candidate_dirs: list[Path] = []
100+
if vendor_theme:
101+
candidate_dirs.extend([
102+
Path(config.MAIN_DIRECTORY) / "vendor" / "themefor3.5" / str(vendor_theme),
103+
Path(config.MAIN_DIRECTORY) / "WIND" / "3.5 Inch SmartMonitor" / "themefor3.5" / str(vendor_theme),
104+
])
105+
106+
candidate_dirs.extend([
107+
Path(config.MAIN_DIRECTORY) / "res" / "smartmonitor" / "projects" / theme_stem,
108+
Path(config.MAIN_DIRECTORY) / "res" / "smartmonitor" / "themes" / theme_stem,
109+
])
110+
111+
for directory in candidate_dirs:
112+
if not directory.is_dir():
113+
continue
114+
ui_files = sorted(directory.glob("*.ui"))
115+
if ui_files:
116+
return ui_files[0]
117+
118+
return None
119+
120+
121+
@lru_cache(maxsize=32)
122+
def _theme_bundle(theme_stem: str):
123+
source_ui_path = _resolve_theme_source_ui_path(theme_stem)
124+
if source_ui_path is None:
96125
return None
97126
try:
98127
from library.smartmonitor_ui import parse_theme_bundle
@@ -197,11 +226,17 @@ def _active_theme_runtime_supported() -> bool:
197226

198227
if metadata.get("compiler") != "experimental_ui_to_imgdat":
199228
return True
200-
201-
theme_bundle = _theme_bundle(theme_stem)
202-
if theme_bundle is None:
229+
if not bool(_display_config().get("SMARTMONITOR_HID_ALLOW_EXPERIMENTAL_RUNTIME", True)):
203230
return False
204-
return bool(_display_config().get("SMARTMONITOR_HID_ALLOW_EXPERIMENTAL_RUNTIME", True))
231+
if _theme_bundle(theme_stem) is not None:
232+
return True
233+
if _tag_mapping():
234+
return True
235+
logger.warning(
236+
"SmartMonitor theme '%s' has no resolved source UI for runtime mapping; using best-effort runtime mode",
237+
_active_theme_name(),
238+
)
239+
return True
205240

206241

207242
def _post_upload_runtime_delay() -> float:
Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
11
[Unit]
2-
Description=Turing Smart Screen
3-
Requires=network-online.target
4-
After=network-online.target
2+
Description=Turing Smart Screen Python HIDdev
3+
After=graphical-session.target network-online.target
4+
Wants=graphical-session.target network-online.target
55

66
[Service]
77
Type=simple
88

9-
; Don't forget to change the value here!
10-
; There is no reason to run this program as root, just use your username
11-
User=YOUR_USERNAME_HERE
9+
; Recommended usage:
10+
; 1. Copy this file to ~/.config/systemd/user/turing-smart-screen-python.service
11+
; 2. Replace WorkingDirectory and ExecStart with your paths
12+
; 3. Run:
13+
; systemctl --user daemon-reload
14+
; systemctl --user enable --now turing-smart-screen-python.service
1215

13-
; If you want to put the program in your home directory instead, remove 'ProtectHome=read-only' line below
14-
WorkingDirectory=/opt/turing-smart-screen-python/
15-
16-
ExecStart=python3 main.py
17-
18-
; Always restart the script
19-
Restart=always
20-
21-
; cf. https://www.darkcoding.net/software/the-joy-of-systemd/
22-
; /usr, /boot and /etc are read-only
23-
ProtectSystem=full
24-
; $HOME is read only ..
25-
ProtectHome=read-only
26-
; /tmp is isolated from all other processes
27-
PrivateTmp=true
28-
; Don't allow process to raise privileges (e.g. disable suid)
29-
NoNewPrivileges=true
16+
WorkingDirectory=/absolute/path/to/turing-smart-screen-python-main
17+
ExecStart=/absolute/path/to/python /absolute/path/to/turing-smart-screen-python-main/main.py
18+
Restart=on-failure
19+
RestartSec=3
20+
Environment=PYTHONUNBUFFERED=1
3021

3122
[Install]
32-
WantedBy=multi-user.target
23+
WantedBy=default.target

0 commit comments

Comments
 (0)