refactor: extract reusable utils, remove screencapture facade
- Add ManagedTimer to replace repeated source_remove/timeout_add boilerplate - Add EvdevLockService base class for numlock/capslock (cut 154→28 lines) - Add BaseNotchIndicator for 5 near-identical indicator classes (cut 404→171 lines) - Add load_pixbuf_thumbnail and resolve_monitor to utils - Delete screencapture.py facade; callers use Screenshot/ScreenRecorder directly
This commit is contained in:
16 files changed
+401
-612
No files matched your search
+12
-72
@@ -1,77 +1,17 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fabric.core.service import Property, Service, Signal
|
||||
from fabric.utils import logger
|
||||
|
||||
from utils.gtk_utils import EvdevLEDMonitor
|
||||
|
||||
# Discover CapsLock LED device
|
||||
capslock_leds = list(Path("/sys/class/leds").glob("input*::capslock"))
|
||||
capslock_device = capslock_leds[0] if capslock_leds else None
|
||||
from utils.evdev_lock import EvdevLockService
|
||||
|
||||
|
||||
class CapsLock(Service):
|
||||
instance = None
|
||||
class CapsLock(EvdevLockService):
|
||||
"""Service for monitoring CapsLock LED state via evdev."""
|
||||
|
||||
_LED_GLOB = "input*::capslock"
|
||||
_LED_INDEX = 1 # LED_CAPSL
|
||||
_SERVICE_NAME = "CapsLock"
|
||||
|
||||
_instance = None
|
||||
|
||||
@staticmethod
|
||||
def get_initial():
|
||||
if CapsLock.instance is None:
|
||||
CapsLock.instance = CapsLock()
|
||||
|
||||
return CapsLock.instance
|
||||
|
||||
@Signal
|
||||
def state_changed(self, is_on: bool) -> None:
|
||||
"""Signal emitted when CapsLock state changes."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.capslock_led_path = capslock_device
|
||||
self._last_state = None
|
||||
self._monitoring_started = False
|
||||
|
||||
if capslock_device is None:
|
||||
logger.warning("CapsLock device not found, CapsLock service disabled")
|
||||
return
|
||||
|
||||
# LED_CAPSL = 1
|
||||
self._monitor = EvdevLEDMonitor(
|
||||
self.capslock_led_path, 1, self._on_state_changed
|
||||
)
|
||||
|
||||
def _on_state_changed(self, is_on: bool):
|
||||
if is_on != self._last_state:
|
||||
self._last_state = is_on
|
||||
self.emit("state_changed", is_on)
|
||||
|
||||
def stop(self):
|
||||
if hasattr(self, "_monitor"):
|
||||
self._monitor.stop()
|
||||
|
||||
def _ensure_monitoring_started(self):
|
||||
if not self._monitoring_started and self.capslock_led_path:
|
||||
self._monitoring_started = True
|
||||
# Set initial state before starting monitoring
|
||||
self._last_state = self._get_current_state()
|
||||
self._monitor._last_state = self._last_state
|
||||
self._monitor.start()
|
||||
|
||||
def _get_current_state(self) -> bool:
|
||||
if not self.capslock_led_path:
|
||||
return False
|
||||
brightness_path = self.capslock_led_path / "brightness"
|
||||
if brightness_path.exists():
|
||||
try:
|
||||
return bool(int(brightness_path.read_text().strip()))
|
||||
except (ValueError, OSError) as e:
|
||||
logger.warning(
|
||||
f"[capslock] return bool(int(brightness_path.read_text().strip())) failed: {e}"
|
||||
)
|
||||
return False
|
||||
return False
|
||||
|
||||
@Property(bool, "read-write", default_value=False)
|
||||
def is_on(self) -> bool:
|
||||
self._ensure_monitoring_started()
|
||||
return self._get_current_state()
|
||||
if CapsLock._instance is None:
|
||||
CapsLock._instance = CapsLock()
|
||||
return CapsLock._instance
|
||||
@@ -37,12 +37,6 @@ def __getattr__(name):
|
||||
_inst = None
|
||||
setattr(mod, name, _inst)
|
||||
return _inst
|
||||
if name == "screen_capture_service":
|
||||
from services.screencapture import ScreenCapture
|
||||
|
||||
_inst = ScreenCapture()
|
||||
setattr(mod, name, _inst)
|
||||
return _inst
|
||||
if name == "screenshot_service":
|
||||
from services.screenshot import Screenshot
|
||||
|
||||
|
||||
+12
-72
@@ -1,77 +1,17 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fabric.core.service import Property, Service, Signal
|
||||
from fabric.utils import logger
|
||||
|
||||
from utils.gtk_utils import EvdevLEDMonitor
|
||||
|
||||
# Discover NumLock LED device
|
||||
numlock_leds = list(Path("/sys/class/leds").glob("input*::numlock"))
|
||||
numlock_device = numlock_leds[0] if numlock_leds else None
|
||||
from utils.evdev_lock import EvdevLockService
|
||||
|
||||
|
||||
class NumLock(Service):
|
||||
instance = None
|
||||
class NumLock(EvdevLockService):
|
||||
"""Service for monitoring NumLock LED state via evdev."""
|
||||
|
||||
_LED_GLOB = "input*::numlock"
|
||||
_LED_INDEX = 0 # LED_NUML
|
||||
_SERVICE_NAME = "NumLock"
|
||||
|
||||
_instance = None
|
||||
|
||||
@staticmethod
|
||||
def get_initial():
|
||||
if NumLock.instance is None:
|
||||
NumLock.instance = NumLock()
|
||||
|
||||
return NumLock.instance
|
||||
|
||||
@Signal
|
||||
def state_changed(self, is_on: bool) -> None:
|
||||
"""Signal emitted when NumLock state changes."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.numlock_led_path = numlock_device
|
||||
self._last_state = None
|
||||
self._monitoring_started = False
|
||||
|
||||
if numlock_device is None:
|
||||
logger.warning("NumLock device not found, NumLock service disabled")
|
||||
return
|
||||
|
||||
# LED_NUML = 0
|
||||
self._monitor = EvdevLEDMonitor(
|
||||
self.numlock_led_path, 0, self._on_state_changed
|
||||
)
|
||||
|
||||
def _on_state_changed(self, is_on: bool):
|
||||
if is_on != self._last_state:
|
||||
self._last_state = is_on
|
||||
self.emit("state_changed", is_on)
|
||||
|
||||
def stop(self):
|
||||
if hasattr(self, "_monitor"):
|
||||
self._monitor.stop()
|
||||
|
||||
def _ensure_monitoring_started(self):
|
||||
if not self._monitoring_started and self.numlock_led_path:
|
||||
self._monitoring_started = True
|
||||
# Set initial state before starting monitoring
|
||||
self._last_state = self._get_current_state()
|
||||
self._monitor._last_state = self._last_state
|
||||
self._monitor.start()
|
||||
|
||||
def _get_current_state(self) -> bool:
|
||||
if not self.numlock_led_path:
|
||||
return False
|
||||
brightness_path = self.numlock_led_path / "brightness"
|
||||
if brightness_path.exists():
|
||||
try:
|
||||
return bool(int(brightness_path.read_text().strip()))
|
||||
except (ValueError, OSError) as e:
|
||||
logger.warning(
|
||||
f"[numlock] return bool(int(brightness_path.read_text().strip())) failed: {e}"
|
||||
)
|
||||
return False
|
||||
return False
|
||||
|
||||
@Property(bool, "read-write", default_value=False)
|
||||
def is_on(self) -> bool:
|
||||
self._ensure_monitoring_started()
|
||||
return self._get_current_state()
|
||||
if NumLock._instance is None:
|
||||
NumLock._instance = NumLock()
|
||||
return NumLock._instance
|
||||
@@ -1,116 +0,0 @@
|
||||
"""Backwards-compatible facade over the split capture services.
|
||||
|
||||
The screenshot and recording logic now live in :mod:`services.screenshot`
|
||||
and :mod:`services.screenrecorder`. This module keeps the historical
|
||||
``ScreenCapture`` API and its ``screenshot-taken`` / ``recording-started``
|
||||
/ ``recording-stopped`` signals so existing widgets keep working while new
|
||||
code can depend on the focused services directly.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fabric.core.service import Property, Service, Signal
|
||||
|
||||
from services.screenrecorder import ScreenRecorder
|
||||
from services.screenshot import Screenshot
|
||||
|
||||
|
||||
class ScreenCapture(Service):
|
||||
"""Facade delegating to :class:`Screenshot` and :class:`ScreenRecorder`."""
|
||||
|
||||
@Signal
|
||||
def screenshot_taken(self, path: str) -> None:
|
||||
"""Emitted after a screenshot attempt (``None`` path on cancel/fail)."""
|
||||
|
||||
@Signal
|
||||
def recording_started(self, path: str) -> None:
|
||||
"""Emitted when a recording starts."""
|
||||
|
||||
@Signal
|
||||
def recording_stopped(self, path: str) -> None:
|
||||
"""Emitted when a recording stops."""
|
||||
|
||||
_instance: "ScreenCapture | None" = None
|
||||
|
||||
def __new__(cls) -> "ScreenCapture":
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
if getattr(self, "_initialized", False):
|
||||
return
|
||||
super().__init__(**kwargs)
|
||||
self._initialized = True
|
||||
|
||||
self._screenshot = Screenshot()
|
||||
self._recorder = ScreenRecorder()
|
||||
|
||||
self.screenshots_dir = self._screenshot.screenshots_dir
|
||||
self.recordings_dir = self._recorder.recordings_dir
|
||||
|
||||
self._screenshot.connect("captured", self._on_captured)
|
||||
self._screenshot.connect("failed", self._on_screenshot_failed)
|
||||
self._recorder.connect("started", self._on_recording_started)
|
||||
self._recorder.connect("stopped", self._on_recording_stopped)
|
||||
|
||||
@property
|
||||
def screenshot_service(self) -> Screenshot:
|
||||
"""The underlying screenshot service."""
|
||||
return self._screenshot
|
||||
|
||||
@property
|
||||
def recorder_service(self) -> ScreenRecorder:
|
||||
"""The underlying recorder service."""
|
||||
return self._recorder
|
||||
|
||||
def screenshot(
|
||||
self,
|
||||
target: str = "region",
|
||||
output_dir: str | Path | None = None,
|
||||
show_cursor: bool = False,
|
||||
) -> bool:
|
||||
"""Take a screenshot. See :meth:`Screenshot.screenshot`."""
|
||||
return self._screenshot.screenshot(
|
||||
target, output_dir=output_dir, show_cursor=show_cursor
|
||||
)
|
||||
|
||||
def record(
|
||||
self,
|
||||
target: str = "selection",
|
||||
use_audio: bool = False,
|
||||
show_cursor: bool = False,
|
||||
) -> bool:
|
||||
"""Start a recording. See :meth:`ScreenRecorder.start`."""
|
||||
return self._recorder.start(
|
||||
target, use_audio=use_audio, show_cursor=show_cursor
|
||||
)
|
||||
|
||||
def stop_recording(self) -> bool:
|
||||
"""Stop the active recording."""
|
||||
return self._recorder.stop()
|
||||
|
||||
@Property(bool, "readable", default_value=False)
|
||||
def is_recording(self) -> bool:
|
||||
"""Whether a recorder process is currently running."""
|
||||
return self._recorder.active
|
||||
|
||||
def _on_captured(self, _service: Screenshot, path: str) -> None:
|
||||
self.screenshot_taken(path)
|
||||
|
||||
def _on_screenshot_failed(self, _service: Screenshot, _reason: str) -> None:
|
||||
self.screenshot_taken(None)
|
||||
|
||||
def _on_recording_started(self, _service: ScreenRecorder, path: str) -> None:
|
||||
self.recording_started(path)
|
||||
|
||||
def _on_recording_stopped(self, _service: ScreenRecorder, path: str) -> None:
|
||||
self.recording_stopped(path)
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
if name == "screen_capture_service":
|
||||
import sys
|
||||
|
||||
return sys.modules["services.modus"].screen_capture_service
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -19,7 +19,7 @@ from shared.capture import (
|
||||
HyprlandCaptureBackend,
|
||||
RecordingRequest,
|
||||
)
|
||||
from utils.functions import is_app_running
|
||||
from utils.functions import is_app_running, resolve_monitor
|
||||
|
||||
_RECORDER_PROCESSES = ("wf-recorder", "gpu-screen-recorder")
|
||||
|
||||
@@ -201,17 +201,9 @@ class ScreenRecorder(Service):
|
||||
output_file=self.recordings_dir / f"{timestamp}.mkv",
|
||||
use_audio=use_audio,
|
||||
show_cursor=show_cursor,
|
||||
monitor=self._resolve_monitor(target),
|
||||
monitor=resolve_monitor(target),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_monitor(target: str) -> str | None:
|
||||
if target != "active":
|
||||
return None
|
||||
from services.modus import get_active_monitor_name
|
||||
|
||||
return get_active_monitor_name()
|
||||
|
||||
def _on_start_result(self, result: CaptureResult) -> None:
|
||||
if result.success and result.path:
|
||||
self._setup_state(result.path)
|
||||
|
||||
@@ -19,7 +19,7 @@ from shared.capture import (
|
||||
HyprlandCaptureBackend,
|
||||
ScreenshotRequest,
|
||||
)
|
||||
from utils.functions import find_binary
|
||||
from utils.functions import find_binary, resolve_monitor
|
||||
|
||||
|
||||
class Screenshot(Service):
|
||||
@@ -117,7 +117,7 @@ class Screenshot(Service):
|
||||
) -> ScreenshotRequest:
|
||||
save_dir = Path(output_dir).expanduser() if output_dir else self.screenshots_dir
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
monitor = self._resolve_monitor(target)
|
||||
monitor = resolve_monitor(target)
|
||||
return ScreenshotRequest(
|
||||
target=target,
|
||||
save_path=save_dir / f"{timestamp}.png",
|
||||
@@ -125,14 +125,6 @@ class Screenshot(Service):
|
||||
monitor=monitor,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_monitor(target: str) -> str | None:
|
||||
if target != "active":
|
||||
return None
|
||||
from services.modus import get_active_monitor_name
|
||||
|
||||
return get_active_monitor_name()
|
||||
|
||||
def _on_result(self, result: CaptureResult) -> None:
|
||||
self._set_busy(False)
|
||||
if result.success and result.path:
|
||||
|
||||
Reference in new issue
Block a user