From ecbea0fe9057a2c4b013eac762643126b6d0c924 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 9 Oct 2026 13:27:40 +0200 Subject: [PATCH] gui: integrate bookmarks with plan9 plumber MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ollie:// URL scheme for plumb messages Format: ollie://session/agent#block - Plumber listens on 'ollie' port for navigation messages - Bookmarks show 📋 button to copy ollie:// link - Add data/plumbing with rules to route ollie:// URLs - On plumb message: switch session/agent, scroll to block, raise window --- data/plumbing | 10 ++++++++ kde/gui/SessionTree.qml | 12 +++++++++ kde/gui/main.cpp | 3 +++ kde/gui/main.qml | 55 +++++++++++++++++++++++++++++++++++++++ kde/gui/plumber.cpp | 57 +++++++++++++++++++++++++---------------- kde/gui/plumber.h | 20 ++++++++++++--- 6 files changed, 132 insertions(+), 25 deletions(-) create mode 100644 data/plumbing diff --git a/data/plumbing b/data/plumbing new file mode 100644 index 0000000..6115d4a --- /dev/null +++ b/data/plumbing @@ -0,0 +1,10 @@ +# Ollie plumbing rules +# Include this file in $HOME/lib/plumbing: +# include /path/to/ollie/data/plumbing +# Then reload: cat $HOME/lib/plumbing | 9p write plumb/rules + +# ollie:// URLs go to the ollie port +# Format: ollie://session/agent#block +type is text +data matches 'ollie://[a-zA-Z0-9_\-./]+(/[a-zA-Z0-9_\-./]+)?(#[0-9]+)?' +plumb to ollie diff --git a/kde/gui/SessionTree.qml b/kde/gui/SessionTree.qml index a30ed38..aa4b641 100644 --- a/kde/gui/SessionTree.qml +++ b/kde/gui/SessionTree.qml @@ -421,6 +421,8 @@ Item { width: bookmarksList.width height: 36 + property string ollieUrl: "ollie://" + encodeURIComponent(sessionName) + "/" + encodeURIComponent(agentName) + "#" + blockIndex + RowLayout { anchors.fill: parent anchors.leftMargin: 8 @@ -446,6 +448,16 @@ Item { } } + ToolButton { + text: "📋" + implicitWidth: 20 + implicitHeight: 20 + onClicked: clipboard.setText(ollieUrl) + ToolTip.visible: hovered + ToolTip.delay: 500 + ToolTip.text: "Copy ollie:// link" + } + ToolButton { text: "✕" implicitWidth: 20 diff --git a/kde/gui/main.cpp b/kde/gui/main.cpp index d0c0104..9acf471 100644 --- a/kde/gui/main.cpp +++ b/kde/gui/main.cpp @@ -79,6 +79,9 @@ const QUrl url(QStringLiteral("qrc:/main.qml")); engine.rootContext()->setContextProperty("trayIcon", trayIcon); + // Start plumber reader for ollie port + plumber->startReader(); + QObject::connect(showAction, &QAction::triggered, [window, trayIcon]() { window->show(); window->raise(); diff --git a/kde/gui/main.qml b/kde/gui/main.qml index 26569e6..6f13327 100644 --- a/kde/gui/main.qml +++ b/kde/gui/main.qml @@ -61,6 +61,61 @@ ApplicationWindow { return false } + // Handle plumb messages from the ollie port + // URL format: ollie://session/agent#block + function handleOllieMessage(url) { + // Parse ollie://session/agent#block + var match = url.match(/^ollie:\/\/([^\/]+)(?:\/([^#]+))?(?:#(\d+))?$/) + if (!match) { + console.log("Invalid ollie URL:", url) + return + } + var sessionName = decodeURIComponent(match[1]) + var agentName = match[2] ? decodeURIComponent(match[2]) : "" + var blockIndex = match[3] ? parseInt(match[3]) : -1 + + // Find session by name and switch to it + var sessions = ollie.sessions + for (var i = 0; i < sessions.length; i++) { + var s = sessions[i] + if (s.name === sessionName) { + ollie.setActiveSessionId(s.id) + + // If agent specified, find and switch to it + if (agentName && s.agents) { + for (var j = 0; j < s.agents.length; j++) { + var a = s.agents[j] + if (a.name === agentName) { + ollie.setActiveAgentId(a.id) + break + } + } + } + + // If block specified, scroll to it after a short delay + if (blockIndex >= 0) { + Qt.callLater(function() { + chatPane.scrollToBlock(blockIndex) + }) + } + + // Raise window + root.show() + root.raise() + root.requestActivate() + return + } + } + console.log("Session not found:", sessionName) + } + + Connections { + target: plumber + function onOllieMessage(url) { + handleOllieMessage(url) + } + } + // Apply theme palette to all Qt Quick Controls palette.active.window: theme.windowColor palette.active.windowText: theme.windowTextColor diff --git a/kde/gui/plumber.cpp b/kde/gui/plumber.cpp index e1b1525..a7f3c58 100644 --- a/kde/gui/plumber.cpp +++ b/kde/gui/plumber.cpp @@ -60,7 +60,7 @@ static QString currentUser() } // --------------------------------------------------------------------------- -// PlumbReader: owns a NineP session on the edit port, loops on blocking reads. +// PlumbReader: owns a NineP session on a named port, loops on blocking reads. // Lives on its own QThread. Emits parsed messages via queued signals so the // Plumber (GUI thread) can act on them. // --------------------------------------------------------------------------- @@ -68,9 +68,10 @@ class PlumbReader : public QThread { Q_OBJECT public: - explicit PlumbReader(const QString &socketPath, QObject *parent = nullptr) + explicit PlumbReader(const QString &socketPath, const QString &portName, QObject *parent = nullptr) : QThread(parent) , m_socketPath(socketPath) + , m_portName(portName) { } @@ -80,7 +81,7 @@ public: } Q_SIGNALS: - void message(const QString &file, const QString &addr, const QString &wdir); + void message(const QString &data, const QString &addr, const QString &wdir); void failed(const QString &message); protected: @@ -91,9 +92,9 @@ protected: Q_EMIT failed(QStringLiteral("plumb connect: %1").arg(nine.errorString())); return; } - static constexpr uint32_t EditFid = 1; - if (!nine.walk(EditFid, QStringLiteral("edit")) || !nine.open(EditFid, NineP::OREAD)) { - Q_EMIT failed(QStringLiteral("plumb open edit: %1").arg(nine.errorString())); + static constexpr uint32_t PortFid = 1; + if (!nine.walk(PortFid, m_portName) || !nine.open(PortFid, NineP::OREAD)) { + Q_EMIT failed(QStringLiteral("plumb open %1: %2").arg(m_portName, nine.errorString())); return; } @@ -101,7 +102,7 @@ protected: bool pending = false; while (!m_stop.loadRelaxed()) { if (!pending) { - if (!nine.beginRead(EditFid, 0, nine.msize())) { + if (!nine.beginRead(PortFid, 0, nine.msize())) { Q_EMIT failed(QStringLiteral("plumb read: %1").arg(nine.errorString())); break; } @@ -130,6 +131,7 @@ protected: private: QString m_socketPath; + QString m_portName; QAtomicInt m_stop{0}; }; @@ -143,15 +145,19 @@ Plumber::Plumber(QObject *parent) Plumber::~Plumber() { - if (!m_reader) - return; - m_reader->stop(); - if (m_reader->wait(3000)) { - delete m_reader; - } else { - connect(m_reader, &QThread::finished, m_reader, &QObject::deleteLater); - } - m_reader = nullptr; + auto stopReader = [](PlumbReader *&reader) { + if (!reader) + return; + reader->stop(); + if (reader->wait(3000)) { + delete reader; + } else { + QObject::connect(reader, &QThread::finished, reader, &QObject::deleteLater); + } + reader = nullptr; + }; + stopReader(m_reader); + stopReader(m_ollieReader); } bool Plumber::available() const @@ -214,17 +220,24 @@ bool Plumber::sendTo(const QString &data, const QString &dst, const QString &wdi void Plumber::startReader() { - if (m_reader) - return; const QString path = socketPath(); if (path.isEmpty()) { Q_EMIT readerError(QStringLiteral("no plumb namespace ($NAMESPACE/$DISPLAY unset)")); return; } - m_reader = new PlumbReader(path); - connect(m_reader, &PlumbReader::message, this, &Plumber::edit, Qt::QueuedConnection); - connect(m_reader, &PlumbReader::failed, this, &Plumber::readerError, Qt::QueuedConnection); - m_reader->start(); + + // Start ollie port reader (for ollie:// URLs) + if (!m_ollieReader) { + m_ollieReader = new PlumbReader(path, QStringLiteral("ollie")); + connect(m_ollieReader, &PlumbReader::message, this, [this](const QString &data, const QString &, const QString &) { + Q_EMIT ollieMessage(data); + }, Qt::QueuedConnection); + connect(m_ollieReader, &PlumbReader::failed, this, [](const QString &msg) { + // Silently ignore ollie port errors — port may not exist in plumb rules + qDebug("plumber: ollie port: %s", qPrintable(msg)); + }, Qt::QueuedConnection); + m_ollieReader->start(); + } } #include "plumber.moc" diff --git a/kde/gui/plumber.h b/kde/gui/plumber.h index e0611cc..1869e0a 100644 --- a/kde/gui/plumber.h +++ b/kde/gui/plumber.h @@ -8,12 +8,17 @@ * plumb message; the plumber's rules resolve it (file:line -> edit port, * url -> web port, etc.). The source is "ollie" and the destination is * left empty so the rules decide. - * - RECEIVE: a dedicated reader thread opens the "edit" port and blocks on - * reads. Each plumb message that arrives (file to open, with an optional - * addr) is emitted via edit() on the GUI thread via a queued signal. + * - RECEIVE: dedicated reader threads open ports and block on reads. + * The "ollie" port receives ollie-specific messages (ollie:// URLs). + * Messages are emitted via signals on the GUI thread. * * The namespace socket is $NAMESPACE/plumb, or /tmp/ns.$USER.$DISPLAY/plumb * when $NAMESPACE is unset — the same resolution plan9's getns() performs. + * + * URL scheme for ollie plumb messages: + * ollie://session/agent#block - navigate to block in chat + * ollie://session/agent - switch to agent + * ollie://session - switch to session */ #ifndef PLUMBER_H #define PLUMBER_H @@ -72,6 +77,14 @@ Q_SIGNALS: */ void edit(const QString &file, const QString &addr, const QString &wdir); + /*! + * A plumb message arrived on the ollie port. The \a url is an ollie:// URL: + * ollie://session/agent#block - navigate to chat block + * ollie://session/agent - switch to agent + * ollie://session - switch to session + */ + void ollieMessage(const QString &url); + /*! The reader stopped with an error (e.g. plumber not running). */ void readerError(const QString &message); @@ -80,6 +93,7 @@ Q_SIGNALS: private: PlumbReader *m_reader = nullptr; + PlumbReader *m_ollieReader = nullptr; QString m_error; };