Files
AR-Autopilot/display_manager/config.py
T
alro65 42b2eec2e1 feat: AR Display Manager — multi-monitor floating app switcher
Adds the AR Display Manager daemon (Task #9):

- display_manager/ package with 8 modules:
  - app_registry.py   : static metadata for the 4 bridge apps
  - config.py         : JSON-persisted config + per-screen layout store
  - win32_utils.py    : ctypes wrappers (EnumWindows, SetWindowPos, ShowWindow)
  - process_manager.py: launch + track app processes, HWND lookup
  - floating_button.py: always-on-top 52×52 px AR button per monitor
                        with draggable placement + custom-painted popup
  - display_manager.py: orchestrator (QScreens → buttons → app placement
                        + system tray with Settings/Launch/Quit)
  - settings_dialog.py: modal dialog for exe paths, button position,
                        and Windows autostart toggle
  - autostart.py      : HKCU Run registry read/write helpers
- display_manager_main.py at repo root: launcher script
- Studio Overview tab: "Lanzar Display Manager" button
- pyproject.toml: display-manager optional dependency group (PySide6)

Layout persistence: ~/.ar-autopilot/display_manager/layout.json
Config:            ~/.ar-autopilot/display_manager/config.json
Supports up to 4 monitors (2 HDMI native + 2 USB DisplayLink).
Double-click tray icon to toggle button visibility.

AR_electronics — AR-Autopilot Project
2026-05-24 21:48:27 -04:00

111 lines
3.8 KiB
Python

"""Configuration and layout persistence for AR Display Manager."""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
CONFIG_DIR = Path.home() / ".ar-autopilot" / "display_manager"
CONFIG_FILE = CONFIG_DIR / "config.json"
LAYOUT_FILE = CONFIG_DIR / "layout.json"
@dataclass
class AppExeConfig:
"""Path + launch args for one application."""
exe: str = ""
args: list[str] = field(default_factory=list)
title_hint: str = "" # substring to match window title when searching
_DEFAULT_EXES: dict[str, AppExeConfig] = {
"autopilot": AppExeConfig(
exe=str(
REPO_ROOT / "display" / "build" / "windows" / "x64" / "runner"
/ "Release" / "display.exe"
),
title_hint="AR Autopilot",
),
"ecdis": AppExeConfig(exe="", title_hint="AR-ECDIS"),
"compass": AppExeConfig(exe="", title_hint="Compass"),
"gps": AppExeConfig(exe="", title_hint="GPS Navigator"),
}
@dataclass
class DisplayManagerConfig:
apps: dict[str, AppExeConfig] = field(default_factory=dict)
button_position: str = "top-right" # top-right | top-left | bottom-right | bottom-left
button_margin: int = 12 # px from screen edge
autolaunch: bool = True # launch apps on startup if exe exists
# ------------------------------------------------------------------ I/O
@classmethod
def load(cls) -> "DisplayManagerConfig":
cfg = cls(apps=dict(_DEFAULT_EXES))
if not CONFIG_FILE.exists():
return cfg
try:
with open(CONFIG_FILE, encoding="utf-8") as f:
data = json.load(f)
cfg.button_position = data.get("button_position", cfg.button_position)
cfg.button_margin = int(data.get("button_margin", cfg.button_margin))
cfg.autolaunch = bool(data.get("autolaunch", cfg.autolaunch))
for app_id, raw in data.get("apps", {}).items():
cfg.apps[app_id] = AppExeConfig(
exe=raw.get("exe", ""),
args=raw.get("args", []),
title_hint=raw.get("title_hint", ""),
)
except Exception:
pass
return cfg
def save(self) -> None:
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(
{
"button_position": self.button_position,
"button_margin": self.button_margin,
"autolaunch": self.autolaunch,
"apps": {
k: {"exe": v.exe, "args": v.args, "title_hint": v.title_hint}
for k, v in self.apps.items()
},
},
f,
indent=2,
)
# --------------------------------------------------------------------------- Layout
class LayoutStore:
"""Persists {screen_serial → app_id} so layout survives restarts."""
def __init__(self) -> None:
self._data: dict[str, str] = {}
self._load()
def _load(self) -> None:
if LAYOUT_FILE.exists():
try:
with open(LAYOUT_FILE, encoding="utf-8") as f:
self._data = json.load(f)
except Exception:
self._data = {}
def get(self, screen_serial: str) -> str | None:
return self._data.get(screen_serial)
def set(self, screen_serial: str, app_id: str) -> None:
self._data[screen_serial] = app_id
self._save()
def _save(self) -> None:
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
with open(LAYOUT_FILE, "w", encoding="utf-8") as f:
json.dump(self._data, f, indent=2)