qt: parity batch 1 — action handlers (B4, B6, B7, B11, B16)

Five fixes from PARITY.md tier 1, all in MainWindow:

  B4 — confirm-close-surface config now honored. Read the config
  string and respect 'false' (never prompt), 'true' (prompt only on
  live processes — the prior behavior), and 'always' (prompt
  regardless). Previously the config was ignored entirely.

  B6 — CLOSE_TAB now honors close_tab_mode. New closeTabsByMode()
  handles THIS / OTHER / RIGHT, single confirm covering every
  affected surface, descending iteration so removeTab doesn't shift
  indices. Previously every CLOSE_TAB collapsed to mode=THIS,
  silently dropping 'close other tabs' and 'close tabs to the right'
  keybinds.

  B7 — INITIAL_SIZE no longer halves the window on HiDPI. The
  action carries logical pixels; the prior code divided by
  devicePixelRatioF, producing half-size windows on 2x displays.

  B11 — RELOAD_CONFIG is now app-scoped. New static
  reloadConfigGlobal() runs the same logic but as a static, posted
  via post(qApp, ...) so the reload still happens if the source
  window is closed mid-dispatch. The config is process-wide already
  (statics in MainWindow); this just removes the per-window cancel
  semantics.

  B16 — COPY_TITLE_TO_CLIPBOARD now copies the source surface's
  tab title, not whatever tab is currently visible. Triggering the
  binding from a non-current tab used to copy the wrong title.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
ntomsic 2026-05-20 20:06:47 -05:00
parent b7e94c01cc
commit 33b5dee468
2 changed files with 90 additions and 23 deletions

View File

