modlink_studio.app

  1from __future__ import annotations
  2
  3import argparse
  4import logging
  5import sys
  6from collections.abc import Sequence
  7from dataclasses import dataclass
  8from importlib.resources import files
  9from pathlib import Path
 10
 11import pyqtgraph as pg
 12from PyQt6.QtGui import QIcon, QPixmap
 13from PyQt6.QtWidgets import QApplication, QMessageBox
 14from qfluentwidgets import Theme, setTheme
 15
 16from modlink_core import ModLinkEngine, configure_host_logging
 17from modlink_ui import MainWindow
 18from modlink_ui.bridge import QtModLinkBridge
 19
 20from .debug_bootstrap import install_debug_bootstrap
 21
 22logger = logging.getLogger(__name__)
 23
 24
 25@dataclass(frozen=True)
 26class _LaunchOptions:
 27    log_path: Path | None
 28    qt_args: tuple[str, ...]
 29
 30
 31def _load_app_icon() -> QIcon:
 32    icon = _load_packaged_app_icon()
 33    if not icon.isNull():
 34        return icon
 35
 36    # Keep a repo-local fallback so editable/dev runs still pick up the asset.
 37    assets_dir = Path(__file__).resolve().parents[3] / "assets"
 38    icon_path = assets_dir / "app_icon.png"
 39    if icon_path.is_file():
 40        return QIcon(str(icon_path))
 41    return QIcon()
 42
 43
 44def _load_packaged_app_icon() -> QIcon:
 45    try:
 46        icon_bytes = files("modlink_studio").joinpath("app_icon.png").read_bytes()
 47    except (FileNotFoundError, ModuleNotFoundError, OSError):
 48        return QIcon()
 49
 50    pixmap = QPixmap()
 51    if not pixmap.loadFromData(icon_bytes):
 52        return QIcon()
 53
 54    icon = QIcon()
 55    icon.addPixmap(pixmap)
 56    return icon
 57
 58
 59def _create_application(argv: Sequence[str] | None = None) -> QApplication:
 60    """Create or reuse the process-level Qt application."""
 61
 62    existing = QApplication.instance()
 63    if existing is not None:
 64        return existing
 65
 66    app = QApplication(list(argv) if argv is not None else sys.argv)
 67    app.setApplicationName("ModLink Studio")
 68    app.setOrganizationName("ModLink")
 69    app.setWindowIcon(_load_app_icon())
 70    return app
 71
 72
 73def _shutdown_bridge_with_prompt(bridge: QtModLinkBridge) -> None:
 74    try:
 75        bridge.shutdown()
 76    except Exception as exc:
 77        QMessageBox.critical(
 78            None,
 79            "ModLink Studio",
 80            f"关闭后台资源时发生错误。\n\n{exc}",
 81        )
 82
 83
 84def _build_argument_parser(*, prog: str) -> argparse.ArgumentParser:
 85    parser = argparse.ArgumentParser(
 86        prog=prog,
 87        description="Start the ModLink Studio desktop host.",
 88    )
 89    parser.add_argument(
 90        "--log-path",
 91        type=Path,
 92        help="write the rotating desktop log to a specific file",
 93    )
 94    return parser
 95
 96
 97def _parse_launch_options(
 98    argv: Sequence[str] | None = None,
 99    *,
100    prog: str,
101) -> _LaunchOptions:
102    parser = _build_argument_parser(prog=prog)
103    raw_args = list(argv) if argv is not None else list(sys.argv[1:])
104    namespace, qt_args = parser.parse_known_args(raw_args)
105    return _LaunchOptions(
106        log_path=namespace.log_path,
107        qt_args=tuple(str(arg) for arg in qt_args),
108    )
109
110
111def _run_app(
112    argv: Sequence[str] | None = None,
113    *,
114    debug: bool,
115    prog: str,
116) -> None:
117    launch_options = _parse_launch_options(argv, prog=prog)
118    if debug:
119        install_debug_bootstrap()
120    log_path = configure_host_logging(
121        log_path=launch_options.log_path,
122        log_filename="modlink-studio.log",
123        debug=debug,
124    )
125    logger.info("Starting ModLink Studio")
126    logger.info("Desktop logs will be written to %s", log_path)
127    if debug:
128        logger.info("Debug mode enabled")
129    if launch_options.qt_args:
130        logger.debug("Forwarding Qt arguments: %s", launch_options.qt_args)
131
132    app = _create_application([prog, *launch_options.qt_args])
133    pg.setConfigOptions(useOpenGL=True)
134    setTheme(Theme.AUTO)
135    runtime = ModLinkEngine(
136        parent=app,
137    )
138    bridge = QtModLinkBridge(runtime, parent=app)
139    app.aboutToQuit.connect(lambda: _shutdown_bridge_with_prompt(bridge))
140    window = MainWindow(engine=bridge)
141    window.setWindowIcon(_load_app_icon())
142    window.show()
143    raise SystemExit(app.exec())
144
145
146def main(argv: Sequence[str] | None = None) -> None:
147    """Single supported non-debug startup entry for ModLink Studio."""
148
149    _run_app(argv, debug=False, prog="modlink-studio")
150
151
152def debug_main(argv: Sequence[str] | None = None) -> None:
153    """Console-backed debug startup entry for ModLink Studio."""
154
155    _run_app(argv, debug=True, prog="modlink-studio-debug")
logger = <Logger modlink_studio.app (WARNING)>
def main(argv: Sequence[str] | None = None) -> None:
147def main(argv: Sequence[str] | None = None) -> None:
148    """Single supported non-debug startup entry for ModLink Studio."""
149
150    _run_app(argv, debug=False, prog="modlink-studio")

Single supported non-debug startup entry for ModLink Studio.

def debug_main(argv: Sequence[str] | None = None) -> None:
153def debug_main(argv: Sequence[str] | None = None) -> None:
154    """Console-backed debug startup entry for ModLink Studio."""
155
156    _run_app(argv, debug=True, prog="modlink-studio-debug")

Console-backed debug startup entry for ModLink Studio.