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.