@ -475,6 +475,40 @@ void MainWindow::closeTab(int index) {
}
}
// Honor libghostty's close_tab_mode (THIS / OTHER / RIGHT) for the
// CLOSE_TAB action. macOS supports all three; GTK supports all three
// via adw.TabView.closeOtherPages / closePagesAfter.
void MainWindow::closeTabsByMode(GhosttySurface *src,
ghostty_action_close_tab_mode_e mode) {
const int srcTab = tabIndexForSurface(src);
if (srcTab < 0) return;
// Build the list of tab indices to close, in DESCENDING order so
// removeTab doesn't shift later indices out from under us.
QList<int> indices;
switch (mode) {
case GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS:
indices = {srcTab};
break;
case GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER:
for (int i = m_tabs->count() - 1; i >= 0; --i)
if (i != srcTab) indices.append(i);
break;
case GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT:
for (int i = m_tabs->count() - 1; i > srcTab; --i) indices.append(i);
break;
}
if (indices.isEmpty()) return;
// Single confirm prompt covering every surface affected.
QList<GhosttySurface *> affected;
for (int i : indices)
for (GhosttySurface *s : surfacesInTab(i)) affected.append(s);
if (!confirmCloseSurfaces(affected)) return;
for (int i : indices) closeTab(i);
}
void MainWindow::adoptTab(MainWindow *src, QWidget *page) {
const int srcIndex = src->m_tabs->indexOf(page);
if (srcIndex < 0 || src == this) return;
@ -549,8 +583,13 @@ void MainWindow::setTabTitleOverride(GhosttySurface *surface,
updateTabText(index);
}
void MainWindow::copyTitleToClipboard() {
const int tab = m_tabs->currentIndex();
void MainWindow::copyTitleToClipboard(GhosttySurface *src) {
// Per-surface: copy the title of the tab containing `src`, not
// whatever tab is currently visible. macOS does the same; otherwise
// a binding triggered from a non-current tab copies the wrong title.
// Falls back to the current tab if the source is unknown.
int tab = src ? tabIndexForSurface(src) : -1;
if (tab < 0) tab = m_tabs->currentIndex();
if (tab < 0) return;
const TabData data = m_tabs->tabBar()->tabData(tab).value<TabData>();
const QString title =
@ -586,10 +625,22 @@ void MainWindow::closeEvent(QCloseEvent *e) {
bool MainWindow::confirmCloseSurfaces(
const QList<GhosttySurface *> &surfaces) {
bool needsConfirm = false;
for (GhosttySurface *s : surfaces)
if (s->surface() && ghostty_surface_needs_confirm_quit(s->surface()))
needsConfirm = true;
// Honor the `confirm-close-surface` config:
// false -> never prompt
// true -> prompt only when libghostty says a process is running
// always -> always prompt, even for surfaces with no live process
// (libghostty Config.zig: ConfirmCloseSurface enum.)
const QString mode = configString("confirm-close-surface");
if (mode == QLatin1String("false")) return true;
bool needsConfirm = (mode == QLatin1String("always"));
if (!needsConfirm) {
for (GhosttySurface *s : surfaces)
if (s->surface() && ghostty_surface_needs_confirm_quit(s->surface())) {
needsConfirm = true;
break;
}
}
if (!needsConfirm) return true;
const auto choice = QMessageBox::question(
@ -975,7 +1026,10 @@ void MainWindow::refreshChrome() {
}
}
void MainWindow::reloadConfig() {
void MainWindow::reloadConfig() { reloadConfigGlobal(); }
void MainWindow::reloadConfigGlobal() {
if (!s_app) return;
// Re-read the config from disk in the same order as initialize().
ghostty_config_t config = ghostty_config_new();
ghostty_config_load_default_files(config);
@ -1238,13 +1292,15 @@ bool MainWindow::onAction(ghostty_app_t, ghostty_target_s target,
return true;
}
case GHOSTTY_ACTION_CLOSE_TAB:
if (src)
post(win, [winp, srcp]() {
if (!winp || !srcp) return;
if (winp->confirmCloseSurfaces({srcp})) winp->removeSurface(srcp);
});
case GHOSTTY_ACTION_CLOSE_TAB: {
if (!src) return false;
const ghostty_action_close_tab_mode_e mode = action.action.close_tab_mode;
post(win, [winp, srcp, mode]() {
if (!winp || !srcp) return;
winp->closeTabsByMode(srcp, mode);
});
return true;
}
case GHOSTTY_ACTION_SET_TITLE: {
const char *title = action.action.set_title.title;
@ -1278,8 +1334,8 @@ bool MainWindow::onAction(ghostty_app_t, ghostty_target_s target,
}
case GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD:
post(win, [winp]() {
if (winp) winp->copyTitleToClipboard();
post(win, [winp, srcp]() {
if (winp) winp->copyTitleToClipboard(srcp);
});
return true;
@ -1408,9 +1464,11 @@ bool MainWindow::onAction(ghostty_app_t, ghostty_target_s target,
}
case GHOSTTY_ACTION_RELOAD_CONFIG:
post(win, [winp]() {
if (winp) winp->reloadConfig();
});
// Reload is app-scoped (the config is process-wide). Post to
// qApp instead of the originating window so the reload still
// happens if the window that issued the action is closed
// between the dispatch and the queued slot.
post(qApp, []() { MainWindow::reloadConfigGlobal(); });
return true;
case GHOSTTY_ACTION_CONFIG_CHANGE:
@ -1425,10 +1483,11 @@ bool MainWindow::onAction(ghostty_app_t, ghostty_target_s target,
const ghostty_action_initial_size_s sz = action.action.initial_size;
post(win, [winp, sz]() {
if (!winp) return;
// The action carries device pixels; resize() takes logical.
const double dpr = winp->devicePixelRatioF();
const QSize logical(static_cast<int>(sz.width / dpr),
static_cast<int>(sz.height / dpr));
// The action carries logical pixels; resize() takes the same.
// The previous code divided by devicePixelRatioF, halving the
// window on a 2x display.
const QSize logical(static_cast<int>(sz.width),
static_cast<int>(sz.height));
winp->m_defaultWindowSize = logical; // for RESET_WINDOW_SIZE
winp->resize(logical);
});

View File

@ -98,6 +98,9 @@ private:
static void frame();
void closeTab(int index);
// Honor close-tab-mode (THIS / OTHER / RIGHT) from libghostty.
void closeTabsByMode(GhosttySurface *src,
ghostty_action_close_tab_mode_e mode);
// Tear tab `index` out into a new window (tabTornOff signal).
void detachTab(int index);
// Move `page` (a tab and its surfaces) from `src` into this window.
@ -129,10 +132,15 @@ private:
// while set, SET_TITLE no longer changes the tab text.
void setTabTitleOverride(GhosttySurface *surface, const QString &title);
// Copy the current tab's effective title to the clipboard.
void copyTitleToClipboard();
void copyTitleToClipboard(GhosttySurface *src);
// Rebuild the config from disk and push it to libghostty.
void reloadConfig();
// App-scoped reload entry point. The config is process-wide (statics
// in this class), so reload from any window has the same effect; the
// RELOAD_CONFIG action posts to qApp via this static so the reload
// can't be cancelled by the source window closing mid-dispatch.
static void reloadConfigGlobal();
// Refresh every window's chrome from the current config (used after a
// reload and on the CONFIG_CHANGE notification).
static void refreshChrome();