#!/usr/bin/env python3 import argparse import json import logging import os import subprocess import sys from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, List, Optional, Union # Configuration LOG_FORMAT = "%(message)s" logging.basicConfig(level=logging.INFO, format=LOG_FORMAT) logger = logging.getLogger(__name__) # Constants for attribute mappings BOOL_ENABLE_MAP = {"true": "enable", "false": "disable"} BOOL_ALLOW_MAP = {"true": "allow", "false": "disallow"} RGB_RANGE_MAP = { "0": "automatic", "1": "full", "2": "limited", "automatic": "automatic", "full": "full", "limited": "limited" } ROTATION_MAP = {"1": "normal", "2": "left", "4": "inverted", "8": "right"} VRR_POLICY_MAP = { "0": "never", "1": "always", "2": "automatic", "never": "never", "always": "always", "automatic": "automatic" } @dataclass class OutputConfig: """Represents the configuration for a single display output.""" data: Dict[str, Any] @property def name(self) -> str: return self.data.get("name", "Unknown") @property def is_enabled(self) -> bool: return bool(self.data.get("enabled", False)) def get(self, key: str, default: Any = None) -> Any: return self.data.get(key, default) class KDEProfileManager: """Manages KDE display profiles using kscreen-doctor.""" def __init__(self, dry_run: bool = False): self.dry_run = dry_run def _run_command(self, cmd: Union[str, List[str]]) -> None: """Executes a shell command.""" cmd_str = cmd if isinstance(cmd, str) else " ".join(cmd) logger.info(f"[CMD] {cmd_str}") if self.dry_run: return try: subprocess.run(cmd, shell=isinstance(cmd, str), check=True, capture_output=False) except subprocess.CalledProcessError as e: logger.error(f"Error executing command: {e}") def save_profile(self, profile_path: Path) -> None: """Saves the current display configuration to a JSON file.""" logger.info(f"Saving current display profile to {profile_path}...") try: profile_path.parent.mkdir(parents=True, exist_ok=True) with open(profile_path, "w") as f: subprocess.run(["kscreen-doctor", "--json"], stdout=f, check=True) logger.info("Profile saved successfully.") except Exception as e: logger.error(f"Failed to save profile: {e}") sys.exit(1) def _apply_attribute(self, output_name: str, attribute: str, value: Any, value_map: Optional[Dict[str, str]] = None) -> None: """Applies a single attribute to an output.""" if value is None: logger.debug(f"Output {output_name} has no attribute {attribute}, skipping...") return if isinstance(value, (list, dict)): logger.warning(f"Output {output_name} attribute {attribute} has complex value {value}, skipping...") return str_value = str(value).lower() if value_map: if str_value in value_map: value = value_map[str_value] else: logger.warning(f"Value '{str_value}' not found in map for '{attribute}', using as is.") self._run_command(f"kscreen-doctor output.{output_name}.{attribute}.{value}") def _get_mode_string(self, output: OutputConfig, mode_id: str) -> Optional[str]: """Finds the mode string (WxH@R) for a given mode ID.""" for mode in output.get("modes", []): if str(mode.get("id")) == str(mode_id): size = mode.get("size", {}) width = size.get("width") height = size.get("height") refresh = round(mode.get("refreshRate", 0)) if width and height: return f"{width}x{height}@{refresh}" return None def _configure_output(self, output: OutputConfig, all_outputs: List[OutputConfig]) -> None: """Configures all attributes for a single output.""" name = output.name logger.info(f"Configuring output: {name}") # Direct mappings self._apply_attribute(name, "wcg", output.get("wcg"), BOOL_ENABLE_MAP) self._apply_attribute(name, "sdr-brightness", output.get("sdr-brightness")) self._apply_attribute(name, "vrrpolicy", output.get("vrrPolicy"), VRR_POLICY_MAP) self._apply_attribute(name, "rgbrange", output.get("rgbRange"), RGB_RANGE_MAP) self._apply_attribute(name, "overscan", output.get("overscan")) self._apply_attribute(name, "hdr", output.get("hdr"), BOOL_ENABLE_MAP) # Brightness (normalized 0.0-1.0 to 0-100) brightness = output.get("brightness") if brightness is not None: self._apply_attribute(name, "brightness", int(float(brightness) * 100)) # Max BPC max_bpc = output.get("maxBpc") if max_bpc == 0: max_bpc = "automatic" self._apply_attribute(name, "maxbpc", max_bpc) # DDC/CI self._apply_attribute(name, "ddcCi", output.get("ddcCiAllowed"), BOOL_ALLOW_MAP) # Mirroring mirror_source_id = output.get("replicationSource", 0) if mirror_source_id != 0: source_name = next((o.name for o in all_outputs if o.get("id") == mirror_source_id), "none") self._apply_attribute(name, "mirror", source_name) else: self._apply_attribute(name, "mirror", "none") # ICC Profile self._apply_attribute(name, "iccProfilePath", output.get("iccProfilePath")) # Mode mode_id = output.get("currentModeId") if mode_id: mode_str = self._get_mode_string(output, mode_id) if mode_str: self._apply_attribute(name, "mode", mode_str) # Position pos = output.get("pos") if pos: self._apply_attribute(name, "position", f"{pos.get('x', 0)},{pos.get('y', 0)}") # Scale and Rotation self._apply_attribute(name, "scale", output.get("scale")) self._apply_attribute(name, "rotation", output.get("rotation"), ROTATION_MAP) # Priority and Primary status priority = output.get("priority") if priority is not None: if priority == 1: self._run_command(f"kscreen-doctor output.{name}.primary") self._run_command(f"kscreen-doctor output.{name}.priority.{priority}") def load_profile(self, profile_path: Path) -> None: """Loads and applies a display configuration from a JSON file.""" logger.info(f"Loading display profile from {profile_path}...") if not profile_path.exists(): logger.error(f"Profile file not found: {profile_path}") sys.exit(1) try: with open(profile_path, "r") as f: data = json.load(f) except Exception as e: logger.error(f"Error reading profile JSON: {e}") sys.exit(1) outputs = [OutputConfig(out) for out in data.get("outputs", [])] # 1. Handle Enable/Disable (Enable first to ensure we always have an active display) logger.info("Enabling requested outputs...") for out in outputs: if out.is_enabled: self._run_command(f"kscreen-doctor output.{out.name}.enable") logger.info("Disabling requested outputs...") for out in outputs: if not out.is_enabled: self._run_command(f"kscreen-doctor output.{out.name}.disable") # 2. Configure each output for out in outputs: if out.is_enabled: self._configure_output(out, outputs) logger.info("Display configuration restored.") def main(): parser = argparse.ArgumentParser(description="KDE Display Profile Manager") parser.add_argument("--dry-run", action="store_true", help="Print commands without executing them") parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") subparsers = parser.add_subparsers(dest="command", required=True) save_parser = subparsers.add_parser("save", help="Save current configuration") save_parser.add_argument("profile", type=Path, help="Path to save the profile") load_parser = subparsers.add_parser("load", help="Load a configuration") load_parser.add_argument("profile", type=Path, help="Path to the profile to load") args = parser.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) manager = KDEProfileManager(dry_run=args.dry_run) if args.command == "save": manager.save_profile(args.profile) elif args.command == "load": manager.load_profile(args.profile) if __name__ == "__main__": main()