#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DevDriver.h" // Qt defines `emit` as an empty macro, and ghostty's C headers declare struct // fields by that name. The collision is silent on clang, which only warns and // suppresses the warning inside a system header, so the struct loses the field. // gcc treats it as a hard error. Suspend the macro while ghostty's headers // parse, exactly as TerminalQuickItem.h does. Nothing below uses the keyword. #undef emit #include #include #include #include // PNG decoder for the Kitty graphics protocol. ghostty-vt parses APC // sequences and stores images, but it needs an external decoder to turn // raw PNG bytes into RGBA pixels. We use QImage (QtGui is already linked). // The decoded buffer is allocated through ghostty's allocator and the // library takes ownership, freeing it with the same allocator. static bool decodePng(void* userdata, const GhosttyAllocator* allocator, const uint8_t* data, size_t data_len, GhosttySysImage* out) { (void)userdata; QImage img = QImage::fromData(data, static_cast(data_len), "PNG"); if (img.isNull()) { return false; } img = img.convertToFormat(QImage::Format_RGBA8888); // Premultiply alpha: our renderer blends with src=One (premultiplied). // PNG is straight alpha; without this, image edges over-brighten. // Rows are independent: walk each scanline as a span of exactly `width` // pixels (never the padding past the row), and premultiply in place. const auto rows = std::views::iota(0, img.height()) | std::views::transform([&img](int y) { return std::span( reinterpret_cast(img.scanLine(y)), // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) static_cast(img.width())); }); for (const std::span row : rows) { for (QRgb& px : row) { const int a = qAlpha(px); px = qRgba(qRed(px) * a / 255, qGreen(px) * a / 255, qBlue(px) * a / 255, a); } } const size_t len = static_cast(img.width()) * static_cast(img.height()) * 4; uint8_t* buf = ghostty_alloc(allocator, len); if (buf == nullptr) { return false; } std::memcpy(buf, img.constBits(), len); out->width = static_cast(img.width()); out->height = static_cast(img.height()); out->data = buf; out->data_len = len; return true; } // Image provider for the pane-drag snapshot. TerminalQuickItem delivers the // dragged pane's QImage via the snapshotReady signal (QML thread), and // PaneDrag stores it here so the drag Image can reference it by URL // ("image://snapshot/current"). The read/write lock guards the hand-off // between the snapshotReady writer and the image-request read. // // QQuickImageProvider already derives from QObject (via QQmlImageProviderBase); // give it a meta-object with Q_OBJECT so the Q_INVOKABLE setImage is visible // to QML as a callable method. class SnapshotImageProvider : public QQuickImageProvider { Q_OBJECT public: SnapshotImageProvider() : QQuickImageProvider(QQuickImageProvider::Image) {} QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override { QReadLocker lock(&m_lock); Q_UNUSED(id); Q_UNUSED(requestedSize); if (size != nullptr) { *size = m_image.size(); } return m_image; } Q_INVOKABLE void setImage(const QImage& image) { QWriteLocker lock(&m_lock); m_image = image; } private: QImage m_image; QReadWriteLock m_lock; }; #include "main.moc" int main(int argc, char *argv[]) { // Pin the surface format to sRGB before the GUI app exists. The QRhi text // shader reads sRGB-encoded colors and writes linear values; the backbuffer // must be tagged sRGB so the GPU encodes back on display. On macOS Metal // and most Vulkan drivers this picks RGBA8_sRGB; X11 EGL on some Linux // distros defaults to RGBA8 if we do not set this explicitly. QSurfaceFormat fmt = QSurfaceFormat::defaultFormat(); fmt.setAlphaBufferSize(8); fmt.setColorSpace(QColorSpace::SRgb); QSurfaceFormat::setDefaultFormat(fmt); QQuickWindow::setDefaultAlphaBuffer(true); // Wheel-momentum scrolling. Qt 6.11 Flickable has no QML switch for the // wheel-decay rate: wheelDeceleration is a private member whose default // (15000) disables wheel momentum and steps each notch a fixed 72px // (wheelScrollLines()*24) with no flick. The only way to select the // momentum path is this environment variable, read once in // QQuickFlickablePrivate's constructor from the process environment, so // it applies to every Flickable/ListView in the app (dialogs, settings, // font lists, palette) — no per-view work needed. // The feel: one notch glides 72px (~968 px/s at deceleration), and the // velocity cap (maximumFlickVelocity) bounds fast rolls: at 6500 a // capped notch glides 307px (1.6 cards), too heavy. 12000 cuts that to // ~167px per capped notch and a fast three-notch roll to ~350px (~1.9 // cards); single notches stay 72px. // NOTE: the sidebar does NOT use this path. Its Flickable owns a // WheelHandler (Sidebar.qml) that consumes wheel events first and maps // trackpad pixelDelta 1:1 plus fixed wheel steps itself, so this env // only affects the remaining Flickables. // Set BEFORE QGuiApplication exists so the first Flickable picks it up. qputenv("QT_QUICK_FLICKABLE_WHEEL_DECELERATION", "12000"); // Native text rendering bypasses the Metal-killing distance-field glyph // cache (QSGRhiDistanceFieldGlyphCache). Required on Apple Silicon. QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); QGuiApplication app(argc, argv); QQuickStyle::setStyle(QStringLiteral("Basic")); // Install the Kitty graphics PNG decoder before any terminal exists. // ghostty-vt needs it to turn APC-transmitted PNG bytes into RGBA. (void)ghostty_sys_set( GHOSTTY_SYS_OPT_DECODE_PNG, reinterpret_cast(&decodePng)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) // CLI: -e/--execute (repeatable) feeds lines to the PTY after spawn; // -c/--command is the existing single-program path. QCommandLineParser parser; parser.setApplicationDescription( QStringLiteral("Xenomorphic terminal emulator")); parser.addHelpOption(); parser.addVersionOption(); QCommandLineOption execOpt(QStringList{"e", "execute"}, QStringLiteral("Execute a shell line after the PTY ready (repeatable)." " Each value is written to the shell followed by '\\n'."), QStringLiteral("line")); parser.addOption(execOpt); QCommandLineOption cmdOpt(QStringList{"c", "command"}, QStringLiteral("Run a single program instead of the login shell" " (space-split argv)."), QStringLiteral("program")); parser.addOption(cmdOpt); QCommandLineOption cwdOpt({"C", "cwd"}, QStringLiteral("Set the spawned shell's working directory" " (default: inherited CWD)."), QStringLiteral("dir")); parser.addOption(cwdOpt); // Scroll-hitch instrumentation gate (XENO_SIDEBAR_DEBUG also enables the // C++ ps-scan timings; this flag reaches QML via Qt.application.arguments). QCommandLineOption debugOpt(QStringList{"xeno-sidebar-debug"}, QStringLiteral("Enable sidebar scroll-hitch debug logging.")); parser.addOption(debugOpt); parser.process(app); QStringList startupCommands = parser.values(execOpt); if (startupCommands.isEmpty()) { // Allow a single --execute with semicolon- or newline-separated // entries as a convenience for shells that don't repeat CLI flags. const QString single = parser.value(execOpt); if (!single.isEmpty()) { startupCommands = single.split(QRegularExpression{"[;\\n]"}, Qt::SkipEmptyParts); for (QString& s : startupCommands) { s = s.trimmed(); } startupCommands.removeAll(QString{}); } } const QString command = parser.value(cmdOpt); QQmlApplicationEngine engine; engine.rootContext()->setContextProperty( QStringLiteral("xenomorphicStartupCommands"), startupCommands); engine.rootContext()->setContextProperty( QStringLiteral("xenomorphicStartupCommand"), command); engine.rootContext()->setContextProperty( QStringLiteral("xenomorphicWorkingDir"), parser.value(cwdOpt)); auto* snapshotProvider = new SnapshotImageProvider(); engine.addImageProvider(QStringLiteral("snapshot"), snapshotProvider); engine.rootContext()->setContextProperty( QStringLiteral("xenomorphicSnapshotProvider"), QVariant::fromValue(snapshotProvider)); // The window agent is a context property (not a registered QML type): // qmlcachegen cannot resolve context properties, so calls on it compile // to runtime name-based dispatch. AOT-compiled calls against a resolved // QML type bake the metaobject's relative method indices in; QWindowKit // registers its types at runtime, so a build-time view of that type // (e.g. a tooling shim) desynchronizes the indices and segfaults. auto* windowAgent = new QWK::QuickWindowAgent(&engine); engine.rootContext()->setContextProperty( QStringLiteral("windowAgent"), windowAgent); engine.load(QUrl(QStringLiteral("qrc:/qt/qml/Xenomorphic/main.qml"))); if (engine.rootObjects().isEmpty()) { return -1; } // Optional live-driving server for development; inert unless // XENO_DEV_SERVER is set in the environment. Its constructor also // installs the "model"/"pool" root-context properties used below. DevDriver devDriver(&engine); // Quit-time session save, including the one-shot restart scan // (buildSessionRoot(true)). The scan reads the daemon's pane cache, // not the QML items, so it does not depend on QML surviving teardown. // The 500 ms debounce path (AppModel::saveSessionNow) keeps passing // scanQuit=false — only the quit path may run the `ps` subprocess. // // Reached through the engine's root-context property (DevDriver // installs the "model" singleton) with a name-based invoke: this // target compiles main.cpp with no models/ include dir, and keeping // the metaobject lookup by name avoids a CMake change. Null-safe for // a quit before the singletons exist. if (auto* model = engine.rootContext()->contextProperty(QStringLiteral("model")) .value()) { QObject::connect(&app, &QGuiApplication::aboutToQuit, model, [model]() { QMetaObject::invokeMethod( model, "saveSessionForQuit"); }); } return QGuiApplication::exec(); }