Skip to content

Commit 437ebd9

Browse files
committed
Add tray double-click reload and sleep-wake frequency reconciliation
1 parent 2387a06 commit 437ebd9

3 files changed

Lines changed: 74 additions & 2 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,5 @@ cython_debug/
163163

164164
# Project specific ignores
165165
log.txt
166-
.claude
166+
.claude
167+
.pipeline/

main.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
# constants
2323
TIME_STEP = 5 # seconds
2424
CONFIG_RELOAD_EVERY = 6 # iterations -> ~30 s
25+
RECONCILE_EVERY = 12 # iterations -> ~60 s, rarer than TIME_STEP (D-4, D-7)
2526

2627
PROJECT_NAME = "SRR"
2728
PROJECT_DISPLAY_NAME = "Smart Refresh Rate"
@@ -250,6 +251,7 @@ async def srr_loop() -> None:
250251
if _tray is not None:
251252
_tray.set_state_text(_state_label(last_state))
252253
counter = 0
254+
reconcile_counter = 0
253255

254256
loop = asyncio.get_running_loop()
255257
managed_display_id: Optional[str] = config_last_target
@@ -337,6 +339,43 @@ async def _do_switch(state: bool) -> None:
337339
await _do_switch(current_state)
338340
counter += 1
339341

342+
# Sleep-wake reconciliation: at a rarer interval than TIME_STEP
343+
# (RECONCILE_EVERY ticks ~60s) compare live current display settings
344+
# (ENUM_CURRENT_SETTINGS, not registry) against desired for the
345+
# current power state; if mismatched (e.g. after resume without
346+
# power toggle) re-apply via _do_switch. No registry write.
347+
if reconcile_counter >= RECONCILE_EVERY:
348+
reconcile_counter = 0
349+
if (
350+
current_config is not None
351+
and current_state is not None
352+
and (_tray is None or not _tray.paused)
353+
):
354+
try:
355+
targets = _target_modes(current_state)
356+
needs_reconcile = False
357+
for mid, desired in targets.items():
358+
adapter = display_map.get(mid)
359+
if adapter is None:
360+
continue
361+
try:
362+
w, h, freq = reschanger.get_display_settings(
363+
adapter, reschanger.ENUM_CURRENT_SETTINGS
364+
)
365+
except RuntimeError:
366+
continue
367+
if (w, h, freq) != tuple(desired):
368+
needs_reconcile = True
369+
break
370+
if needs_reconcile:
371+
logging.info(
372+
"reconcile: live settings diverged from desired, re-applying"
373+
)
374+
await _do_switch(current_state)
375+
except Exception as e:
376+
logging.warning(f"reconcile check failed: {e}")
377+
reconcile_counter += 1
378+
340379
if current_state != last_state and current_config is not None:
341380
if current_state is not None:
342381
await _do_switch(current_state)

tray.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import os
55
import threading
6+
import time
67
from pathlib import Path
78
from typing import TYPE_CHECKING, Callable, Optional
89

@@ -66,6 +67,14 @@ def __init__(
6667
self._icon: Optional[_Icon] = None
6768
self._thread: Optional[threading.Thread] = None
6869

70+
# Double-click detection: pystray Win32 backend has no native
71+
# double-click (window class style 0 lacks CS_DBLCLKS, _on_notify
72+
# only handles WM_LBUTTONUP / WM_RBUTTONUP). Use timing-based
73+
# detection by wrapping Icon.__call__ (invoked on WM_LBUTTONUP)
74+
# and measuring interval between activations.
75+
self._last_click_ts: float = 0.0
76+
self._DOUBLE_CLICK_INTERVAL: float = 0.4 # seconds (<400 ms)
77+
6978
# --- menu actions -------------------------------------------------
7079

7180
def _toggle_pause(self, icon, item):
@@ -196,7 +205,30 @@ def _build_menu(self) -> pystray.Menu:
196205
return pystray.Menu(*items)
197206

198207
def start(self):
199-
icon = pystray.Icon(
208+
# Double-click handling: pystray Win32 backend has no native
209+
# double-click support (window class style=0 lacks CS_DBLCLKS and
210+
# _on_notify only dispatches WM_LBUTTONUP/WM_RBUTTONUP). Implement
211+
# timing-based detection by intercepting Icon.__call__ (invoked on
212+
# WM_LBUTTONUP) — two activations within <400 ms are treated as a
213+
# double-click that reloads config; single click delegates to the
214+
# original Icon.__call__ to preserve default menu behavior.
215+
controller = self
216+
217+
class _SRRIcon(pystray.Icon): # type: ignore[misc]
218+
def __call__(self): # type: ignore[override]
219+
now = time.monotonic()
220+
if now - controller._last_click_ts < controller._DOUBLE_CLICK_INTERVAL:
221+
logging.info("tray: double-click detected — reloading config")
222+
controller._last_click_ts = 0.0
223+
try:
224+
controller._on_reload()
225+
except Exception as e:
226+
logging.warning(f"tray double-click reload failed: {e}")
227+
return
228+
controller._last_click_ts = now
229+
return super().__call__()
230+
231+
icon = _SRRIcon(
200232
self.project_name,
201233
self._icon_image,
202234
f"SRR — {self.state_text}",

0 commit comments

Comments
 (0)