gui: integrate bookmarks with plan9 plumber
- 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
This commit is contained in:
parent
96b6406871
commit
ecbea0fe90
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue