vibefpmgui.main_window
Main window: pick an fpm.toml, run fpm commands, view the dependency graph.
MainWindow is put together from the mixins in vibefpmgui.window (one
per area of the window); here it's started, built in order and closed,
and main starts the app.
1"""Main window: pick an fpm.toml, run fpm commands, view the dependency graph. 2 3`MainWindow` is put together from the mixins in `vibefpmgui.window` (one 4per area of the window); here it's started, built in order and closed, 5and `main` starts the app. 6""" 7 8from __future__ import annotations 9 10import codecs 11import os 12import sys 13import tempfile 14from collections import deque 15from pathlib import Path 16 17from qtpy.QtCore import QProcess, QSettings, Qt 18from qtpy.QtGui import QAction 19from qtpy.QtWidgets import QApplication, QMainWindow, QMessageBox 20 21from . import __version__, lsp, themes 22from . import manifest as m 23from .compat import set_macos_bundle_name, set_macos_display_name, set_windows_app_id 24from .graph_view import EDGE_STYLES, LAYOUTS, THEME_CHOICES 25from .icons import app_icon 26from .parallel_tests import ParallelTests 27from .project_options import LayeredSettings 28from .window.ai import AIMixin 29from .window.ai_chat import AIChatMixin 30from .window.build_outputs import BuildOutputsMixin 31from .window.complexity import ComplexityMixin 32from .window.configs import ConfigsMixin 33from .window.coverage import CoverageMixin 34from .window.debugging import DebuggingMixin 35from .window.editing import EditingMixin 36from .window.editor_tabs import EditorTabsMixin 37from .window.git import GitMixin 38from .window.language import LanguageMixin 39from .window.options import ProjectOptionsMixin 40from .window.output import OUTPUT_TAIL_LINES, OutputMixin 41from .window.packages import PackagesMixin 42from .window.panels import PanelsMixin 43from .window.project import ProjectMixin 44from .window.running import RunningMixin 45from .window.search import SearchMixin 46from .window.testing import TestingMixin 47from .window.view import ViewMixin 48 49 50class MainWindow(PanelsMixin, ViewMixin, ProjectMixin, PackagesMixin, 51 EditorTabsMixin, EditingMixin, LanguageMixin, SearchMixin, GitMixin, 52 RunningMixin, DebuggingMixin, ConfigsMixin, ProjectOptionsMixin, BuildOutputsMixin, OutputMixin, TestingMixin, CoverageMixin, ComplexityMixin, 53 AIMixin, AIChatMixin, 54 QMainWindow): 55 """The window. Each part of it is a mixin in `vibefpmgui.window`; this 56 class starts it, builds it (in order: `_build_ui`), and closes it.""" 57 58 def __init__(self, manifest_path: str | None = None, remember_last: bool = True): 59 """``remember_last`` False: an extra window (File → Open in New Window) 60 doesn't become the project reopened at the next start.""" 61 super().__init__() 62 self.remember_last = remember_last 63 self.setWindowTitle("VibeFPMGUI") 64 self.resize(1300, 850) 65 self.settings = LayeredSettings(make_settings()) 66 # The project opened first: its options (if its configuration file has 67 # them) are the settings the window is made with. 68 start = manifest_path or self.settings.value("last_manifest", "") 69 start = Path(start).resolve() if start and Path(start).is_file() else None 70 self._use_config_file(self._config_file_for(start) if start is not None else None, start) 71 themes.chosen_style = str(self.settings.value("app_style", themes.AUTOMATIC) or themes.AUTOMATIC) 72 themes.apply(str(self.settings.value("app_theme", themes.SYSTEM) or themes.SYSTEM)) # (before the widgets) 73 self._session_busy = False # restoring the open files (don't save meanwhile) 74 self.lsp_client = lsp.LspClient(self) # fortls, for the editor (see _update_language_server) 75 self.lsp_client.stopped.connect(self._language_server_stopped) 76 self.lsp_client.message.connect(lambda text: self.statusBar().showMessage(f"fortls: {text}", 8000)) 77 self.lsp_client.diagnostics.connect(self._lsp_diagnostics) 78 self._lsp_diagnostics_by_file: dict[Path, list] = {} # the latest, to show again when turned on 79 self.manifest_path: Path | None = None 80 self.graph: m.DependencyGraph | None = None 81 self.process: QProcess | None = None 82 self.parallel_tests: ParallelTests | None = None # tests run by us, several at once 83 self._refresh_after = False 84 self._stop_requested = False # Stop was pressed for the running process (see stop) 85 self._monitoring = False 86 self._partial = "" # output text after the last newline 87 # The running command's output as text: a character whose bytes arrive 88 # in two reads is decoded whole (see _begin_output). 89 self._decoder = codecs.getincrementaldecoder("utf-8")(errors="replace") 90 self._esc_tail = "" # color code cut off at the end of the last output chunk 91 self._full_output_path = Path(tempfile.gettempdir()) / f"vibefpmgui-output-{os.getpid()}.log" 92 self._full_output = None # the file the running command's whole output is saved to 93 self._shown_chars = 0 # how much of it the Output panel shows (see OUTPUT_SHOW_LIMIT) 94 self._hidden_chars = 0 95 self._hidden_lines = 0 96 self._tail: deque[str] = deque(maxlen=OUTPUT_TAIL_LINES) 97 self._tail_partial = "" 98 # None, or while running Test/Run: "list" (fpm ... --list), "marked" 99 # (fpm ... --runner <wrapper>) or "direct" (a built program, via the wrapper) 100 self._phase: str | None = None 101 self._captured: list[str] = [] # output of "fpm test --list" 102 self._held_blank = False # blank line that may precede a test marker 103 self._test_args: tuple = () 104 self.command_actions: list[QAction] = [] 105 106 self._build_ui() 107 self._restore_settings() 108 self._update_editor_actions() 109 110 if start is not None: 111 self.open_manifest(start) 112 113 # ------------------------------------------------------------------ UI 114 def _build_ui(self): 115 """The menus, the panels and the editor, in this order (later steps 116 connect to what earlier ones made).""" 117 self._build_file_menu() 118 self._build_edit_menu() 119 self._build_view_menu() 120 options = self._build_options_panel() 121 self._build_commands() 122 self._build_debugging() 123 self._build_features_menu() 124 self._build_ai_menu() 125 files_box = self._build_project_files() 126 graph_tab = self._build_graph_panel() 127 self._build_editor_tabs() 128 self._build_outputs_view() 129 console_box = self._build_output_panel() 130 self._build_run_panels() 131 self._build_review_panel() 132 self._build_search_panels() 133 self._build_terminal() 134 self._build_git_panel() 135 self._build_complexity_panel() 136 self._link_file_lists() 137 self._build_central() 138 self._build_docks(options, files_box, graph_tab, console_box) 139 self.statusBar().showMessage("Ready") 140 self._set_project_enabled(False) 141 self._build_help_menu() 142 143 # ------------------------------------------------------------ settings 144 def _restore_settings(self): 145 s = self.settings 146 geometry = s.value("geometry") 147 if geometry is not None: 148 self.restoreGeometry(geometry) 149 layout = s.value("panels") # (where each panel was: docked, stacked, floating, hidden) 150 self._layout_restored = layout is not None and self.restore_layout( 151 layout, s.value("panels_front", [], type=list) or []) 152 # A layout saved before a panel existed: the panel goes in the stack of 153 # the one it's next to by default. 154 for name, beside in (("coverage", "tests"), ("git", "search"), ("complexity", "coverage"), 155 ("debug", "run")): 156 dock = self.docks[name] 157 if self._layout_restored and self.dockWidgetArea(dock) == Qt.NoDockWidgetArea: 158 self.addDockWidget(Qt.BottomDockWidgetArea, dock) 159 self.tabifyDockWidget(self.docks[beside], dock) 160 # Applied once the project's own [profiles] are in the list. 161 self._apply_build_options() 162 self.fpm_edit.setText(s.value("fpm", "")) 163 self.set_font_delta(s.value("font_delta", 0, type=int), announce=False) 164 self._set_graph_layout(s.value("graph_layout", next(iter(LAYOUTS)))) 165 self._set_graph_theme(s.value("graph_theme", next(iter(THEME_CHOICES)))) 166 self._set_graph_edges(s.value("graph_edges", next(iter(EDGE_STYLES)))) 167 168 def clear_settings_and_quit(self): 169 """Forget every saved setting and quit, so the next start is like the 170 first (after asking; nothing happens if a window can't close).""" 171 answer = QMessageBox.warning( 172 self, "Clear Settings and Quit", 173 "Clear all of VibeFPMGUI's saved settings and quit?\n\n" 174 "Recent projects, open files, window and panel layout, build options, View menu " 175 "choices and your spelling dictionary are all forgotten; the next start is like " 176 "the first. Your files are not touched.\n\n" 177 "Close any other VibeFPMGUI windows first: they save their settings when they close.", 178 QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.Cancel) 179 if answer != QMessageBox.Yes: 180 return 181 self._clearing_settings = True # (closeEvent: nothing saved) 182 if not self.close(): # (unsaved files: Cancel keeps the window and the settings) 183 self._clearing_settings = False 184 return 185 self.settings.sync() # (anything written while closing, then all of it removed) 186 self.settings.clear() 187 self.settings.sync() 188 QApplication.quit() 189 190 def closeEvent(self, event): 191 clearing = getattr(self, "_clearing_settings", False) 192 # (Before closing brings other panels to the front: e.g. stopping a review shows Build.) 193 front = self.front_panels() 194 if not clearing: 195 self._save_session() 196 # Every question first: answering Cancel or No leaves everything running. 197 if not self.maybe_save_configuration(): 198 event.ignore() 199 return 200 for editor in self.editors(): 201 self.tabs.setCurrentWidget(editor) 202 if not editor.maybe_save(): 203 event.ignore() 204 return 205 if self.busy(): 206 answer = QMessageBox.question(self, "fpm is running", 207 "An fpm command is still running. Stop it and quit?") 208 if answer != QMessageBox.Yes: 209 event.ignore() 210 return 211 self._stop_and_wait(2000) 212 for editor in self.editors(): 213 editor.close_down() 214 self.debug_session.stop() 215 self.lsp_client.stop() 216 self.search_panel.shutdown() 217 self.terminal_panel.shutdown() 218 self._shutdown_ai() 219 self._stop_coverage() 220 self._end_output(show_tail=False) 221 self._full_output_path.unlink(missing_ok=True) # (the full-output log: only for this run) 222 if clearing: 223 super().closeEvent(event) 224 return 225 s = self.settings 226 s.setValue("geometry", self.saveGeometry()) 227 s.setValue("fpm", self.fpm_edit.text()) 228 self._store_window_options(front) # (the project's options, or the app's settings) 229 self._write_config_file() 230 super().closeEvent(event) 231 232 233def make_settings() -> QSettings: 234 """The app's saved settings. VIBEFPMGUI_SETTINGS=/path/file.ini uses a 235 separate file instead (e.g. for testing without touching your own).""" 236 path = os.environ.get("VIBEFPMGUI_SETTINGS") 237 if path: 238 return QSettings(path, QSettings.IniFormat) 239 return QSettings("VibeFPMGUI", "VibeFPMGUI") 240 241 242_APP_NAME = "VibeFPMGUI" 243 244 245def qt_argv(argv: list[str]) -> list[str]: 246 """The command line for QApplication: the app's name in place of the 247 script's ("…/vibefpmgui/__main__.py" when started with python -m).""" 248 return [_APP_NAME, *argv[1:]] 249 250 251def main(argv: list[str] | None = None) -> int: 252 argv = sys.argv if argv is None else argv 253 set_windows_app_id() # Windows: our own taskbar icon, not python.exe's 254 set_macos_bundle_name(_APP_NAME) # macOS: our name in the menu bar, not "Python" (before Qt starts) 255 # (argv[0] is the name Qt uses for macOS's About/Hide/Quit items: "__main__.py" otherwise.) 256 app = QApplication.instance() or QApplication(qt_argv(argv)) 257 set_macos_display_name(_APP_NAME) # and in the Dock, not "python3.14" 258 app.setApplicationName(_APP_NAME) 259 app.setWindowIcon(app_icon()) # also the Dock icon on macOS 260 app.setApplicationVersion(__version__) 261 path = next((a for a in argv[1:] if not a.startswith("-")), None) 262 if path and Path(path).is_dir(): 263 path = str(Path(path) / "fpm.toml") 264 # --new-window: opened from another window, so not the project to reopen next time 265 window = MainWindow(path, remember_last="--new-window" not in argv[1:]) 266 window.show() 267 return app.exec()
51class MainWindow(PanelsMixin, ViewMixin, ProjectMixin, PackagesMixin, 52 EditorTabsMixin, EditingMixin, LanguageMixin, SearchMixin, GitMixin, 53 RunningMixin, DebuggingMixin, ConfigsMixin, ProjectOptionsMixin, BuildOutputsMixin, OutputMixin, TestingMixin, CoverageMixin, ComplexityMixin, 54 AIMixin, AIChatMixin, 55 QMainWindow): 56 """The window. Each part of it is a mixin in `vibefpmgui.window`; this 57 class starts it, builds it (in order: `_build_ui`), and closes it.""" 58 59 def __init__(self, manifest_path: str | None = None, remember_last: bool = True): 60 """``remember_last`` False: an extra window (File → Open in New Window) 61 doesn't become the project reopened at the next start.""" 62 super().__init__() 63 self.remember_last = remember_last 64 self.setWindowTitle("VibeFPMGUI") 65 self.resize(1300, 850) 66 self.settings = LayeredSettings(make_settings()) 67 # The project opened first: its options (if its configuration file has 68 # them) are the settings the window is made with. 69 start = manifest_path or self.settings.value("last_manifest", "") 70 start = Path(start).resolve() if start and Path(start).is_file() else None 71 self._use_config_file(self._config_file_for(start) if start is not None else None, start) 72 themes.chosen_style = str(self.settings.value("app_style", themes.AUTOMATIC) or themes.AUTOMATIC) 73 themes.apply(str(self.settings.value("app_theme", themes.SYSTEM) or themes.SYSTEM)) # (before the widgets) 74 self._session_busy = False # restoring the open files (don't save meanwhile) 75 self.lsp_client = lsp.LspClient(self) # fortls, for the editor (see _update_language_server) 76 self.lsp_client.stopped.connect(self._language_server_stopped) 77 self.lsp_client.message.connect(lambda text: self.statusBar().showMessage(f"fortls: {text}", 8000)) 78 self.lsp_client.diagnostics.connect(self._lsp_diagnostics) 79 self._lsp_diagnostics_by_file: dict[Path, list] = {} # the latest, to show again when turned on 80 self.manifest_path: Path | None = None 81 self.graph: m.DependencyGraph | None = None 82 self.process: QProcess | None = None 83 self.parallel_tests: ParallelTests | None = None # tests run by us, several at once 84 self._refresh_after = False 85 self._stop_requested = False # Stop was pressed for the running process (see stop) 86 self._monitoring = False 87 self._partial = "" # output text after the last newline 88 # The running command's output as text: a character whose bytes arrive 89 # in two reads is decoded whole (see _begin_output). 90 self._decoder = codecs.getincrementaldecoder("utf-8")(errors="replace") 91 self._esc_tail = "" # color code cut off at the end of the last output chunk 92 self._full_output_path = Path(tempfile.gettempdir()) / f"vibefpmgui-output-{os.getpid()}.log" 93 self._full_output = None # the file the running command's whole output is saved to 94 self._shown_chars = 0 # how much of it the Output panel shows (see OUTPUT_SHOW_LIMIT) 95 self._hidden_chars = 0 96 self._hidden_lines = 0 97 self._tail: deque[str] = deque(maxlen=OUTPUT_TAIL_LINES) 98 self._tail_partial = "" 99 # None, or while running Test/Run: "list" (fpm ... --list), "marked" 100 # (fpm ... --runner <wrapper>) or "direct" (a built program, via the wrapper) 101 self._phase: str | None = None 102 self._captured: list[str] = [] # output of "fpm test --list" 103 self._held_blank = False # blank line that may precede a test marker 104 self._test_args: tuple = () 105 self.command_actions: list[QAction] = [] 106 107 self._build_ui() 108 self._restore_settings() 109 self._update_editor_actions() 110 111 if start is not None: 112 self.open_manifest(start) 113 114 # ------------------------------------------------------------------ UI 115 def _build_ui(self): 116 """The menus, the panels and the editor, in this order (later steps 117 connect to what earlier ones made).""" 118 self._build_file_menu() 119 self._build_edit_menu() 120 self._build_view_menu() 121 options = self._build_options_panel() 122 self._build_commands() 123 self._build_debugging() 124 self._build_features_menu() 125 self._build_ai_menu() 126 files_box = self._build_project_files() 127 graph_tab = self._build_graph_panel() 128 self._build_editor_tabs() 129 self._build_outputs_view() 130 console_box = self._build_output_panel() 131 self._build_run_panels() 132 self._build_review_panel() 133 self._build_search_panels() 134 self._build_terminal() 135 self._build_git_panel() 136 self._build_complexity_panel() 137 self._link_file_lists() 138 self._build_central() 139 self._build_docks(options, files_box, graph_tab, console_box) 140 self.statusBar().showMessage("Ready") 141 self._set_project_enabled(False) 142 self._build_help_menu() 143 144 # ------------------------------------------------------------ settings 145 def _restore_settings(self): 146 s = self.settings 147 geometry = s.value("geometry") 148 if geometry is not None: 149 self.restoreGeometry(geometry) 150 layout = s.value("panels") # (where each panel was: docked, stacked, floating, hidden) 151 self._layout_restored = layout is not None and self.restore_layout( 152 layout, s.value("panels_front", [], type=list) or []) 153 # A layout saved before a panel existed: the panel goes in the stack of 154 # the one it's next to by default. 155 for name, beside in (("coverage", "tests"), ("git", "search"), ("complexity", "coverage"), 156 ("debug", "run")): 157 dock = self.docks[name] 158 if self._layout_restored and self.dockWidgetArea(dock) == Qt.NoDockWidgetArea: 159 self.addDockWidget(Qt.BottomDockWidgetArea, dock) 160 self.tabifyDockWidget(self.docks[beside], dock) 161 # Applied once the project's own [profiles] are in the list. 162 self._apply_build_options() 163 self.fpm_edit.setText(s.value("fpm", "")) 164 self.set_font_delta(s.value("font_delta", 0, type=int), announce=False) 165 self._set_graph_layout(s.value("graph_layout", next(iter(LAYOUTS)))) 166 self._set_graph_theme(s.value("graph_theme", next(iter(THEME_CHOICES)))) 167 self._set_graph_edges(s.value("graph_edges", next(iter(EDGE_STYLES)))) 168 169 def clear_settings_and_quit(self): 170 """Forget every saved setting and quit, so the next start is like the 171 first (after asking; nothing happens if a window can't close).""" 172 answer = QMessageBox.warning( 173 self, "Clear Settings and Quit", 174 "Clear all of VibeFPMGUI's saved settings and quit?\n\n" 175 "Recent projects, open files, window and panel layout, build options, View menu " 176 "choices and your spelling dictionary are all forgotten; the next start is like " 177 "the first. Your files are not touched.\n\n" 178 "Close any other VibeFPMGUI windows first: they save their settings when they close.", 179 QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.Cancel) 180 if answer != QMessageBox.Yes: 181 return 182 self._clearing_settings = True # (closeEvent: nothing saved) 183 if not self.close(): # (unsaved files: Cancel keeps the window and the settings) 184 self._clearing_settings = False 185 return 186 self.settings.sync() # (anything written while closing, then all of it removed) 187 self.settings.clear() 188 self.settings.sync() 189 QApplication.quit() 190 191 def closeEvent(self, event): 192 clearing = getattr(self, "_clearing_settings", False) 193 # (Before closing brings other panels to the front: e.g. stopping a review shows Build.) 194 front = self.front_panels() 195 if not clearing: 196 self._save_session() 197 # Every question first: answering Cancel or No leaves everything running. 198 if not self.maybe_save_configuration(): 199 event.ignore() 200 return 201 for editor in self.editors(): 202 self.tabs.setCurrentWidget(editor) 203 if not editor.maybe_save(): 204 event.ignore() 205 return 206 if self.busy(): 207 answer = QMessageBox.question(self, "fpm is running", 208 "An fpm command is still running. Stop it and quit?") 209 if answer != QMessageBox.Yes: 210 event.ignore() 211 return 212 self._stop_and_wait(2000) 213 for editor in self.editors(): 214 editor.close_down() 215 self.debug_session.stop() 216 self.lsp_client.stop() 217 self.search_panel.shutdown() 218 self.terminal_panel.shutdown() 219 self._shutdown_ai() 220 self._stop_coverage() 221 self._end_output(show_tail=False) 222 self._full_output_path.unlink(missing_ok=True) # (the full-output log: only for this run) 223 if clearing: 224 super().closeEvent(event) 225 return 226 s = self.settings 227 s.setValue("geometry", self.saveGeometry()) 228 s.setValue("fpm", self.fpm_edit.text()) 229 self._store_window_options(front) # (the project's options, or the app's settings) 230 self._write_config_file() 231 super().closeEvent(event)
The window. Each part of it is a mixin in vibefpmgui.window; this
class starts it, builds it (in order: _build_ui), and closes it.
59 def __init__(self, manifest_path: str | None = None, remember_last: bool = True): 60 """``remember_last`` False: an extra window (File → Open in New Window) 61 doesn't become the project reopened at the next start.""" 62 super().__init__() 63 self.remember_last = remember_last 64 self.setWindowTitle("VibeFPMGUI") 65 self.resize(1300, 850) 66 self.settings = LayeredSettings(make_settings()) 67 # The project opened first: its options (if its configuration file has 68 # them) are the settings the window is made with. 69 start = manifest_path or self.settings.value("last_manifest", "") 70 start = Path(start).resolve() if start and Path(start).is_file() else None 71 self._use_config_file(self._config_file_for(start) if start is not None else None, start) 72 themes.chosen_style = str(self.settings.value("app_style", themes.AUTOMATIC) or themes.AUTOMATIC) 73 themes.apply(str(self.settings.value("app_theme", themes.SYSTEM) or themes.SYSTEM)) # (before the widgets) 74 self._session_busy = False # restoring the open files (don't save meanwhile) 75 self.lsp_client = lsp.LspClient(self) # fortls, for the editor (see _update_language_server) 76 self.lsp_client.stopped.connect(self._language_server_stopped) 77 self.lsp_client.message.connect(lambda text: self.statusBar().showMessage(f"fortls: {text}", 8000)) 78 self.lsp_client.diagnostics.connect(self._lsp_diagnostics) 79 self._lsp_diagnostics_by_file: dict[Path, list] = {} # the latest, to show again when turned on 80 self.manifest_path: Path | None = None 81 self.graph: m.DependencyGraph | None = None 82 self.process: QProcess | None = None 83 self.parallel_tests: ParallelTests | None = None # tests run by us, several at once 84 self._refresh_after = False 85 self._stop_requested = False # Stop was pressed for the running process (see stop) 86 self._monitoring = False 87 self._partial = "" # output text after the last newline 88 # The running command's output as text: a character whose bytes arrive 89 # in two reads is decoded whole (see _begin_output). 90 self._decoder = codecs.getincrementaldecoder("utf-8")(errors="replace") 91 self._esc_tail = "" # color code cut off at the end of the last output chunk 92 self._full_output_path = Path(tempfile.gettempdir()) / f"vibefpmgui-output-{os.getpid()}.log" 93 self._full_output = None # the file the running command's whole output is saved to 94 self._shown_chars = 0 # how much of it the Output panel shows (see OUTPUT_SHOW_LIMIT) 95 self._hidden_chars = 0 96 self._hidden_lines = 0 97 self._tail: deque[str] = deque(maxlen=OUTPUT_TAIL_LINES) 98 self._tail_partial = "" 99 # None, or while running Test/Run: "list" (fpm ... --list), "marked" 100 # (fpm ... --runner <wrapper>) or "direct" (a built program, via the wrapper) 101 self._phase: str | None = None 102 self._captured: list[str] = [] # output of "fpm test --list" 103 self._held_blank = False # blank line that may precede a test marker 104 self._test_args: tuple = () 105 self.command_actions: list[QAction] = [] 106 107 self._build_ui() 108 self._restore_settings() 109 self._update_editor_actions() 110 111 if start is not None: 112 self.open_manifest(start)
remember_last False: an extra window (File → Open in New Window)
doesn't become the project reopened at the next start.
169 def clear_settings_and_quit(self): 170 """Forget every saved setting and quit, so the next start is like the 171 first (after asking; nothing happens if a window can't close).""" 172 answer = QMessageBox.warning( 173 self, "Clear Settings and Quit", 174 "Clear all of VibeFPMGUI's saved settings and quit?\n\n" 175 "Recent projects, open files, window and panel layout, build options, View menu " 176 "choices and your spelling dictionary are all forgotten; the next start is like " 177 "the first. Your files are not touched.\n\n" 178 "Close any other VibeFPMGUI windows first: they save their settings when they close.", 179 QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.Cancel) 180 if answer != QMessageBox.Yes: 181 return 182 self._clearing_settings = True # (closeEvent: nothing saved) 183 if not self.close(): # (unsaved files: Cancel keeps the window and the settings) 184 self._clearing_settings = False 185 return 186 self.settings.sync() # (anything written while closing, then all of it removed) 187 self.settings.clear() 188 self.settings.sync() 189 QApplication.quit()
Forget every saved setting and quit, so the next start is like the first (after asking; nothing happens if a window can't close).
191 def closeEvent(self, event): 192 clearing = getattr(self, "_clearing_settings", False) 193 # (Before closing brings other panels to the front: e.g. stopping a review shows Build.) 194 front = self.front_panels() 195 if not clearing: 196 self._save_session() 197 # Every question first: answering Cancel or No leaves everything running. 198 if not self.maybe_save_configuration(): 199 event.ignore() 200 return 201 for editor in self.editors(): 202 self.tabs.setCurrentWidget(editor) 203 if not editor.maybe_save(): 204 event.ignore() 205 return 206 if self.busy(): 207 answer = QMessageBox.question(self, "fpm is running", 208 "An fpm command is still running. Stop it and quit?") 209 if answer != QMessageBox.Yes: 210 event.ignore() 211 return 212 self._stop_and_wait(2000) 213 for editor in self.editors(): 214 editor.close_down() 215 self.debug_session.stop() 216 self.lsp_client.stop() 217 self.search_panel.shutdown() 218 self.terminal_panel.shutdown() 219 self._shutdown_ai() 220 self._stop_coverage() 221 self._end_output(show_tail=False) 222 self._full_output_path.unlink(missing_ok=True) # (the full-output log: only for this run) 223 if clearing: 224 super().closeEvent(event) 225 return 226 s = self.settings 227 s.setValue("geometry", self.saveGeometry()) 228 s.setValue("fpm", self.fpm_edit.text()) 229 self._store_window_options(front) # (the project's options, or the app's settings) 230 self._write_config_file() 231 super().closeEvent(event)
closeEvent(self, event: PySide6.QtGui.QCloseEvent, /) -> None
Inherited Members
- vibefpmgui.window.panels.PanelsMixin
- PANEL_AREAS
- LAYOUT_VERSION
- restore_layout
- default_layout
- reset_layout
- front_panels
- raise_panels
- show_panel
- panel_in_front
- showEvent
- show_terminal
- vibefpmgui.window.view.ViewMixin
- show_command_palette
- set_window_style
- set_theme
- set_syntax_style
- changeEvent
- FONT_DELTA_RANGE
- apply_toolbar_look
- set_font_delta
- choose_editor_font
- set_editor_font
- set_show_coverage
- set_highlight_matches
- set_blame_enabled
- set_minimap_enabled
- set_indentation
- set_word_wrap
- set_lsp_diagnostics_enabled
- set_spelling_enabled
- set_gremlins_enabled
- set_spelling_language
- add_spelling_word
- set_lint_enabled
- show_about
- vibefpmgui.window.project.ProjectMixin
- new_project
- browse_project
- open_project
- open_in_new_window
- open_manifest
- MAX_RECENT
- update_window_title
- recent_projects
- open_recent
- clear_recent_projects
- reload
- vibefpmgui.window.packages.PackagesMixin
- export_graph
- check_dependency_updates
- show_package
- open_package_manifest
- vibefpmgui.window.editor_tabs.EditorTabsMixin
- set_tab_rows
- editors
- current_editor
- browse_file
- preview_file
- open_file
- new_file
- close_all_editor_tabs
- rename_file
- delete_file
- close_all_editors
- close_editor
- vibefpmgui.window.editing.EditingMixin
- go_to_file
- show_punch_card
- align_cursors
- trim_trailing_whitespace
- set_trim_on_save
- convert_line_endings
- fix_lint_issues
- format_file
- vibefpmgui.window.debugging.DebuggingMixin
- start_debugging
- debug_arguments
- debug_named
- all_breakpoints
- toggle_breakpoint
- toggle_breakpoint_enabled
- any_enabled_breakpoint
- set_all_breakpoints_enabled
- remove_all_breakpoints
- toggle_listed_breakpoint
- set_breakpoint_enabled
- remove_breakpoint
- set_session_breakpoints
- session_breakpoints
- vibefpmgui.window.configs.ConfigsMixin
- configurations
- config_shown
- options_locked
- has_unsaved_configuration
- current_options
- free_options
- select_configuration
- reload_configurations
- save_configuration
- save_configuration_as
- edit_configuration
- cancel_editing
- maybe_save_configuration
- edit_configuration_as_new
- delete_configuration
- browse_configuration
- open_configuration
- vibefpmgui.window.options.ProjectOptionsMixin
- config_file
- config_path
- options_in_use
- reload_config_file
234def make_settings() -> QSettings: 235 """The app's saved settings. VIBEFPMGUI_SETTINGS=/path/file.ini uses a 236 separate file instead (e.g. for testing without touching your own).""" 237 path = os.environ.get("VIBEFPMGUI_SETTINGS") 238 if path: 239 return QSettings(path, QSettings.IniFormat) 240 return QSettings("VibeFPMGUI", "VibeFPMGUI")
The app's saved settings. VIBEFPMGUI_SETTINGS=/path/file.ini uses a separate file instead (e.g. for testing without touching your own).
246def qt_argv(argv: list[str]) -> list[str]: 247 """The command line for QApplication: the app's name in place of the 248 script's ("…/vibefpmgui/__main__.py" when started with python -m).""" 249 return [_APP_NAME, *argv[1:]]
The command line for QApplication: the app's name in place of the script's ("…/vibefpmgui/__main__.py" when started with python -m).
252def main(argv: list[str] | None = None) -> int: 253 argv = sys.argv if argv is None else argv 254 set_windows_app_id() # Windows: our own taskbar icon, not python.exe's 255 set_macos_bundle_name(_APP_NAME) # macOS: our name in the menu bar, not "Python" (before Qt starts) 256 # (argv[0] is the name Qt uses for macOS's About/Hide/Quit items: "__main__.py" otherwise.) 257 app = QApplication.instance() or QApplication(qt_argv(argv)) 258 set_macos_display_name(_APP_NAME) # and in the Dock, not "python3.14" 259 app.setApplicationName(_APP_NAME) 260 app.setWindowIcon(app_icon()) # also the Dock icon on macOS 261 app.setApplicationVersion(__version__) 262 path = next((a for a in argv[1:] if not a.startswith("-")), None) 263 if path and Path(path).is_dir(): 264 path = str(Path(path) / "fpm.toml") 265 # --new-window: opened from another window, so not the project to reopen next time 266 window = MainWindow(path, remember_last="--new-window" not in argv[1:]) 267 window.show() 268 return app.exec()