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.

MainWindow(manifest_path: str | None = None, remember_last: bool = True)
 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.

remember_last
settings
lsp_client
manifest_path: pathlib.Path | None
process: PySide6.QtCore.QProcess | None
command_actions: list[PySide6.QtGui.QAction]
def clear_settings_and_quit(self):
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).

def closeEvent(self, event):
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

staticMetaObject = PySide6.QtCore.QMetaObject("MainWindow" inherits "QMainWindow": )
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
package_menu
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
navigate_to
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.language.LanguageMixin
set_fortls_enabled
go_to_symbol
vibefpmgui.window.search.SearchMixin
find_in_files
vibefpmgui.window.git.GitMixin
discard_git_change
refresh_git
AWAY_SECONDS
vibefpmgui.window.running.RunningMixin
busy
effective_compiler
build_arguments
run_fpm
stop
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
vibefpmgui.window.build_outputs.BuildOutputsMixin
refresh_build_outputs
delete_configurations
run_valgrind
run_program_in_terminal
run_program
show_compile_command
vibefpmgui.window.output.OutputMixin
show_test_output
show_build_details
show_all_output
vibefpmgui.window.testing.TestingMixin
run_tests
run_programs
vibefpmgui.window.complexity.ComplexityMixin
refresh_complexity
vibefpmgui.window.ai.AIMixin
start_review
stop_review
stop_ollama
vibefpmgui.window.ai_chat.AIChatMixin
EXPLAINABLE
explain_problem
send_chat
edit_with_ai
fix_with_ai
new_chat
clear_ai_history
go_to_finding
fix_finding
show_findings_in_editor
accept_edit
accept_all_edits
reject_edit
reject_all_edits
show_edit
def make_settings() -> PySide6.QtCore.QSettings:
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).

def qt_argv(argv: list[str]) -> list[str]:
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).

def main(argv: list[str] | None = None) -> int:
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()