diff --git a/qt/src/InspectorWindow.cpp b/qt/src/InspectorWindow.cpp index 25bc3bd3f..10be6f161 100644 --- a/qt/src/InspectorWindow.cpp +++ b/qt/src/InspectorWindow.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -46,7 +47,15 @@ InspectorWindow::InspectorWindow(ghostty_surface_t surface) setWindowTitle(QStringLiteral("Ghastty Inspector")); setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); - resize(800, 600); + + // Restore the last saved size/position. macOS uses NSWindow's + // autosaveName; Qt has no built-in equivalent, so we persist via + // QSettings ourselves. First-run default matches the prior + // hard-coded 800x600. + QSettings s; + const QByteArray geom = + s.value(QStringLiteral("inspector/geometry")).toByteArray(); + if (!restoreGeometry(geom)) resize(800, 600); m_inspector = ghostty_surface_inspector(m_surface); @@ -150,6 +159,8 @@ void InspectorWindow::closeEvent(QCloseEvent *e) { // deleted only when the surface is destroyed. Stop the redraw // timer too — a hidden inspector has no work to do. if (m_timer) m_timer->stop(); + // Persist size + position so the next reveal restores them. + QSettings().setValue(QStringLiteral("inspector/geometry"), saveGeometry()); hide(); e->ignore(); } diff --git a/qt/src/MainWindow.cpp b/qt/src/MainWindow.cpp index 9d9727af0..82c38d21f 100644 --- a/qt/src/MainWindow.cpp +++ b/qt/src/MainWindow.cpp @@ -943,7 +943,23 @@ void MainWindow::setupLayerShell() { : ki == QLatin1String("none") ? LSW::KeyboardInteractivityNone : LSW::KeyboardInteractivityOnDemand); + // quick-terminal-screen: pick which output to anchor on. `main` + // (the default) maps to the primary screen; `mouse` to the screen + // under the cursor; `macos-menu-bar` is macOS-only and falls + // through to primary on Linux. LayerShellQt reads the QWindow's + // QScreen when ScreenFromQWindow is set, so we just set the + // window's screen before anchoring. + const QString screenMode = configString("quick-terminal-screen"); QScreen *screen = handle->screen(); + if (screenMode == QLatin1String("mouse")) { + if (QScreen *s = QGuiApplication::screenAt(QCursor::pos())) screen = s; + } else if (screenMode == QLatin1String("main") || + screenMode == QLatin1String("macos-menu-bar")) { + if (QScreen *s = QGuiApplication::primaryScreen()) screen = s; + } + if (screen && handle->screen() != screen) handle->setScreen(screen); + ls->setScreenConfiguration(LSW::ScreenFromQWindow); + const QSize scr = screen ? screen->size() : QSize(1920, 1080); // quick-terminal-size: primary is the edge-perpendicular extent. @@ -1268,12 +1284,66 @@ void MainWindow::playBellAudio() { m_bellPlayer->play(); } -// Refresh every window's chrome (tab-bar policy, colour scheme, blur) -// from the current s_config. +// Refresh every window's chrome from the current s_config: tab-bar +// policy, colour scheme, blur — plus window-level state that +// previously only applied at startup (window-decoration, fullscreen, +// maximize) and the quit-after-last-window-closed delay. void MainWindow::refreshChrome() { + // Refresh app-scoped state. quit-after-last-window-closed[-delay] + // can change the delay or the quitOnLastWindowClosed strategy at + // runtime; mirrors the calculation in initialize(). + if (s_config) { + bool quitAfter = true; + configGet(s_config, &quitAfter, "quit-after-last-window-closed"); + unsigned long long delayNs = 0; + configGet(s_config, &delayNs, "quit-after-last-window-closed-delay"); + s_quitDelayMs = quitAfter ? static_cast(delayNs / 1000000ULL) : 0; + QApplication::setQuitOnLastWindowClosed(quitAfter && s_quitDelayMs == 0); + } + for (MainWindow *w : s_windows) { w->applyWindowConfig(); w->applyBlur(); + + // Quick terminal is layer-shell-anchored and window flags don't + // apply; the rest of the per-window state is config-driven and + // only the static initialize() ever touched it before. This + // brings reload-time changes through to live windows. + if (w->m_quickTerminal) continue; + + // window-decoration: `none` → frameless, anything else → decorated. + // Toggling Qt::FramelessWindowHint hides+reshows the window, so + // gate on a real change. + const bool wantFrameless = + w->configString("window-decoration") == QLatin1String("none"); + const bool isFrameless = + w->windowFlags().testFlag(Qt::FramelessWindowHint); + if (wantFrameless != isFrameless) { + const bool wasVisible = w->isVisible(); + w->setWindowFlag(Qt::FramelessWindowHint, wantFrameless); + if (wasVisible) { + w->show(); + w->activateWindow(); + } + } + + // fullscreen / maximize: `fullscreen=true` wins over `maximize`. + // Setting back to a non-fullscreen window goes through showNormal + // first so the WM lets us out of fullscreen cleanly. + const QString fs = w->configString("fullscreen"); + const bool wantFullscreen = !fs.isEmpty() && fs != QLatin1String("false"); + const bool wantMax = w->configBool("maximize", false); + if (wantFullscreen) { + if (!w->isFullScreen()) w->showFullScreen(); + } else if (w->isFullScreen()) { + w->showNormal(); + } + if (!wantFullscreen) { + if (wantMax && !w->isMaximized()) w->showMaximized(); + // No "un-maximize on reload" path: a user who removed `maximize` + // from their config probably doesn't want their existing + // maximized window snapped back to its non-maximized geometry. + } } } @@ -1303,6 +1373,26 @@ void MainWindow::reloadConfigGlobal() { refreshChrome(); } +bool MainWindow::wantsInitialWindow() { + // s_config exists once the bootstrap window has called initialize(). + if (!s_config) return true; + bool wanted = true; + configGet(s_config, &wanted, "initial-window"); + return wanted; +} + +void MainWindow::closeInitialWindow() { + if (s_windows.isEmpty()) return; + // Close the bootstrap window without re-prompting; nothing has run + // in it yet so confirmCloseSurfaces would return true anyway, but + // m_skipCloseConfirm avoids any chrome flicker. closeAllWindows + // also resets the quit-on-last-window flag to keep the process + // alive until the user binds the quick-terminal shortcut. + MainWindow *first = s_windows.first(); + first->m_skipCloseConfirm = true; + first->close(); +} + QString MainWindow::configString(const char *key) const { const char *value = nullptr; if (!s_config || @@ -2353,15 +2443,22 @@ void MainWindow::onConfirmReadClipboard(void *ud, const char *str, void *state, while (cut > 0 && preview.at(cut - 1).isHighSurrogate()) --cut; preview = preview.left(cut) + QStringLiteral("…"); } - const auto reply = QMessageBox::warning( - sp->owner(), QStringLiteral("Confirm Paste"), - QStringLiteral("The text being pasted may be unsafe:\n\n%1\n\n" - "Paste anyway?") - .arg(preview), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + // Destructive Paste / Cancel buttons, default Cancel — + // mirrors the close-confirmation styling. + QMessageBox box(sp->owner()); + box.setIcon(QMessageBox::Warning); + box.setWindowTitle(QStringLiteral("Confirm Paste")); + box.setText(QStringLiteral("The text being pasted may be unsafe.")); + box.setInformativeText(preview); + QPushButton *paste = box.addButton(QStringLiteral("Paste"), + QMessageBox::DestructiveRole); + QPushButton *cancel = box.addButton(QStringLiteral("Cancel"), + QMessageBox::RejectRole); + box.setDefaultButton(cancel); + box.exec(); ghostty_surface_complete_clipboard_request( sp->surface(), content.constData(), state, - reply == QMessageBox::Yes); + box.clickedButton() == paste); }, Qt::QueuedConnection); } diff --git a/qt/src/MainWindow.h b/qt/src/MainWindow.h index b525bfb40..e98086570 100644 --- a/qt/src/MainWindow.h +++ b/qt/src/MainWindow.h @@ -74,6 +74,15 @@ public: // The live libghostty config (for keybind lookups, etc.). ghostty_config_t config() const { return s_config; } + // initial-window config plumbing. The libghostty app+config is + // built by the first MainWindow::initialize, so main.cpp opens a + // bootstrap window and asks afterwards whether the user actually + // wanted one — closing it cleanly if not. Headless start-up is + // how a user runs ghastty as a daemon for the global quick- + // terminal shortcut. + static bool wantsInitialWindow(); + static void closeInitialWindow(); + // UNDO / REDO close-tab/window. The libghostty actions carry no // payload — the apprt is responsible for tracking what was closed // and reviving it. macOS uses NSUndoManager; we keep a small bounded diff --git a/qt/src/main.cpp b/qt/src/main.cpp index 462645b9a..1a2456cc4 100644 --- a/qt/src/main.cpp +++ b/qt/src/main.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -28,6 +29,13 @@ int main(int argc, char **argv) { QApplication app(argc, argv); + // QSettings storage path keys: applicationName + organizationDomain. + // Used by the inspector window's geometry autosave (and any future + // QSettings-backed UI state) — the keys go to + // ~/.config/ghastty/ghastty.conf. + QCoreApplication::setApplicationName(QStringLiteral("ghastty")); + QCoreApplication::setOrganizationDomain(QStringLiteral("ghastty")); + // Match the installed ghastty.desktop: this becomes the Wayland app-id // (and X11 WM_CLASS), so the compositor associates the window with the // desktop entry — taskbar icon, launcher identity. @@ -53,12 +61,22 @@ int main(int argc, char **argv) { return 1; } - // The first window; further windows are opened on demand by the - // new_window action. Each window owns itself (WA_DeleteOnClose). + // initial-window: when false, start headless (no window opens at + // launch). Combined with quit-after-last-window-closed=false this + // is how a user runs ghastty as a daemon for the global quick- + // terminal shortcut. We need the libghostty app first, so spin up + // a temporary "config bootstrap" by opening + immediately closing + // a window — but cheaper: peek at the config directly here. + // ghostty_init has already run, but the libghostty app is built + // by the first MainWindow::initialize. There is no app-less + // accessor for the config, so we open the window and close if the + // bool is false. Cheaper alternative: set a static flag and have + // initialize() bail before show. if (!MainWindow::newWindow(nullptr)) { std::fprintf(stderr, "[ghastty] window initialization failed\n"); return 1; } + if (!MainWindow::wantsInitialWindow()) MainWindow::closeInitialWindow(); // Register global shortcuts via the XDG portal so the quick terminal // can be toggled while Ghostty is unfocused. Keys are assigned by the