refactor(services): split screen capture into screenshot + recorder services
Split the monolithic ScreenCapture service into focused Screenshot and ScreenRecorder services behind a pluggable capture backend. - Add shared/capture.py: CaptureBackend abstraction + HyprlandCaptureBackend (hyprshot/grim/slurp/wf-recorder) and reusable CaptureNotifier - Add services/screenshot.py and services/screenrecorder.py with Fabric properties/signals; recording state kept in memory (no /tmp files) - Keep services/screencapture.py as a backwards-compatible facade so existing widgets and signals keep working - Resolve monitors via Hyprland IPC helpers in services/modus.py instead of spawning `hyprctl monitors -j` per capture - Implement real window recording (slurp window boxes) and split the record-window / record-region UI mappings - Register screenshot_service / screen_recorder_service singletons - Remove dead src/utils/inhibit.py duplicate; minor modus.lua quoting fix
This commit is contained in:
9 files changed
+1063
-566
No files matched your search
@@ -1,193 +0,0 @@
|
||||
# From https://github.com/stwa/wayland-idle-inhibitor
|
||||
# License: WTFPL Version 2
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from fabric.utils import logger
|
||||
|
||||
# Add the project root to sys.path to allow absolute imports from 'utils'
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from signal import SIGINT, SIGTERM, signal
|
||||
from threading import Event, Timer
|
||||
|
||||
from pywayland.client.display import Display
|
||||
from pywayland.protocol.idle_inhibit_unstable_v1.zwp_idle_inhibit_manager_v1 import (
|
||||
ZwpIdleInhibitManagerV1,
|
||||
)
|
||||
from pywayland.protocol.wayland.wl_compositor import WlCompositor
|
||||
from pywayland.protocol.wayland.wl_registry import WlRegistryProxy
|
||||
from pywayland.protocol.wayland.wl_surface import WlSurface
|
||||
|
||||
from utils.functions import set_process_name
|
||||
|
||||
|
||||
@dataclass
|
||||
class GlobalRegistry:
|
||||
surface: WlSurface | None = None
|
||||
inhibit_manager: ZwpIdleInhibitManagerV1 | None = None
|
||||
|
||||
|
||||
def parse_duration(duration_str: str) -> int:
|
||||
"""Parse duration string into seconds.
|
||||
Examples: '1h', '30m', '45s', '1.5h', '1.5m', '30.5s', 'on', 'off'
|
||||
"""
|
||||
try:
|
||||
if duration_str.lower() in ["on", "off"]:
|
||||
return 0
|
||||
elif duration_str.endswith("h"):
|
||||
return int(float(duration_str[:-1]) * 3600)
|
||||
elif duration_str.endswith("m"):
|
||||
return int(float(duration_str[:-1]) * 60)
|
||||
elif duration_str.endswith("s"):
|
||||
return int(float(duration_str[:-1]))
|
||||
else:
|
||||
return int(duration_str)
|
||||
except ValueError as err:
|
||||
raise ValueError(
|
||||
"Invalid duration format. Use '1h', '30m', '45s', 'on', 'off', etc."
|
||||
) from err
|
||||
|
||||
|
||||
def kill_existing_inhibit_processes():
|
||||
"""Kill any existing modus-inhibit processes."""
|
||||
try:
|
||||
# Find all modus-inhibit processes except the current one
|
||||
output = subprocess.check_output(["pgrep", "-f", "modus-inhibit"], text=True)
|
||||
pids = output.strip().split("\n")
|
||||
current_pid = str(os.getpid())
|
||||
|
||||
killed_count = 0
|
||||
for pid in pids:
|
||||
pid = pid.strip()
|
||||
if pid and pid != current_pid:
|
||||
try:
|
||||
subprocess.run(["kill", pid], check=True)
|
||||
killed_count += 1
|
||||
except subprocess.CalledProcessError:
|
||||
pass # Process might have already exited
|
||||
|
||||
if killed_count > 0:
|
||||
print(f"Stopped {killed_count} existing inhibit process(es)")
|
||||
else:
|
||||
print("No existing inhibit processes were running")
|
||||
return killed_count
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
# No processes found
|
||||
logger.error("No existing inhibit processes were running")
|
||||
return 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping existing processes: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
def handle_registry_global(
|
||||
wl_registry: WlRegistryProxy, id_num: int, iface_name: str, version: int
|
||||
) -> None:
|
||||
global_registry: GlobalRegistry = wl_registry.user_data or GlobalRegistry()
|
||||
|
||||
if iface_name == "wl_compositor":
|
||||
compositor = wl_registry.bind(id_num, WlCompositor, version)
|
||||
global_registry.surface = compositor.create_surface() # type: ignore
|
||||
elif iface_name == "zwp_idle_inhibit_manager_v1":
|
||||
global_registry.inhibit_manager = wl_registry.bind(
|
||||
id_num, ZwpIdleInhibitManagerV1, version
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Inhibit system idle for a specified duration"
|
||||
)
|
||||
parser.add_argument(
|
||||
"duration",
|
||||
nargs="?",
|
||||
default="0",
|
||||
help='Duration to inhibit (e.g., "1h", "30m", "45s", "on", "off"). Use "on" for indefinite, "off" to stop.',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Handle "off" argument - kill existing processes and exit
|
||||
if args.duration.lower() == "off":
|
||||
kill_existing_inhibit_processes()
|
||||
sys.exit(0)
|
||||
|
||||
done = Event()
|
||||
signal(SIGINT, lambda _, __: done.set())
|
||||
signal(SIGTERM, lambda _, __: done.set())
|
||||
|
||||
global_registry = GlobalRegistry()
|
||||
|
||||
try:
|
||||
display = Display()
|
||||
display.connect()
|
||||
|
||||
registry = display.get_registry() # type: ignore
|
||||
registry.user_data = global_registry
|
||||
registry.dispatcher["global"] = handle_registry_global
|
||||
|
||||
def shutdown() -> None:
|
||||
display.dispatch()
|
||||
display.roundtrip()
|
||||
display.disconnect()
|
||||
|
||||
display.dispatch()
|
||||
display.roundtrip()
|
||||
|
||||
if global_registry.surface is None:
|
||||
print("Error: Failed to create Wayland surface.")
|
||||
shutdown()
|
||||
sys.exit(1)
|
||||
|
||||
if global_registry.inhibit_manager is None:
|
||||
print("Error: Your Wayland compositor does not support idle inhibition.")
|
||||
print("This usually means either:")
|
||||
print(
|
||||
"1. Your compositor (like Hyprland) doesn't support the idle-inhibit protocol"
|
||||
)
|
||||
print("2. The protocol is not enabled in your compositor")
|
||||
print("\nFor Hyprland, you can enable it by adding to your config:")
|
||||
print("misc:disable_autoreload = true")
|
||||
print("misc:enable_wayland_protocols = true")
|
||||
shutdown()
|
||||
sys.exit(1)
|
||||
|
||||
inhibitor = global_registry.inhibit_manager.create_inhibitor( # type: ignore
|
||||
global_registry.surface
|
||||
)
|
||||
|
||||
display.dispatch()
|
||||
display.roundtrip()
|
||||
|
||||
duration = parse_duration(args.duration)
|
||||
if duration > 0:
|
||||
print(f"Inhibiting idle for {args.duration}...")
|
||||
# Set up timer to release inhibition
|
||||
timer = Timer(duration, lambda: done.set())
|
||||
timer.start()
|
||||
else:
|
||||
print("Inhibiting idle indefinitely...")
|
||||
|
||||
done.wait()
|
||||
print("Shutting down...")
|
||||
|
||||
inhibitor.destroy()
|
||||
if duration > 0:
|
||||
timer.cancel()
|
||||
|
||||
shutdown()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e!s}")
|
||||
logger.error("Make sure you're running this under a Wayland session.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
set_process_name("modus-inhibit")
|
||||
main()
|
||||
@@ -14,6 +14,8 @@ def __getattr__(name):
|
||||
"notification_service",
|
||||
"audio_service",
|
||||
"screen_capture_service",
|
||||
"screenshot_service",
|
||||
"screen_recorder_service",
|
||||
):
|
||||
return getattr(_mod(), name)
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
Reference in new issue
Block a user