qt: phase 1.3b — move clipboard + closeSurface callbacks to GhosttyApp
Final piece of phase 1: the four remaining libghostty runtime
callbacks (onReadClipboard, onConfirmReadClipboard, onWriteClipboard,
onCloseSurface) plus the surfaceAlive predicate that gates them all
move from MainWindow to GhosttyApp.
- All four callback bodies move verbatim. The only edits are
`surfaceAlive(...)` → `instance().surfaceAlive(...)` (the
predicate is now a member of the singleton).
- surfaceAlive itself iterates m_windows on the singleton, calling
a new public MainWindow::ownsSurface(GhosttySurface*) accessor
instead of directly poking the private m_surfaces field.
- ensureInitialized now registers GhosttyApp::onReadClipboard et
al. with libghostty's runtime config bundle.
After this commit, MainWindow::onAction is the only libghostty
runtime callback still living on MainWindow; phase 2 retires it for
an ActionDispatcher.
Phase 1 closeout summary:
- Phase 1.0: libghostty handles + bring-up/teardown
- Phase 1.1: window registry (s_windows)
- Phase 1.2: frame timer + quit timer + onWakeup + tickPending
- Phase 1.3a: quickTerminal pointer + visibility / quick-term toggles
- Phase 1.3b: read/confirm/write clipboard + closeSurface
The s_app / s_config / s_needsPremultiply / s_quitDelayMs mirrors on
MainWindow are deliberately retained — they're read by ~50 call
sites across MainWindow.cpp and retiring them is a focused phase 1.4
follow-up that can land independently.
Build verification: not run on this Mac. Needs a docker compile
before merge.
Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
parent
7ea8c87a63
commit
f669aa0d7a
|
|
@ -742,7 +742,7 @@ void MainWindow::showTabContextMenu(int index, const QPoint &globalPos) {
|
|||
QAction *aRename = menu.addAction(QStringLiteral("Rename Tab…"));
|
||||
|
||||
QAction *chosen = menu.exec(globalPos);
|
||||
if (!chosen || !surfaceAlive(src)) return;
|
||||
if (!chosen || !GhosttyApp::instance().surfaceAlive(src)) return;
|
||||
if (chosen == aClose)
|
||||
closeTabsByMode(src, GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS);
|
||||
else if (chosen == aOther)
|
||||
|
|
@ -1952,13 +1952,8 @@ void MainWindow::toggleSplitZoom(GhosttySurface *surface) {
|
|||
}
|
||||
|
||||
// --- libghostty runtime callbacks ------------------------------------
|
||||
|
||||
bool MainWindow::surfaceAlive(GhosttySurface *s) {
|
||||
if (!s) return false;
|
||||
for (MainWindow *w : GhosttyApp::instance().windows())
|
||||
if (w->m_surfaces.contains(s)) return true;
|
||||
return false;
|
||||
}
|
||||
// All five non-action callbacks (onWakeup + the clipboard / close
|
||||
// quartet) live on GhosttyApp now. onAction stays here until phase 2.
|
||||
|
||||
// Map a libghostty mouse shape to the nearest Qt cursor.
|
||||
static Qt::CursorShape mouseShapeToCursor(ghostty_action_mouse_shape_e s) {
|
||||
|
|
@ -2743,104 +2738,5 @@ bool MainWindow::onAction(ghostty_app_t, ghostty_target_s target,
|
|||
}
|
||||
}
|
||||
|
||||
bool MainWindow::onReadClipboard(void *ud, ghostty_clipboard_e loc,
|
||||
void *state) {
|
||||
// surface userdata. Called synchronously by libghostty when a
|
||||
// surface needs clipboard contents (paste). This runs on the GUI
|
||||
// thread by construction: every libghostty entry point that
|
||||
// surfaces a paste lives behind ghostty_app_tick, which the
|
||||
// process-wide frame timer drives — and that timer is on the GUI
|
||||
// thread. QClipboard is GUI-thread-only, so reading directly here
|
||||
// is safe; surfaceAlive still validates the pointer in case a
|
||||
// surface is mid-destruction on this same thread.
|
||||
auto *surface = static_cast<GhosttySurface *>(ud);
|
||||
if (!surfaceAlive(surface) || !surface->surface()) return false;
|
||||
|
||||
const QClipboard::Mode mode = loc == GHOSTTY_CLIPBOARD_SELECTION
|
||||
? QClipboard::Selection
|
||||
: QClipboard::Clipboard;
|
||||
const QByteArray text = QGuiApplication::clipboard()->text(mode).toUtf8();
|
||||
ghostty_surface_complete_clipboard_request(surface->surface(),
|
||||
text.constData(), state, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::onConfirmReadClipboard(void *ud, const char *str, void *state,
|
||||
ghostty_clipboard_request_e) {
|
||||
// libghostty asks for confirmation when a paste looks unsafe. The
|
||||
// dialog MUST be deferred: this callback runs inside libghostty, and a
|
||||
// modal dialog here spins a nested event loop that re-enters libghostty
|
||||
// through the render tick — a crash/freeze. `state` is a completion
|
||||
// token valid until used; `str` is not, so copy it.
|
||||
auto *surface = static_cast<GhosttySurface *>(ud);
|
||||
if (!surfaceAlive(surface) || !surface->surface()) return;
|
||||
|
||||
QPointer<GhosttySurface> sp(surface);
|
||||
const QByteArray content(str);
|
||||
QMetaObject::invokeMethod(
|
||||
surface->owner(),
|
||||
[sp, content, state]() {
|
||||
if (!sp || !sp->surface()) return;
|
||||
QString preview = QString::fromUtf8(content);
|
||||
// Truncate by code unit but back off to a non-surrogate boundary
|
||||
// so we don't slice a surrogate pair half.
|
||||
if (preview.size() > 200) {
|
||||
int cut = 200;
|
||||
while (cut > 0 && preview.at(cut - 1).isHighSurrogate()) --cut;
|
||||
preview = preview.left(cut) + QStringLiteral("…");
|
||||
}
|
||||
// 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,
|
||||
box.clickedButton() == paste);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void MainWindow::onWriteClipboard(void *ud, ghostty_clipboard_e loc,
|
||||
const ghostty_clipboard_content_s *content,
|
||||
size_t n, bool) {
|
||||
if (n == 0 || !content[0].data) return;
|
||||
auto *surface = static_cast<GhosttySurface *>(ud);
|
||||
if (!surfaceAlive(surface)) return;
|
||||
|
||||
const QClipboard::Mode mode = loc == GHOSTTY_CLIPBOARD_SELECTION
|
||||
? QClipboard::Selection
|
||||
: QClipboard::Clipboard;
|
||||
const QString text = QString::fromUtf8(content[0].data);
|
||||
// The clipboard is process-global; route via qApp so a window dying
|
||||
// mid-flight does not strand the write.
|
||||
QMetaObject::invokeMethod(
|
||||
qApp,
|
||||
[text, mode]() { QGuiApplication::clipboard()->setText(text, mode); },
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void MainWindow::onCloseSurface(void *ud, bool) {
|
||||
// surface userdata. Deferred out of this callback so the confirm
|
||||
// dialog cannot spin a nested event loop back into libghostty.
|
||||
auto *surface = static_cast<GhosttySurface *>(ud);
|
||||
if (!surfaceAlive(surface)) return;
|
||||
MainWindow *self = surface->owner();
|
||||
QPointer<MainWindow> selfp(self);
|
||||
QPointer<GhosttySurface> sp(surface);
|
||||
QMetaObject::invokeMethod(
|
||||
self,
|
||||
[selfp, sp]() {
|
||||
if (!selfp || !sp) return;
|
||||
if (selfp->confirmCloseSurfaces({sp})) selfp->removeSurface(sp);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
// onReadClipboard / onConfirmReadClipboard / onWriteClipboard /
|
||||
// onCloseSurface all moved to GhosttyApp in phase 1.3b.
|
||||
|
|
|
|||
|
|
@ -116,6 +116,13 @@ public:
|
|||
// private m_surfaces member.
|
||||
const QList<GhosttySurface *> &surfaces() const { return m_surfaces; }
|
||||
|
||||
// Whether `s` is one of this window's surfaces. Used by
|
||||
// GhosttyApp::surfaceAlive to validate libghostty userdata pointers
|
||||
// against a destruction race on worker-thread callbacks.
|
||||
bool ownsSurface(GhosttySurface *s) const {
|
||||
return m_surfaces.contains(s);
|
||||
}
|
||||
|
||||
protected:
|
||||
bool event(QEvent *) override;
|
||||
void showEvent(QShowEvent *) override;
|
||||
|
|
@ -232,24 +239,14 @@ private:
|
|||
// / back into the splitter tree.
|
||||
void toggleSplitZoom(GhosttySurface *surface);
|
||||
|
||||
// Runtime callbacks dispatched by libghostty. action is app-level
|
||||
// (routed via the target surface or the GhosttyApp window
|
||||
// registry); clipboard/close carry the surface userdata. wakeup
|
||||
// moved to GhosttyApp::onWakeup in phase 1.2.
|
||||
// The libghostty action callback. Stays here because its switch
|
||||
// body still needs private MainWindow access; phase 2 retires it
|
||||
// for an ActionDispatcher. The other five runtime callbacks
|
||||
// (onWakeup + clipboard quartet) live on GhosttyApp.
|
||||
static bool onAction(ghostty_app_t, ghostty_target_s, ghostty_action_s);
|
||||
static bool onReadClipboard(void *ud, ghostty_clipboard_e, void *state);
|
||||
static void onConfirmReadClipboard(void *ud, const char *, void *state,
|
||||
ghostty_clipboard_request_e);
|
||||
static void onWriteClipboard(void *ud, ghostty_clipboard_e,
|
||||
const ghostty_clipboard_content_s *, size_t,
|
||||
bool);
|
||||
static void onCloseSurface(void *ud, bool process_active);
|
||||
|
||||
// True if `s` is still owned by some live MainWindow. The surface
|
||||
// userdata callbacks above use this to validate a libghostty-supplied
|
||||
// pointer before dereferencing — a worker-thread callback can race
|
||||
// the GhosttySurface destructor.
|
||||
static bool surfaceAlive(GhosttySurface *s);
|
||||
// surfaceAlive moved to GhosttyApp::surfaceAlive (it iterates the
|
||||
// live window registry, which is owned by the singleton).
|
||||
|
||||
TabWidget *m_tabs = nullptr;
|
||||
QList<GhosttySurface *> m_surfaces; // every live surface in this window
|
||||
|
|
|
|||
|
|
@ -4,11 +4,16 @@
|
|||
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QEvent>
|
||||
#include <QFile>
|
||||
#include <QGuiApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QMetaObject>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
|
|
@ -73,15 +78,14 @@ bool GhosttyApp::ensureInitialized() {
|
|||
// surface, and app-level actions via the GhosttyApp window registry.
|
||||
rt.userdata = nullptr;
|
||||
rt.supports_selection_clipboard = true;
|
||||
// onWakeup migrated in phase 1.2; the rest still live on
|
||||
// MainWindow and migrate alongside the action dispatcher in
|
||||
// phase 1.3+.
|
||||
// onAction stays on MainWindow until phase 2 introduces the
|
||||
// ActionDispatcher; the rest are owned by GhosttyApp.
|
||||
rt.wakeup_cb = GhosttyApp::onWakeup;
|
||||
rt.action_cb = MainWindow::onAction;
|
||||
rt.read_clipboard_cb = MainWindow::onReadClipboard;
|
||||
rt.confirm_read_clipboard_cb = MainWindow::onConfirmReadClipboard;
|
||||
rt.write_clipboard_cb = MainWindow::onWriteClipboard;
|
||||
rt.close_surface_cb = MainWindow::onCloseSurface;
|
||||
rt.read_clipboard_cb = GhosttyApp::onReadClipboard;
|
||||
rt.confirm_read_clipboard_cb = GhosttyApp::onConfirmReadClipboard;
|
||||
rt.write_clipboard_cb = GhosttyApp::onWriteClipboard;
|
||||
rt.close_surface_cb = GhosttyApp::onCloseSurface;
|
||||
|
||||
m_app = ghostty_app_new(&rt, m_config);
|
||||
if (!m_app) {
|
||||
|
|
@ -260,3 +264,113 @@ void GhosttyApp::teardown() {
|
|||
}
|
||||
m_needsPremultiply = false;
|
||||
}
|
||||
|
||||
bool GhosttyApp::surfaceAlive(GhosttySurface *s) const {
|
||||
if (!s) return false;
|
||||
for (MainWindow *w : m_windows)
|
||||
if (w->ownsSurface(s)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GhosttyApp::onReadClipboard(void *ud, ghostty_clipboard_e loc,
|
||||
void *state) {
|
||||
// surface userdata. Called synchronously by libghostty when a
|
||||
// surface needs clipboard contents (paste). This runs on the GUI
|
||||
// thread by construction: every libghostty entry point that
|
||||
// surfaces a paste lives behind ghostty_app_tick, which the
|
||||
// process-wide frame timer drives — and that timer is on the GUI
|
||||
// thread. QClipboard is GUI-thread-only, so reading directly here
|
||||
// is safe; surfaceAlive still validates the pointer in case a
|
||||
// surface is mid-destruction on this same thread.
|
||||
auto *surface = static_cast<GhosttySurface *>(ud);
|
||||
if (!instance().surfaceAlive(surface) || !surface->surface()) return false;
|
||||
|
||||
const QClipboard::Mode mode = loc == GHOSTTY_CLIPBOARD_SELECTION
|
||||
? QClipboard::Selection
|
||||
: QClipboard::Clipboard;
|
||||
const QByteArray text = QGuiApplication::clipboard()->text(mode).toUtf8();
|
||||
ghostty_surface_complete_clipboard_request(surface->surface(),
|
||||
text.constData(), state, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GhosttyApp::onConfirmReadClipboard(void *ud, const char *str,
|
||||
void *state,
|
||||
ghostty_clipboard_request_e) {
|
||||
// libghostty asks for confirmation when a paste looks unsafe. The
|
||||
// dialog MUST be deferred: this callback runs inside libghostty,
|
||||
// and a modal dialog here spins a nested event loop that re-enters
|
||||
// libghostty through the render tick — a crash/freeze. `state` is
|
||||
// a completion token valid until used; `str` is not, so copy it.
|
||||
auto *surface = static_cast<GhosttySurface *>(ud);
|
||||
if (!instance().surfaceAlive(surface) || !surface->surface()) return;
|
||||
|
||||
QPointer<GhosttySurface> sp(surface);
|
||||
const QByteArray content(str);
|
||||
QMetaObject::invokeMethod(
|
||||
surface->owner(),
|
||||
[sp, content, state]() {
|
||||
if (!sp || !sp->surface()) return;
|
||||
QString preview = QString::fromUtf8(content);
|
||||
// Truncate by code unit but back off to a non-surrogate
|
||||
// boundary so we don't slice a surrogate pair half.
|
||||
if (preview.size() > 200) {
|
||||
int cut = 200;
|
||||
while (cut > 0 && preview.at(cut - 1).isHighSurrogate()) --cut;
|
||||
preview = preview.left(cut) + QStringLiteral("…");
|
||||
}
|
||||
// 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,
|
||||
box.clickedButton() == paste);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void GhosttyApp::onWriteClipboard(void *ud, ghostty_clipboard_e loc,
|
||||
const ghostty_clipboard_content_s *content,
|
||||
size_t n, bool) {
|
||||
if (n == 0 || !content[0].data) return;
|
||||
auto *surface = static_cast<GhosttySurface *>(ud);
|
||||
if (!instance().surfaceAlive(surface)) return;
|
||||
|
||||
const QClipboard::Mode mode = loc == GHOSTTY_CLIPBOARD_SELECTION
|
||||
? QClipboard::Selection
|
||||
: QClipboard::Clipboard;
|
||||
const QString text = QString::fromUtf8(content[0].data);
|
||||
// The clipboard is process-global; route via qApp so a window
|
||||
// dying mid-flight does not strand the write.
|
||||
QMetaObject::invokeMethod(
|
||||
qApp,
|
||||
[text, mode]() { QGuiApplication::clipboard()->setText(text, mode); },
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void GhosttyApp::onCloseSurface(void *ud, bool) {
|
||||
// surface userdata. Deferred out of this callback so the confirm
|
||||
// dialog cannot spin a nested event loop back into libghostty.
|
||||
auto *surface = static_cast<GhosttySurface *>(ud);
|
||||
if (!instance().surfaceAlive(surface)) return;
|
||||
MainWindow *self = surface->owner();
|
||||
QPointer<MainWindow> selfp(self);
|
||||
QPointer<GhosttySurface> sp(surface);
|
||||
QMetaObject::invokeMethod(
|
||||
self,
|
||||
[selfp, sp]() {
|
||||
if (!selfp || !sp) return;
|
||||
if (selfp->confirmCloseSurfaces({sp})) selfp->removeSurface(sp);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,8 +90,19 @@ public:
|
|||
void setQuitDelayMs(int ms) { m_quitDelayMs = ms; }
|
||||
|
||||
// ---- libghostty runtime callbacks (registered in ensureInitialized).
|
||||
// Phase 1.2 ports onWakeup; the others still live on MainWindow.
|
||||
static void onWakeup(void *ud);
|
||||
static bool onReadClipboard(void *ud, ghostty_clipboard_e, void *state);
|
||||
static void onConfirmReadClipboard(void *ud, const char *, void *state,
|
||||
ghostty_clipboard_request_e);
|
||||
static void onWriteClipboard(void *ud, ghostty_clipboard_e,
|
||||
const ghostty_clipboard_content_s *, size_t,
|
||||
bool);
|
||||
static void onCloseSurface(void *ud, bool process_active);
|
||||
|
||||
// True if the surface pointer (a libghostty userdata) is still owned
|
||||
// by a live MainWindow. Worker-thread callbacks use this to gate
|
||||
// against a destruction race.
|
||||
bool surfaceAlive(GhosttySurface *s) const;
|
||||
|
||||
private:
|
||||
GhosttyApp() = default;
|
||||
|
|
|
|||
Loading…
Reference in New Issue