qt: parity tier 3 batch 5 — config reload + inspector autosave + initial-window

Five fixes:

  B12 / B47 — refreshChrome now propagates window-decoration,
  fullscreen, and maximize to running windows on reload-config.
  Toggling Qt::FramelessWindowHint hides+reshows the window so
  we gate on a real change. Fullscreen exits via showNormal()
  before reverting; maximize is one-way (don't undo a user's
  manual maximize on reload).

  B48 — `quit-after-last-window-closed[-delay]` is now refreshed
  on reload too. Previously s_quitDelayMs was cached at init, so
  changing the delay required a process restart.

  B43 / C11 — quick-terminal-screen honored. `main` (default) →
  primary screen; `mouse` → screen under cursor. Implemented by
  setting handle->setScreen() before LayerShellQt's
  setScreenConfiguration(ScreenFromQWindow). `macos-menu-bar`
  falls through to primary on Linux (it's a macOS-only mode).

  B49 — Inspector window geometry autosaves via QSettings. First
  reveal restores the prior size+position; the prior 800x600
  hard-coded resize() is the first-run fallback. main.cpp now
  sets QCoreApplication::setApplicationName + setOrganizationDomain
  so QSettings has stable storage keys.

  C20 — `initial-window: false` honored. main.cpp creates a
  bootstrap window so libghostty's app+config exists, then closes
  it if the user opted out. New helpers:
  MainWindow::wantsInitialWindow / closeInitialWindow.

Polish: confirm-paste dialog uses destructive Paste / Cancel
buttons, matching the I1 close-confirmation styling. C16
(clipboard-trim-trailing-spaces) is silently honored — libghostty
applies it inside Surface.zig before invoking write_clipboard_cb.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
ntomsic 2026-05-21 09:17:42 -05:00
parent bfd39a4dd9
commit 6d700c36b3
4 changed files with 147 additions and 12 deletions

View File

@ -11,6 +11,7 @@
#include <QOpenGLFramebufferObject>
#include <QOpenGLFunctions>
#include <QPainter>
#include <QSettings>
#include <QSurfaceFormat>
#include <QTimer>
#include <QWheelEvent>
@ -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();
}

View File

@ -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<int>(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);
}

View File

@ -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

View File

@ -1,6 +1,7 @@
#include <cstdio>
#include <QApplication>
#include <QCoreApplication>
#include <QIcon>
#include <QSurfaceFormat>
@ -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