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
|
width: bookmarksList.width
|
||||||
height: 36
|
height: 36
|
||||||
|
|
||||||
|
property string ollieUrl: "ollie://" + encodeURIComponent(sessionName) + "/" + encodeURIComponent(agentName) + "#" + blockIndex
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: 8
|
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 {
|
ToolButton {
|
||||||
text: "✕"
|
text: "✕"
|
||||||
implicitWidth: 20
|
implicitWidth: 20
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,9 @@ const QUrl url(QStringLiteral("qrc:/main.qml"));
|
||||||
|
|
||||||
engine.rootContext()->setContextProperty("trayIcon", trayIcon);
|
engine.rootContext()->setContextProperty("trayIcon", trayIcon);
|
||||||
|
|
||||||
|
// Start plumber reader for ollie port
|
||||||
|
plumber->startReader();
|
||||||
|
|
||||||
QObject::connect(showAction, &QAction::triggered, [window, trayIcon]() {
|
QObject::connect(showAction, &QAction::triggered, [window, trayIcon]() {
|
||||||
window->show();
|
window->show();
|
||||||
window->raise();
|
window->raise();
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,61 @@ ApplicationWindow {
|
||||||
return false
|
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
|
// Apply theme palette to all Qt Quick Controls
|
||||||
palette.active.window: theme.windowColor
|
palette.active.window: theme.windowColor
|
||||||
palette.active.windowText: theme.windowTextColor
|
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
|
// Lives on its own QThread. Emits parsed messages via queued signals so the
|
||||||
// Plumber (GUI thread) can act on them.
|
// Plumber (GUI thread) can act on them.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -68,9 +68,10 @@ class PlumbReader : public QThread
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit PlumbReader(const QString &socketPath, QObject *parent = nullptr)
|
explicit PlumbReader(const QString &socketPath, const QString &portName, QObject *parent = nullptr)
|
||||||
: QThread(parent)
|
: QThread(parent)
|
||||||
, m_socketPath(socketPath)
|
, m_socketPath(socketPath)
|
||||||
|
, m_portName(portName)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,7 +81,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_SIGNALS:
|
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);
|
void failed(const QString &message);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
@ -91,9 +92,9 @@ protected:
|
||||||
Q_EMIT failed(QStringLiteral("plumb connect: %1").arg(nine.errorString()));
|
Q_EMIT failed(QStringLiteral("plumb connect: %1").arg(nine.errorString()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
static constexpr uint32_t EditFid = 1;
|
static constexpr uint32_t PortFid = 1;
|
||||||
if (!nine.walk(EditFid, QStringLiteral("edit")) || !nine.open(EditFid, NineP::OREAD)) {
|
if (!nine.walk(PortFid, m_portName) || !nine.open(PortFid, NineP::OREAD)) {
|
||||||
Q_EMIT failed(QStringLiteral("plumb open edit: %1").arg(nine.errorString()));
|
Q_EMIT failed(QStringLiteral("plumb open %1: %2").arg(m_portName, nine.errorString()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +102,7 @@ protected:
|
||||||
bool pending = false;
|
bool pending = false;
|
||||||
while (!m_stop.loadRelaxed()) {
|
while (!m_stop.loadRelaxed()) {
|
||||||
if (!pending) {
|
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()));
|
Q_EMIT failed(QStringLiteral("plumb read: %1").arg(nine.errorString()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -130,6 +131,7 @@ protected:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_socketPath;
|
QString m_socketPath;
|
||||||
|
QString m_portName;
|
||||||
QAtomicInt m_stop{0};
|
QAtomicInt m_stop{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -143,15 +145,19 @@ Plumber::Plumber(QObject *parent)
|
||||||
|
|
||||||
Plumber::~Plumber()
|
Plumber::~Plumber()
|
||||||
{
|
{
|
||||||
if (!m_reader)
|
auto stopReader = [](PlumbReader *&reader) {
|
||||||
return;
|
if (!reader)
|
||||||
m_reader->stop();
|
return;
|
||||||
if (m_reader->wait(3000)) {
|
reader->stop();
|
||||||
delete m_reader;
|
if (reader->wait(3000)) {
|
||||||
} else {
|
delete reader;
|
||||||
connect(m_reader, &QThread::finished, m_reader, &QObject::deleteLater);
|
} else {
|
||||||
}
|
QObject::connect(reader, &QThread::finished, reader, &QObject::deleteLater);
|
||||||
m_reader = nullptr;
|
}
|
||||||
|
reader = nullptr;
|
||||||
|
};
|
||||||
|
stopReader(m_reader);
|
||||||
|
stopReader(m_ollieReader);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Plumber::available() const
|
bool Plumber::available() const
|
||||||
|
|
@ -214,17 +220,24 @@ bool Plumber::sendTo(const QString &data, const QString &dst, const QString &wdi
|
||||||
|
|
||||||
void Plumber::startReader()
|
void Plumber::startReader()
|
||||||
{
|
{
|
||||||
if (m_reader)
|
|
||||||
return;
|
|
||||||
const QString path = socketPath();
|
const QString path = socketPath();
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
Q_EMIT readerError(QStringLiteral("no plumb namespace ($NAMESPACE/$DISPLAY unset)"));
|
Q_EMIT readerError(QStringLiteral("no plumb namespace ($NAMESPACE/$DISPLAY unset)"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_reader = new PlumbReader(path);
|
|
||||||
connect(m_reader, &PlumbReader::message, this, &Plumber::edit, Qt::QueuedConnection);
|
// Start ollie port reader (for ollie:// URLs)
|
||||||
connect(m_reader, &PlumbReader::failed, this, &Plumber::readerError, Qt::QueuedConnection);
|
if (!m_ollieReader) {
|
||||||
m_reader->start();
|
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"
|
#include "plumber.moc"
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,17 @@
|
||||||
* plumb message; the plumber's rules resolve it (file:line -> edit port,
|
* plumb message; the plumber's rules resolve it (file:line -> edit port,
|
||||||
* url -> web port, etc.). The source is "ollie" and the destination is
|
* url -> web port, etc.). The source is "ollie" and the destination is
|
||||||
* left empty so the rules decide.
|
* left empty so the rules decide.
|
||||||
* - RECEIVE: a dedicated reader thread opens the "edit" port and blocks on
|
* - RECEIVE: dedicated reader threads open ports and block on reads.
|
||||||
* reads. Each plumb message that arrives (file to open, with an optional
|
* The "ollie" port receives ollie-specific messages (ollie:// URLs).
|
||||||
* addr) is emitted via edit() on the GUI thread via a queued signal.
|
* Messages are emitted via signals on the GUI thread.
|
||||||
*
|
*
|
||||||
* The namespace socket is $NAMESPACE/plumb, or /tmp/ns.$USER.$DISPLAY/plumb
|
* The namespace socket is $NAMESPACE/plumb, or /tmp/ns.$USER.$DISPLAY/plumb
|
||||||
* when $NAMESPACE is unset — the same resolution plan9's getns() performs.
|
* 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
|
#ifndef PLUMBER_H
|
||||||
#define PLUMBER_H
|
#define PLUMBER_H
|
||||||
|
|
@ -72,6 +77,14 @@ Q_SIGNALS:
|
||||||
*/
|
*/
|
||||||
void edit(const QString &file, const QString &addr, const QString &wdir);
|
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). */
|
/*! The reader stopped with an error (e.g. plumber not running). */
|
||||||
void readerError(const QString &message);
|
void readerError(const QString &message);
|
||||||
|
|
||||||
|
|
@ -80,6 +93,7 @@ Q_SIGNALS:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
PlumbReader *m_reader = nullptr;
|
PlumbReader *m_reader = nullptr;
|
||||||
|
PlumbReader *m_ollieReader = nullptr;
|
||||||
QString m_error;
|
QString m_error;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue