-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
731 lines (632 loc) · 27.9 KB
/
main.py
File metadata and controls
731 lines (632 loc) · 27.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
"""
ShittyRandomPhotoScreenSaver - Main Entry Point
Windows screensaver application that displays photos with transitions.
"""
import sys
try:
import builtins as _builtins # type: ignore[attr-defined]
except Exception: # pragma: no cover - very early in startup
_builtins = None
import os
import gc
import shutil
import ctypes
import time
from pathlib import Path
from enum import Enum
from PySide6.QtWidgets import QApplication, QMessageBox
from PySide6.QtCore import Qt, QCoreApplication
from PySide6.QtGui import QSurfaceFormat, QImageReader, QIcon
from core.logging.logger import (
clear_logs_for_fresh_start,
setup_logging,
get_logger,
get_log_dir,
)
from core.settings.settings_manager import SettingsManager
from core.animation import AnimationManager
from engine.screensaver_engine import ScreensaverEngine
from ui.settings_dialog import SettingsDialog
from rendering.gl_format import build_surface_format
from ui.system_tray import ScreensaverTrayIcon
from versioning import APP_VERSION, APP_EXE_NAME
logger = get_logger(__name__)
# Windows timer resolution management for smoother animations.
# Default Windows timer resolution is ~15.6ms which causes timer coalescing
# and frame timing jitter. We request 1ms resolution for the duration of
# the screensaver to ensure smooth 60fps+ animations.
_winmm = None
_timer_resolution_set = False
def _set_windows_timer_resolution(resolution_ms: int = 1) -> bool:
"""Request higher timer resolution on Windows for smoother animations.
Args:
resolution_ms: Desired timer resolution in milliseconds (1-15)
Returns:
True if resolution was set successfully
"""
global _winmm, _timer_resolution_set
if sys.platform != 'win32':
return False
if _timer_resolution_set:
return True
try:
_winmm = ctypes.windll.winmm
result = _winmm.timeBeginPeriod(resolution_ms)
if result == 0: # TIMERR_NOERROR
_timer_resolution_set = True
return True
except Exception as e:
logger.debug("[MAIN] Exception suppressed: %s", e)
return False
def _restore_windows_timer_resolution(resolution_ms: int = 1) -> None:
"""Restore default Windows timer resolution."""
global _winmm, _timer_resolution_set
if not _timer_resolution_set or _winmm is None:
return
try:
_winmm.timeEndPeriod(resolution_ms)
_timer_resolution_set = False
except Exception as e:
logger.debug("[MAIN] Exception suppressed: %s", e)
class ScreensaverMode(Enum):
"""Screensaver execution modes based on Windows arguments."""
RUN = "run" # /s - Run screensaver
CONFIG = "config" # /c - Configuration dialog
PREVIEW = "preview" # /p <hwnd> - Preview in settings window
def _is_frozen_build() -> bool:
"""Return True when running from a compiled/frozen executable."""
if bool(getattr(sys, "frozen", False)):
return True
if globals().get("__compiled__", False):
return True
if _builtins is not None and bool(getattr(_builtins, "__compiled__", False)):
return True
main_mod = sys.modules.get("__main__")
if main_mod is not None and bool(getattr(main_mod, "__compiled__", False)):
return True
exe_path = Path(getattr(sys, "executable", "") or "")
exe_name = exe_path.name.lower()
if exe_name and exe_name not in ("python.exe", "pythonw.exe"):
if exe_name.startswith("srpss") or exe_name.endswith(".scr"):
return True
return False
def parse_screensaver_args() -> tuple[ScreensaverMode, int | None]:
"""
Parse Windows screensaver command-line arguments.
Windows screensaver arguments:
- /s - Run the screensaver
- /c - Show configuration dialog
- /p <hwnd> - Preview mode (show in window with handle <hwnd>)
Debug flags (ignored here, handled earlier):
- --debug, -d - Enable debug logging
- --verbose, -v - Enable full verbose log stream
- --perf - Enable performance logging
- --viz - Enable visualizer logging and diagnostics
- --geo - Enable geometry/z-order/edit-layout diagnostics
- --set - Enable settings mutation/import/schema diagnostics
- --life - Enable widget/worker/engine lifecycle diagnostics
- --cache - Enable image-cache/prefetch/cache-authority diagnostics
- --noupdates - Disable automatic Gmail/Reddit/Weather retrievals; manual refresh still works
- --viz-diagnostics (or --viz-diag) - Legacy alias for extra Spotify visualizer diagnostics
- -devblob - Enable dev-gated Blob visualizer mode
- --devcurve - Legacy no-op flag kept for compatibility
Returns:
tuple: (ScreensaverMode, preview_window_handle)
"""
# Filter out debug/viz/dev-gate flags
_filtered = {
"--debug", "-d", "--verbose", "-v", "--perf", "--viz", "--geo", "--set", "--life", "--cache",
"--noupdates",
"--viz-diagnostics", "--viz-diag",
"--fresh", "-devblob", "--devcurve",
}
args = [arg for arg in sys.argv if arg not in _filtered]
logger.debug(f"Command-line arguments: {sys.argv}")
logger.debug(f"Filtered arguments: {args}")
# Detect whether we are running as a frozen executable (.exe/.scr)
# or as a plain Python script.
is_frozen = _is_frozen_build()
# Default mode depends on environment:
# - Script runs (python main.py) default to RUN for convenience.
# - Frozen builds (SRPSS.exe/SRPSS.scr) default to CONFIG to avoid
# surprising full-screen runs when selected in the Windows dialog
# or double-clicked.
if len(args) == 1:
if is_frozen:
logger.info("No arguments provided in frozen build, defaulting to CONFIG mode")
return ScreensaverMode.CONFIG, None
logger.info("No arguments provided in script mode, defaulting to RUN mode")
return ScreensaverMode.RUN, None
# Get the first argument (after program name)
raw_arg = args[1]
arg = raw_arg.lower().strip()
# Run screensaver (Windows /s only). For convenience, -s/--s open settings.
if arg == '/s':
logger.info("RUN mode selected")
return ScreensaverMode.RUN, None
# Configuration dialog. Windows may pass "/c" or "/c:####" (with a
# parent window handle); treat any "/c*" pattern as CONFIG mode so the
# Screen Saver Settings "Settings" button never accidentally runs the
# saver full-screen.
elif arg.startswith('/c') or arg in ('-c', '-s', '--s'):
logger.info("CONFIG mode selected")
return ScreensaverMode.CONFIG, None
# Preview mode
elif arg == '/p' or arg == '-p':
if len(args) > 2:
try:
hwnd = int(args[2])
logger.info(f"PREVIEW mode selected with window handle: {hwnd}")
return ScreensaverMode.PREVIEW, hwnd
except ValueError:
logger.error(f"Invalid window handle: {args[2]}")
return ScreensaverMode.PREVIEW, None
else:
logger.warning("PREVIEW mode selected but no window handle provided")
return ScreensaverMode.PREVIEW, None
# Unknown argument – default mode depends on environment so we never
# "surprise run" a frozen build while keeping script usage simple.
else:
if is_frozen:
logger.warning(f"Unknown argument: {arg}, defaulting to CONFIG mode (frozen)")
return ScreensaverMode.CONFIG, None
logger.warning(f"Unknown argument: {arg}, defaulting to RUN mode (script)")
return ScreensaverMode.RUN, None
def is_script_mode() -> bool:
"""
Check if running as a script (not compiled executable).
Returns:
True if running as .py script, False if compiled .exe/.scr
"""
# PyInstaller and similar bundlers set sys.frozen on the runtime
# executable; treat any such environment as non-script.
if _is_frozen_build():
return False
# Check if running from a .py file or if __file__ exists
return hasattr(sys, 'ps1') or (
hasattr(sys.modules['__main__'], '__file__') and
sys.modules['__main__'].__file__.endswith('.py')
)
def cleanup_pycache(root_path: Path) -> int:
"""
Recursively delete all __pycache__ directories.
Args:
root_path: Root directory to start cleanup from
Returns:
Number of directories removed
"""
removed_count = 0
try:
for dirpath, dirnames, _ in os.walk(root_path):
# Look for __pycache__ directories
if '__pycache__' in dirnames:
pycache_path = Path(dirpath) / '__pycache__'
try:
shutil.rmtree(pycache_path)
removed_count += 1
except Exception as e:
logger.warning(f"Failed to remove pycache {pycache_path}: {e}")
except Exception as e:
logger.warning(f"Error during pycache cleanup: {e}")
return removed_count
def _schedule_runtime_reddit_helper_session(engine) -> bool:
"""Keep a saver-session ticket fresh and request task-owned helper launch.
The saver does not spawn the helper directly anymore. It only refreshes a
benign ProgramData ticket and asks Windows Task Scheduler to start the
already-registered interactive helper task when needed.
"""
try:
from core.mc import is_mc_build
from core.windows.reddit_helper_installer import _log_helper_event
from core.windows import reddit_helper_runtime
script_mode = bool(is_script_mode())
mc_mode = bool(is_mc_build())
if script_mode or mc_mode:
_log_helper_event(
"session helper skipped "
f"script={int(script_mode)} mc={int(mc_mode)} "
f"argv0={Path(str(getattr(sys, 'argv', [''])[0] or '')).name} "
f"exe={Path(str(getattr(sys, 'executable', '') or '')).name}"
)
return False
except Exception:
logger.debug("[REDDIT-HELPER] Failed to resolve session-helper environment", exc_info=True)
return False
timer = getattr(engine, "_reddit_helper_session_timer", None)
if timer is not None:
try:
timer.stop()
timer.deleteLater()
except Exception:
logger.debug("[REDDIT-HELPER] Failed to reset previous session timer", exc_info=True)
thread_manager = getattr(engine, "thread_manager", None)
if thread_manager is None:
_log_helper_event("session helper skipped no-thread-manager")
logger.info("[REDDIT-HELPER] Session helper skipped because ThreadManager is unavailable")
return False
if not reddit_helper_runtime.refresh_session_ticket(source="run_session_start"):
_log_helper_event("session helper ticket-refresh-failed source=run_session_start")
launched = reddit_helper_runtime.ensure_helper_runtime(
source="run_session_start",
persistent=False,
allow_system=True,
)
_log_helper_event(
"session helper start "
f"launched={int(bool(launched))}"
)
interval_ms = int(max(1000, reddit_helper_runtime.SESSION_TICKET_REFRESH_SECONDS * 1000.0))
def _session_tick() -> None:
try:
if not bool(getattr(engine, "_running", False)):
reddit_helper_runtime.clear_session_ticket(source="run_session_stop")
timer_ref = getattr(engine, "_reddit_helper_session_timer", None)
if timer_ref is not None:
try:
timer_ref.stop()
timer_ref.deleteLater()
except Exception:
logger.debug("[REDDIT-HELPER] Failed to stop session timer", exc_info=True)
finally:
engine._reddit_helper_session_timer = None
_log_helper_event("session helper stopped engine-not-running")
return
reddit_helper_runtime.refresh_session_ticket(source="run_session_keepalive")
if not reddit_helper_runtime.is_helper_healthy():
relaunched = reddit_helper_runtime.ensure_helper_runtime(
source="run_session_keepalive",
persistent=False,
allow_system=True,
)
_log_helper_event(f"session helper keepalive launch={int(bool(relaunched))}")
except Exception as exc:
try:
from core.windows.reddit_helper_installer import _log_helper_event as _fallback_log_helper_event
_fallback_log_helper_event(f"session helper callback exception: {exc!r}")
except Exception:
pass
logger.debug("[REDDIT-HELPER] Session helper keepalive failed", exc_info=True)
timer = thread_manager.schedule_recurring(
interval_ms,
_session_tick,
description="Reddit helper session keepalive",
)
engine._reddit_helper_session_timer = timer
logger.info("[REDDIT-HELPER] Scheduled session ticket keepalive every %sms", interval_ms)
return True
def run_screensaver(app: QApplication) -> int:
"""
Run the screensaver.
Args:
app: Qt application instance
Returns:
Exit code
"""
logger.info("Initializing screensaver engine")
# Create settings manager
settings = SettingsManager()
# Determine whether Interaction Mode is enabled so we can optionally
# expose a small system tray for Settings/Exit while the saver runs.
interaction_mode_enabled = False
try:
raw_interaction_mode = settings.get('input.interaction_mode', False)
if hasattr(SettingsManager, "to_bool"):
interaction_mode_enabled = SettingsManager.to_bool(raw_interaction_mode, False)
else:
interaction_mode_enabled = bool(raw_interaction_mode)
except Exception as e:
logger.debug("[MAIN] Exception suppressed: %s", e)
interaction_mode_enabled = False
# Check if sources are configured (using dot notation)
folders = settings.get('sources.folders', [])
rss_feeds = settings.get('sources.rss_feeds', [])
if not folders and not rss_feeds:
logger.warning("No image sources configured - opening settings dialog")
msg = QMessageBox(
QMessageBox.Icon.Information,
"No Sources Configured",
"No image sources have been configured.\n\n"
"Please add folders or RSS feeds in the settings dialog.\n\n"
"This dialog will close automatically in 10 seconds.",
)
msg.setWindowFlags(msg.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
msg.raise_()
msg.activateWindow()
# Auto-close after 10 seconds — uses QTimer.singleShot (static, no
# compositor active at this point so no performance concern).
from PySide6.QtCore import QTimer
QTimer.singleShot(10_000, msg.accept)
msg.exec()
return run_config(app)
# Create and start screensaver engine
try:
engine = ScreensaverEngine()
if not engine.initialize():
logger.error("Failed to initialize screensaver engine")
logger.warning("Opening settings dialog to configure sources")
msg2 = QMessageBox(
QMessageBox.Icon.Warning,
"Configuration Required",
"Failed to initialize screensaver.\n\n"
"Please configure image sources in the settings dialog.\n\n"
"This dialog will close automatically in 10 seconds.",
)
msg2.setWindowFlags(msg2.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
msg2.raise_()
msg2.activateWindow()
from PySide6.QtCore import QTimer
QTimer.singleShot(10_000, msg2.accept)
msg2.exec()
return run_config(app)
if not engine.start():
logger.error("Failed to start screensaver engine")
logger.warning("Opening settings dialog")
msg3 = QMessageBox(
QMessageBox.Icon.Warning,
"Startup Failed",
"Failed to start screensaver.\n\n"
"Please check your configuration.\n\n"
"This dialog will close automatically in 10 seconds.",
)
msg3.setWindowFlags(msg3.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
msg3.raise_()
msg3.activateWindow()
from PySide6.QtCore import QTimer
QTimer.singleShot(10_000, msg3.accept)
msg3.exec()
return run_config(app)
# Optional system tray presence in Interaction Mode.
tray_icon = None
if interaction_mode_enabled:
try:
tray_icon = ScreensaverTrayIcon(app, app.windowIcon())
except Exception:
logger.debug("Failed to create system tray icon", exc_info=True)
if tray_icon is not None:
# Delegate to the engine's existing S-key workflow so tray
# Settings behaves identically to pressing S.
def _on_tray_settings() -> None:
try:
# _on_settings_requested performs a full stop →
# settings dialog → restart cycle.
engine._on_settings_requested() # type: ignore[attr-defined]
except Exception:
logger.exception("Failed to open settings from system tray")
def _on_tray_exit() -> None:
try:
engine.stop()
except Exception:
logger.exception("Failed to stop engine from system tray")
app.quit()
tray_icon.settings_requested.connect(_on_tray_settings)
tray_icon.exit_requested.connect(_on_tray_exit)
logger.info("Screensaver engine started - entering event loop")
_schedule_runtime_reddit_helper_session(engine)
return app.exec()
except Exception as e:
logger.exception(f"Failed to start screensaver engine: {e}")
QMessageBox.critical(
None,
"Screensaver Error",
f"Failed to start screensaver:\n{e}"
)
return 1
def run_config(app: QApplication) -> int:
"""
Run configuration dialog.
Args:
app: Qt application instance
Returns:
Exit code
"""
logger.info("Opening configuration dialog")
# Create settings manager
settings = SettingsManager()
# Create animation manager
animations = AnimationManager()
# Create and show settings dialog
try:
dialog = SettingsDialog(settings, animations)
dialog.show()
logger.info("Configuration dialog opened - entering event loop")
return app.exec()
except Exception as e:
logger.exception(f"Failed to open configuration dialog: {e}")
QMessageBox.critical(
None,
"Configuration Error",
f"Failed to open settings:\n{e}"
)
return 1
def main():
"""Main entry point for the screensaver application."""
fresh_mode = '--fresh' in sys.argv
fresh_result: tuple[Path, int] | None = None
if fresh_mode:
fresh_result = clear_logs_for_fresh_start()
# Setup logging first
debug_mode = '--debug' in sys.argv or '-d' in sys.argv
verbose_mode = '--verbose' in sys.argv or '-v' in sys.argv
perf_mode = '--perf' in sys.argv
viz_mode = '--viz' in sys.argv
geo_mode = '--geo' in sys.argv
settings_trace_mode = '--set' in sys.argv
lifecycle_mode = '--life' in sys.argv
cache_trace_mode = '--cache' in sys.argv
viz_diag_mode = viz_mode or '--viz-diagnostics' in sys.argv or '--viz-diag' in sys.argv
setup_logging(
debug=debug_mode,
verbose=verbose_mode,
perf=perf_mode,
viz=viz_mode,
viz_diag=viz_diag_mode,
geo=geo_mode,
settings_trace=settings_trace_mode,
lifecycle=lifecycle_mode,
cache_trace=cache_trace_mode,
)
if fresh_result is not None:
fresh_log_dir, fresh_deleted = fresh_result
logger.info(
"[FRESH] Cleared %s log files from %s before startup",
fresh_deleted,
fresh_log_dir,
)
# GC tracking for performance debugging
if perf_mode:
_gc_start_time = [0.0]
def _gc_callback(phase: str, info: dict) -> None:
if phase == 'start':
_gc_start_time[0] = time.time()
elif phase == 'stop':
elapsed_ms = (time.time() - _gc_start_time[0]) * 1000.0
if elapsed_ms > 10.0:
logger.warning("[PERF] [GC] Collection took %.2fms (gen=%s, collected=%s)",
elapsed_ms, info.get('generation', '?'), info.get('collected', '?'))
gc.callbacks.append(_gc_callback)
logger.info("[PERF] GC tracking enabled")
logger.info("=" * 60)
logger.info("ShittyRandomPhotoScreenSaver Starting")
logger.info("=" * 60)
# Startup should not pay the recursive pycache-cleanup cost in script mode.
# Exit cleanup is still retained for local developer hygiene.
# Parse command-line arguments
mode, preview_hwnd = parse_screensaver_args()
# Enable High DPI scaling BEFORE creating QApplication
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
# Configure OpenGL globally BEFORE creating QApplication
try:
# Prefer desktop OpenGL and share contexts across widgets
QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_UseDesktopOpenGL, True)
QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_ShareOpenGLContexts, True)
fmt, prefs = build_surface_format(reason="startup")
QSurfaceFormat.setDefaultFormat(fmt)
logger.info(
"Global QSurfaceFormat configured (swap=%s, interval=%s, depth=%s, stencil=%s)",
fmt.swapBehavior(),
fmt.swapInterval(),
fmt.depthBufferSize(),
fmt.stencilBufferSize(),
)
except Exception as e:
logger.warning(f"Failed to configure global OpenGL format: {e}")
# Create Qt application
app = QApplication(sys.argv)
app.setApplicationName(APP_EXE_NAME)
app.setOrganizationName("ShittyRandomPhotoScreenSaver")
try:
app.setApplicationVersion(APP_VERSION)
except Exception:
logger.debug("[MAIN] Failed to set application version")
# Apply application icon from SRPSS.ico when available so the
# taskbar/systray and dialogs share a consistent identity.
icon_path = Path(__file__).with_name("SRPSS.ico")
if icon_path.exists():
try:
app.setWindowIcon(QIcon(str(icon_path)))
except Exception:
logger.debug("Failed to set application icon from SRPSS.ico", exc_info=True)
# Increase Qt image allocation limit from 256MB to 1GB for high-res images
# This is per-image when loaded, not total memory for all images
# Images are loaded on-demand, not all at startup (ImageQueue stores metadata only)
QImageReader.setAllocationLimit(1024) # 1GB in MB
logger.info("Qt image allocation limit: 1GB (supports 8K+ images, per-image on-demand)")
logger.info("Qt Application created: %s", app.applicationName())
logger.debug("High DPI scaling enabled")
# Register bundled custom fonts before any widgets are created
from ui.tabs.shared_styles import ensure_custom_fonts
ensure_custom_fonts()
logger.debug("Custom fonts registered")
# Route to appropriate mode
exit_code = 0
# Set Windows timer resolution for smoother animations (RUN mode only)
timer_res_set = False
try:
if mode == ScreensaverMode.RUN:
logger.info("Starting screensaver in RUN mode")
# Request 1ms timer resolution for smooth 60fps+ animations
timer_res_set = _set_windows_timer_resolution(1)
if timer_res_set:
logger.info("Windows timer resolution set to 1ms for smooth animations")
else:
logger.debug("Could not set Windows timer resolution (non-Windows or failed)")
profile_flag = os.getenv("SRPSS_PROFILE_CPU", "").strip().lower()
if profile_flag in ("1", "true", "on", "yes"):
import cProfile
profiler = cProfile.Profile()
profiler.enable()
exit_code = run_screensaver(app)
profiler.disable()
try:
profile_path = get_log_dir() / "screensaver_run.pstats"
profiler.dump_stats(str(profile_path))
logger.info("[PERF] [CPU] cProfile stats written to %s", profile_path)
except Exception:
logger.debug("[PERF] [CPU] Failed to write cProfile stats", exc_info=True)
else:
exit_code = run_screensaver(app)
elif mode == ScreensaverMode.CONFIG:
logger.info("Starting configuration dialog")
profile_flag = os.getenv("SRPSS_PROFILE_CPU", "").strip().lower()
if profile_flag in ("1", "true", "on", "yes"):
import cProfile
profiler = cProfile.Profile()
profiler.enable()
exit_code = run_config(app)
profiler.disable()
try:
profile_path = get_log_dir() / "screensaver_config.pstats"
profiler.dump_stats(str(profile_path))
logger.info("[PERF] [CPU] cProfile stats written to %s", profile_path)
except Exception:
logger.debug("[PERF] [CPU] Failed to write cProfile stats", exc_info=True)
else:
exit_code = run_config(app)
elif mode == ScreensaverMode.PREVIEW:
logger.info(f"Starting preview mode (hwnd={preview_hwnd})")
# FEATURE BACKLOG: Preview mode shows thumbnail in Windows Screen Saver dialog.
# Currently not implemented - would embed into host window via hwnd.
# No window shown to avoid surprising users in dialog preview.
logger.warning("PREVIEW mode not yet implemented (no window shown)")
except Exception as e:
logger.exception(f"Fatal error in main: {e}")
exit_code = 1
finally:
# Restore Windows timer resolution if we changed it
if timer_res_set:
_restore_windows_timer_resolution(1)
logger.debug("Windows timer resolution restored to default")
# Cleanup pycache on exit (script mode only)
if is_script_mode():
logger.info("Cleaning pycache on exit")
project_root = Path(__file__).parent
removed = cleanup_pycache(project_root)
if removed > 0:
logger.info(f"Removed {removed} __pycache__ directories")
logger.info("=" * 60)
logger.info(f"ShittyRandomPhotoScreenSaver Exiting (code={exit_code})")
logger.info("=" * 60)
# When PERF metrics are enabled for this run, automatically invoke the
# PERF helper to summarise recent Spotify visualiser and Slide metrics
# from the dedicated screensaver_perf.log. This is a best-effort helper
# and failures are logged at DEBUG only so normal runs are unaffected.
try:
if perf_mode:
try:
from scripts import spotify_vis_metrics_parser as _sv # type: ignore[import]
_sv.main()
except Exception:
logger.debug(
"[PERF] spotify_vis_metrics_parser auto-run failed",
exc_info=True,
)
except Exception:
logger.debug(
"[PERF] spotify_vis_metrics_parser auto-run guard failed",
exc_info=True,
)
return exit_code
if __name__ == "__main__":
sys.exit(main())