gui: native 9P plumb client with edit receiver and mouse chording
Replace QProcess plumb binary shell-out with proper 9P2000 plumb client: - ninep.h/cpp: minimal 9P2000 protocol client for plan9port plumber - plumbmsg.h/cpp: plumb message pack/unpack wire codec - plumber.h/cpp: full plumb client with: - socketPath() resolving $NAMESPACE/plumb or display fallback - send()/sendTo() write packed messages to send port via 9P - PlumbReader thread blocks on edit port, emits edit(file,addr,wdir) - startReader() starts the receive loop - available() checks plumb socket existence Add Acme-style mouse chording to ChatBlockDelegate.qml: - B1+B2 = cut (snarf) - B1+B3 = paste - Context menu now includes Cut/Paste with chord hints Legacy plumb()/plumbTo() methods preserved as aliases to send()/sendTo().
This commit is contained in:
parent
4774cb9fe9
commit
9618c79232
|
|
@ -120,6 +120,8 @@ add_executable(ollie-gui
|
|||
gui/thememanager.cpp
|
||||
gui/clipboardhelper.cpp
|
||||
gui/plumber.cpp
|
||||
gui/ninep.cpp
|
||||
gui/plumbmsg.cpp
|
||||
)
|
||||
add_dependencies(ollie-gui lib9p_build)
|
||||
target_include_directories(ollie-gui PRIVATE
|
||||
|
|
|
|||
|
|
@ -207,13 +207,41 @@ Column {
|
|||
selectedTextColor: theme.highlightedTextColor
|
||||
|
||||
// Acme-style mouse handling: B2=execute, B3=plumb/menu
|
||||
// B1+B2=cut, B1+B3=paste (chord detection)
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.MiddleButton | Qt.RightButton
|
||||
cursorShape: Qt.IBeamCursor
|
||||
propagateComposedEvents: true
|
||||
|
||||
property bool leftHeld: false
|
||||
|
||||
onPressed: function(mouse) {
|
||||
// Check if this is a chord (B1 already held)
|
||||
if (mouse.buttons & Qt.LeftButton) {
|
||||
leftHeld = true
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
// B1+B2 = cut (snarf)
|
||||
mainTextEdit.cut()
|
||||
mouse.accepted = true
|
||||
return
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
// B1+B3 = paste
|
||||
mainTextEdit.paste()
|
||||
mouse.accepted = true
|
||||
return
|
||||
}
|
||||
}
|
||||
leftHeld = false
|
||||
mouse.accepted = false
|
||||
}
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (leftHeld) {
|
||||
leftHeld = false
|
||||
return
|
||||
}
|
||||
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
// B2: Execute selection as prompt
|
||||
var sel = mainTextEdit.selectedText
|
||||
|
|
@ -237,6 +265,10 @@ Column {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
leftHeld = false
|
||||
}
|
||||
}
|
||||
property var _menu: null
|
||||
}
|
||||
|
|
@ -326,14 +358,39 @@ Column {
|
|||
selectionColor: theme.highlightColor
|
||||
selectedTextColor: theme.highlightedTextColor
|
||||
|
||||
// Acme-style mouse handling
|
||||
// Acme-style mouse handling with chord support
|
||||
// B1+B2=cut, B1+B3=paste (chord detection)
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.MiddleButton | Qt.RightButton
|
||||
cursorShape: Qt.IBeamCursor
|
||||
propagateComposedEvents: true
|
||||
|
||||
property bool leftHeld: false
|
||||
|
||||
onPressed: function(mouse) {
|
||||
if (mouse.buttons & Qt.LeftButton) {
|
||||
leftHeld = true
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
textEdit.cut()
|
||||
mouse.accepted = true
|
||||
return
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
textEdit.paste()
|
||||
mouse.accepted = true
|
||||
return
|
||||
}
|
||||
}
|
||||
leftHeld = false
|
||||
mouse.accepted = false
|
||||
}
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (leftHeld) {
|
||||
leftHeld = false
|
||||
return
|
||||
}
|
||||
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
var sel = textEdit.selectedText
|
||||
if (sel && sel.trim() !== "") {
|
||||
|
|
@ -354,6 +411,10 @@ Column {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
leftHeld = false
|
||||
}
|
||||
}
|
||||
|
||||
property Menu _menu: null
|
||||
|
|
@ -382,11 +443,20 @@ Column {
|
|||
onTriggered: if (ctxMenu.target) plumber.plumb(ctxMenu.target.selectedText.trim())
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem {
|
||||
text: "Cut (B1+B2)"
|
||||
enabled: ctxMenu.target ? ctxMenu.target.selectedText.length > 0 : false
|
||||
onTriggered: if (ctxMenu.target) ctxMenu.target.cut()
|
||||
}
|
||||
MenuItem {
|
||||
text: "Copy"
|
||||
enabled: ctxMenu.target ? ctxMenu.target.selectedText.length > 0 : false
|
||||
onTriggered: if (ctxMenu.target) ctxMenu.target.copy()
|
||||
}
|
||||
MenuItem {
|
||||
text: "Paste (B1+B3)"
|
||||
onTriggered: if (ctxMenu.target) ctxMenu.target.paste()
|
||||
}
|
||||
MenuItem {
|
||||
text: "Select All"
|
||||
onTriggered: if (ctxMenu.target) ctxMenu.target.selectAll()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,418 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
#include "ninep.h"
|
||||
|
||||
#include <QLocalSocket>
|
||||
|
||||
// 9P2000 message type codes (plan9port fcall.h)
|
||||
enum {
|
||||
Tversion = 100,
|
||||
Rversion = 101,
|
||||
Tattach = 104,
|
||||
Rattach = 105,
|
||||
Rerror = 107,
|
||||
Twalk = 110,
|
||||
Rwalk = 111,
|
||||
Topen = 112,
|
||||
Ropen = 113,
|
||||
Tread = 116,
|
||||
Rread = 117,
|
||||
Twrite = 118,
|
||||
Rwrite = 119,
|
||||
Tclunk = 120,
|
||||
Rclunk = 121,
|
||||
};
|
||||
|
||||
static constexpr uint16_t NOTAG = 0xFFFF;
|
||||
static constexpr uint32_t NOFID = 0xFFFFFFFFu;
|
||||
static constexpr uint16_t Tag = 1;
|
||||
static constexpr int ConnectTimeoutMs = 5000;
|
||||
static constexpr uint32_t IOHDRSZ_LOCAL = 24;
|
||||
|
||||
// Little-endian field appenders
|
||||
static void put1(QByteArray &b, uint8_t v) { b.append(char(v)); }
|
||||
static void put2(QByteArray &b, uint16_t v)
|
||||
{
|
||||
b.append(char(v & 0xFF));
|
||||
b.append(char((v >> 8) & 0xFF));
|
||||
}
|
||||
static void put4(QByteArray &b, uint32_t v)
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
b.append(char((v >> (8 * i)) & 0xFF));
|
||||
}
|
||||
static void put8(QByteArray &b, uint64_t v)
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
b.append(char((v >> (8 * i)) & 0xFF));
|
||||
}
|
||||
static void putStr(QByteArray &b, const QByteArray &s)
|
||||
{
|
||||
put2(b, uint16_t(s.size()));
|
||||
b.append(s);
|
||||
}
|
||||
|
||||
// Little-endian field reader
|
||||
namespace {
|
||||
struct Reader {
|
||||
const char *p;
|
||||
int remaining;
|
||||
bool ok = true;
|
||||
static Reader of(const QByteArray &b) { return Reader{b.constData(), int(b.size())}; }
|
||||
uint8_t u1()
|
||||
{
|
||||
if (remaining < 1) { ok = false; return 0; }
|
||||
uint8_t v = uint8_t(*p);
|
||||
p++; remaining--;
|
||||
return v;
|
||||
}
|
||||
uint16_t u2()
|
||||
{
|
||||
if (remaining < 2) { ok = false; return 0; }
|
||||
uint16_t v = uint8_t(p[0]) | (uint16_t(uint8_t(p[1])) << 8);
|
||||
p += 2; remaining -= 2;
|
||||
return v;
|
||||
}
|
||||
uint32_t u4()
|
||||
{
|
||||
if (remaining < 4) { ok = false; return 0; }
|
||||
uint32_t v = 0;
|
||||
for (int i = 0; i < 4; i++)
|
||||
v |= uint32_t(uint8_t(p[i])) << (8 * i);
|
||||
p += 4; remaining -= 4;
|
||||
return v;
|
||||
}
|
||||
QByteArray str()
|
||||
{
|
||||
uint16_t n = u2();
|
||||
if (!ok || remaining < int(n)) { ok = false; return QByteArray(); }
|
||||
QByteArray s(p, n);
|
||||
p += n; remaining -= n;
|
||||
return s;
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
NineP::NineP() = default;
|
||||
|
||||
NineP::~NineP()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
bool NineP::isConnected() const
|
||||
{
|
||||
return m_sock && m_sock->state() == QLocalSocket::ConnectedState;
|
||||
}
|
||||
|
||||
void NineP::close()
|
||||
{
|
||||
if (m_sock) {
|
||||
if (m_sock->state() == QLocalSocket::ConnectedState)
|
||||
clunk(RootFid);
|
||||
m_sock->abort();
|
||||
delete m_sock;
|
||||
m_sock = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool NineP::readExactly(char *buf, int n)
|
||||
{
|
||||
static constexpr int ReadChunkTimeoutMs = 2000;
|
||||
int got = 0;
|
||||
while (got < n) {
|
||||
if (m_sock->bytesAvailable() == 0) {
|
||||
if (!m_sock->waitForReadyRead(ReadChunkTimeoutMs)) {
|
||||
m_error = QStringLiteral("socket read failed: %1").arg(m_sock->errorString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const qint64 r = m_sock->read(buf + got, n - got);
|
||||
if (r < 0) {
|
||||
m_error = QStringLiteral("socket read error: %1").arg(m_sock->errorString());
|
||||
return false;
|
||||
}
|
||||
got += int(r);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NineP::sendMsg(uint8_t type, uint16_t tag, const QByteArray &body)
|
||||
{
|
||||
QByteArray msg;
|
||||
const uint32_t size = 4 + 1 + 2 + uint32_t(body.size());
|
||||
put4(msg, size);
|
||||
put1(msg, type);
|
||||
put2(msg, tag);
|
||||
msg.append(body);
|
||||
if (m_sock->write(msg) != msg.size()) {
|
||||
m_error = QStringLiteral("socket write failed: %1").arg(m_sock->errorString());
|
||||
return false;
|
||||
}
|
||||
if (!m_sock->waitForBytesWritten(ConnectTimeoutMs)) {
|
||||
m_error = QStringLiteral("socket flush timed out");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NineP::recvMsg(uint8_t *type, uint16_t *tag, QByteArray *body)
|
||||
{
|
||||
char hdr[7];
|
||||
if (!readExactly(hdr, 7))
|
||||
return false;
|
||||
const uint32_t size = uint32_t(uint8_t(hdr[0])) | (uint32_t(uint8_t(hdr[1])) << 8)
|
||||
| (uint32_t(uint8_t(hdr[2])) << 16) | (uint32_t(uint8_t(hdr[3])) << 24);
|
||||
if (size < 7) {
|
||||
m_error = QStringLiteral("9P message too short (%1)").arg(size);
|
||||
return false;
|
||||
}
|
||||
*type = uint8_t(hdr[4]);
|
||||
*tag = uint16_t(uint8_t(hdr[5]) | (uint16_t(uint8_t(hdr[6])) << 8));
|
||||
const int bodyLen = int(size) - 7;
|
||||
QByteArray b;
|
||||
b.resize(bodyLen);
|
||||
if (bodyLen > 0 && !readExactly(b.data(), bodyLen))
|
||||
return false;
|
||||
if (*type == Rerror) {
|
||||
Reader r = Reader::of(b);
|
||||
const QByteArray ename = r.str();
|
||||
m_error = QStringLiteral("9P Rerror: %1").arg(QString::fromUtf8(ename));
|
||||
return false;
|
||||
}
|
||||
*body = b;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NineP::connectAndAttach(const QString &socketPath, const QString &user,
|
||||
const QString &aname)
|
||||
{
|
||||
close();
|
||||
m_sock = new QLocalSocket();
|
||||
m_sock->connectToServer(socketPath, QLocalSocket::ReadWrite);
|
||||
if (!m_sock->waitForConnected(ConnectTimeoutMs)) {
|
||||
m_error = QStringLiteral("connect %1: %2").arg(socketPath, m_sock->errorString());
|
||||
delete m_sock;
|
||||
m_sock = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tversion: msize[4] version[s]
|
||||
{
|
||||
QByteArray body;
|
||||
put4(body, m_msize);
|
||||
putStr(body, QByteArrayLiteral("9P2000"));
|
||||
if (!sendMsg(Tversion, NOTAG, body)) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
uint8_t type;
|
||||
uint16_t tag;
|
||||
QByteArray rb;
|
||||
if (!recvMsg(&type, &tag, &rb) || type != Rversion) {
|
||||
if (m_error.isEmpty())
|
||||
m_error = QStringLiteral("unexpected reply to Tversion");
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
Reader r = Reader::of(rb);
|
||||
const uint32_t srvMsize = r.u4();
|
||||
const QByteArray srvVer = r.str();
|
||||
if (!r.ok || !srvVer.startsWith("9P2000")) {
|
||||
m_error = QStringLiteral("server declined 9P2000 (got '%1')")
|
||||
.arg(QString::fromUtf8(srvVer));
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
if (srvMsize > 0 && srvMsize < m_msize)
|
||||
m_msize = srvMsize;
|
||||
}
|
||||
|
||||
// Tattach: fid[4] afid[4] uname[s] aname[s]
|
||||
{
|
||||
QByteArray body;
|
||||
put4(body, RootFid);
|
||||
put4(body, NOFID);
|
||||
putStr(body, user.toUtf8());
|
||||
putStr(body, aname.toUtf8());
|
||||
if (!sendMsg(Tattach, Tag, body)) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
uint8_t type;
|
||||
uint16_t tag;
|
||||
QByteArray rb;
|
||||
if (!recvMsg(&type, &tag, &rb) || type != Rattach) {
|
||||
if (m_error.isEmpty())
|
||||
m_error = QStringLiteral("unexpected reply to Tattach");
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NineP::walk(uint32_t newFid, const QString &name)
|
||||
{
|
||||
// Twalk: fid[4] newfid[4] nwname[2] nwname*(wname[s])
|
||||
QByteArray body;
|
||||
put4(body, RootFid);
|
||||
put4(body, newFid);
|
||||
const QByteArray n = name.toUtf8();
|
||||
put2(body, 1);
|
||||
putStr(body, n);
|
||||
if (!sendMsg(Twalk, Tag, body))
|
||||
return false;
|
||||
uint8_t type;
|
||||
uint16_t tag;
|
||||
QByteArray rb;
|
||||
if (!recvMsg(&type, &tag, &rb) || type != Rwalk) {
|
||||
if (m_error.isEmpty())
|
||||
m_error = QStringLiteral("walk '%1' failed").arg(name);
|
||||
return false;
|
||||
}
|
||||
Reader r = Reader::of(rb);
|
||||
const uint16_t nwqid = r.u2();
|
||||
if (!r.ok || nwqid != 1) {
|
||||
m_error = QStringLiteral("walk '%1' did not resolve").arg(name);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NineP::open(uint32_t fid, uint8_t mode)
|
||||
{
|
||||
QByteArray body;
|
||||
put4(body, fid);
|
||||
put1(body, mode);
|
||||
if (!sendMsg(Topen, Tag, body))
|
||||
return false;
|
||||
uint8_t type;
|
||||
uint16_t tag;
|
||||
QByteArray rb;
|
||||
if (!recvMsg(&type, &tag, &rb) || type != Ropen) {
|
||||
if (m_error.isEmpty())
|
||||
m_error = QStringLiteral("open failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int NineP::write(uint32_t fid, uint64_t offset, const QByteArray &data)
|
||||
{
|
||||
if (uint32_t(data.size()) + 4 + 1 + 2 + 4 + 8 + 4 > m_msize) {
|
||||
m_error = QStringLiteral("write exceeds msize");
|
||||
return -1;
|
||||
}
|
||||
QByteArray body;
|
||||
put4(body, fid);
|
||||
put8(body, offset);
|
||||
put4(body, uint32_t(data.size()));
|
||||
body.append(data);
|
||||
if (!sendMsg(Twrite, Tag, body))
|
||||
return -1;
|
||||
uint8_t type;
|
||||
uint16_t tag;
|
||||
QByteArray rb;
|
||||
if (!recvMsg(&type, &tag, &rb) || type != Rwrite) {
|
||||
if (m_error.isEmpty())
|
||||
m_error = QStringLiteral("write failed");
|
||||
return -1;
|
||||
}
|
||||
Reader r = Reader::of(rb);
|
||||
const uint32_t count = r.u4();
|
||||
if (!r.ok) {
|
||||
m_error = QStringLiteral("malformed Rwrite");
|
||||
return -1;
|
||||
}
|
||||
return int(count);
|
||||
}
|
||||
|
||||
int NineP::read(uint32_t fid, uint64_t offset, uint32_t count, QByteArray *out)
|
||||
{
|
||||
if (count > m_msize - IOHDRSZ_LOCAL)
|
||||
count = m_msize - IOHDRSZ_LOCAL;
|
||||
QByteArray body;
|
||||
put4(body, fid);
|
||||
put8(body, offset);
|
||||
put4(body, count);
|
||||
if (!sendMsg(Tread, Tag, body))
|
||||
return -1;
|
||||
uint8_t type;
|
||||
uint16_t tag;
|
||||
QByteArray rb;
|
||||
if (!recvMsg(&type, &tag, &rb) || type != Rread) {
|
||||
if (m_error.isEmpty())
|
||||
m_error = QStringLiteral("read failed");
|
||||
return -1;
|
||||
}
|
||||
Reader r = Reader::of(rb);
|
||||
const uint32_t n = r.u4();
|
||||
if (!r.ok || int(n) > rb.size() - 4) {
|
||||
m_error = QStringLiteral("malformed Rread");
|
||||
return -1;
|
||||
}
|
||||
if (out)
|
||||
*out = rb.mid(4, int(n));
|
||||
return int(n);
|
||||
}
|
||||
|
||||
bool NineP::beginRead(uint32_t fid, uint64_t offset, uint32_t count)
|
||||
{
|
||||
if (count > m_msize - IOHDRSZ_LOCAL)
|
||||
count = m_msize - IOHDRSZ_LOCAL;
|
||||
QByteArray body;
|
||||
put4(body, fid);
|
||||
put8(body, offset);
|
||||
put4(body, count);
|
||||
return sendMsg(Tread, Tag, body);
|
||||
}
|
||||
|
||||
int NineP::recvReadReply(QByteArray *out, int firstByteTimeoutMs, bool *timedOut)
|
||||
{
|
||||
if (timedOut)
|
||||
*timedOut = false;
|
||||
if (m_sock->bytesAvailable() == 0) {
|
||||
if (!m_sock->waitForReadyRead(firstByteTimeoutMs)) {
|
||||
if (m_sock->state() == QLocalSocket::ConnectedState) {
|
||||
if (timedOut)
|
||||
*timedOut = true;
|
||||
return 0;
|
||||
}
|
||||
m_error = QStringLiteral("socket read failed: %1").arg(m_sock->errorString());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
uint8_t type;
|
||||
uint16_t tag;
|
||||
QByteArray rb;
|
||||
if (!recvMsg(&type, &tag, &rb) || type != Rread) {
|
||||
if (m_error.isEmpty())
|
||||
m_error = QStringLiteral("read reply failed");
|
||||
return -1;
|
||||
}
|
||||
Reader r = Reader::of(rb);
|
||||
const uint32_t n = r.u4();
|
||||
if (!r.ok || int(n) > rb.size() - 4) {
|
||||
m_error = QStringLiteral("malformed Rread");
|
||||
return -1;
|
||||
}
|
||||
if (out)
|
||||
*out = rb.mid(4, int(n));
|
||||
return int(n);
|
||||
}
|
||||
|
||||
bool NineP::clunk(uint32_t fid)
|
||||
{
|
||||
QByteArray body;
|
||||
put4(body, fid);
|
||||
if (!sendMsg(Tclunk, Tag, body))
|
||||
return false;
|
||||
uint8_t type;
|
||||
uint16_t tag;
|
||||
QByteArray rb;
|
||||
if (!recvMsg(&type, &tag, &rb) || type != Rclunk)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* NineP — a minimal, synchronous 9P2000 client over a Unix-domain socket.
|
||||
*
|
||||
* Just enough protocol to talk to a plan9port plumber: version negotiation,
|
||||
* attach, walk, open, read and write. No auth (plumber accepts NOFID afid),
|
||||
* no create/remove/stat. All little-endian, matching plan9port fcall.h.
|
||||
*/
|
||||
#ifndef OLLIE_NINEP_H
|
||||
#define OLLIE_NINEP_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <cstdint>
|
||||
|
||||
class QLocalSocket;
|
||||
|
||||
/*!
|
||||
* One synchronous 9P2000 session. Not thread-safe; create one per thread.
|
||||
*/
|
||||
class NineP
|
||||
{
|
||||
public:
|
||||
NineP();
|
||||
~NineP();
|
||||
|
||||
NineP(const NineP &) = delete;
|
||||
NineP &operator=(const NineP &) = delete;
|
||||
|
||||
/*!
|
||||
* Connect to \a socketPath, negotiate 9P2000, and attach as \a user.
|
||||
* Returns true on success. On failure, errorString() explains why.
|
||||
*/
|
||||
bool connectAndAttach(const QString &socketPath, const QString &user,
|
||||
const QString &aname = QString());
|
||||
|
||||
bool isConnected() const;
|
||||
void close();
|
||||
|
||||
/*!
|
||||
* Walk from the attached root to \a name (single path element)
|
||||
* binding \a newFid to it. Returns true on success.
|
||||
*/
|
||||
bool walk(uint32_t newFid, const QString &name);
|
||||
|
||||
/*! Open \a fid with the given 9P \a mode (OREAD=0, OWRITE=1, ORDWR=2). */
|
||||
bool open(uint32_t fid, uint8_t mode);
|
||||
|
||||
/*!
|
||||
* Write \a data to \a fid at \a offset. Returns bytes accepted, or -1.
|
||||
*/
|
||||
int write(uint32_t fid, uint64_t offset, const QByteArray &data);
|
||||
|
||||
/*!
|
||||
* Read up to \a count bytes from \a fid at \a offset. Returns bytes read
|
||||
* (0 at EOF), or -1 on error. Blocks until server responds.
|
||||
*/
|
||||
int read(uint32_t fid, uint64_t offset, uint32_t count, QByteArray *out);
|
||||
|
||||
/*!
|
||||
* Send Tread without waiting for reply. Used with recvReadReply() for
|
||||
* cancellable blocking reads on edit port.
|
||||
*/
|
||||
bool beginRead(uint32_t fid, uint64_t offset, uint32_t count);
|
||||
|
||||
/*!
|
||||
* Wait up to \a firstByteTimeoutMs for Rread reply. Returns byte count
|
||||
* (>=0), 0 with \a timedOut set when nothing arrived, or -1 on error.
|
||||
*/
|
||||
int recvReadReply(QByteArray *out, int firstByteTimeoutMs, bool *timedOut);
|
||||
|
||||
bool clunk(uint32_t fid);
|
||||
|
||||
uint32_t msize() const { return m_msize; }
|
||||
QString errorString() const { return m_error; }
|
||||
|
||||
enum OpenMode : uint8_t { OREAD = 0, OWRITE = 1, ORDWR = 2 };
|
||||
static constexpr uint32_t RootFid = 0;
|
||||
|
||||
private:
|
||||
bool sendMsg(uint8_t type, uint16_t tag, const QByteArray &body);
|
||||
bool recvMsg(uint8_t *type, uint16_t *tag, QByteArray *body);
|
||||
bool readExactly(char *buf, int n);
|
||||
|
||||
QLocalSocket *m_sock = nullptr;
|
||||
uint32_t m_msize = 8192;
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -1,94 +1,230 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
#include "plumber.h"
|
||||
#include <QStandardPaths>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "ninep.h"
|
||||
#include "plumbmsg.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <QThread>
|
||||
|
||||
Plumber::Plumber(QObject *parent)
|
||||
: QObject(parent)
|
||||
#include <unistd.h>
|
||||
|
||||
// Canonicalise the X11 display the way plan9 getns() does: "xxx:0.0" -> "xxx:0"
|
||||
// and '/' -> '_'. Only the trailing ".0" is stripped.
|
||||
static QString canonicalDisplay(QString disp)
|
||||
{
|
||||
// Find plumb binary - check common plan9port locations
|
||||
QStringList searchPaths = {
|
||||
"/usr/local/plan9/bin/plumb",
|
||||
"/usr/lib/plan9/bin/plumb",
|
||||
QDir::homePath() + "/.local/plan9/bin/plumb",
|
||||
QDir::homePath() + "/plan9port/bin/plumb"
|
||||
};
|
||||
const int colon = disp.lastIndexOf(QLatin1Char(':'));
|
||||
if (colon >= 0) {
|
||||
int p = colon + 1;
|
||||
while (p < disp.size() && disp[p].isDigit())
|
||||
p++;
|
||||
if (disp.mid(p) == QLatin1String(".0"))
|
||||
disp.truncate(p);
|
||||
}
|
||||
disp.replace(QLatin1Char('/'), QLatin1Char('_'));
|
||||
return disp;
|
||||
}
|
||||
|
||||
// Also check PLAN9 environment variable
|
||||
QString plan9 = qEnvironmentVariable("PLAN9");
|
||||
if (!plan9.isEmpty()) {
|
||||
searchPaths.prepend(plan9 + "/bin/plumb");
|
||||
QString Plumber::socketPath()
|
||||
{
|
||||
const QByteArray ns = qgetenv("NAMESPACE");
|
||||
QString nsDir;
|
||||
if (!ns.isEmpty()) {
|
||||
nsDir = QString::fromLocal8Bit(ns);
|
||||
} else {
|
||||
const QByteArray disp = qgetenv("DISPLAY");
|
||||
if (disp.isEmpty())
|
||||
return QString(); // no namespace resolvable
|
||||
QString user = QString::fromLocal8Bit(qgetenv("USER"));
|
||||
if (user.isEmpty())
|
||||
user = QString::fromLocal8Bit(qgetenv("LOGNAME"));
|
||||
nsDir = QStringLiteral("/tmp/ns.%1.%2")
|
||||
.arg(user, canonicalDisplay(QString::fromLocal8Bit(disp)));
|
||||
}
|
||||
return nsDir + QStringLiteral("/plumb");
|
||||
}
|
||||
|
||||
// Current user name for the 9P attach
|
||||
static QString currentUser()
|
||||
{
|
||||
QString user = QString::fromLocal8Bit(qgetenv("USER"));
|
||||
if (user.isEmpty())
|
||||
user = QString::fromLocal8Bit(qgetenv("LOGNAME"));
|
||||
if (user.isEmpty())
|
||||
user = QStringLiteral("none");
|
||||
return user;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PlumbReader: owns a NineP session on the edit 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.
|
||||
// ---------------------------------------------------------------------------
|
||||
class PlumbReader : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PlumbReader(const QString &socketPath, QObject *parent = nullptr)
|
||||
: QThread(parent)
|
||||
, m_socketPath(socketPath)
|
||||
{
|
||||
}
|
||||
|
||||
for (const QString &path : searchPaths) {
|
||||
if (QFileInfo::exists(path)) {
|
||||
m_plumbPath = path;
|
||||
qDebug() << "Plumber: found plumb at" << m_plumbPath;
|
||||
break;
|
||||
void stop()
|
||||
{
|
||||
m_stop.storeRelaxed(1);
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void message(const QString &file, const QString &addr, const QString &wdir);
|
||||
void failed(const QString &message);
|
||||
|
||||
protected:
|
||||
void run() override
|
||||
{
|
||||
NineP nine;
|
||||
if (!nine.connectAndAttach(m_socketPath, currentUser())) {
|
||||
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()));
|
||||
return;
|
||||
}
|
||||
|
||||
static constexpr int PollMs = 200;
|
||||
bool pending = false;
|
||||
while (!m_stop.loadRelaxed()) {
|
||||
if (!pending) {
|
||||
if (!nine.beginRead(EditFid, 0, nine.msize())) {
|
||||
Q_EMIT failed(QStringLiteral("plumb read: %1").arg(nine.errorString()));
|
||||
break;
|
||||
}
|
||||
pending = true;
|
||||
}
|
||||
QByteArray buf;
|
||||
bool timedOut = false;
|
||||
const int n = nine.recvReadReply(&buf, PollMs, &timedOut);
|
||||
if (timedOut)
|
||||
continue;
|
||||
if (n < 0) {
|
||||
if (!m_stop.loadRelaxed())
|
||||
Q_EMIT failed(QStringLiteral("plumb read: %1").arg(nine.errorString()));
|
||||
break;
|
||||
}
|
||||
pending = false;
|
||||
if (n == 0)
|
||||
continue;
|
||||
PlumbMsg m;
|
||||
if (PlumbMsg::unpack(buf, &m)) {
|
||||
Q_EMIT message(QString::fromUtf8(m.data), m.lookup(QStringLiteral("addr")),
|
||||
m.wdir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_plumbPath.isEmpty()) {
|
||||
qDebug() << "Plumber: plumb binary not found, B3-plumb disabled";
|
||||
private:
|
||||
QString m_socketPath;
|
||||
QAtomicInt m_stop{0};
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plumber
|
||||
// ---------------------------------------------------------------------------
|
||||
Plumber::Plumber(QObject *parent)
|
||||
: 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;
|
||||
}
|
||||
|
||||
bool Plumber::available() const
|
||||
{
|
||||
return !m_plumbPath.isEmpty();
|
||||
const QString path = socketPath();
|
||||
if (path.isEmpty())
|
||||
return false;
|
||||
return QFileInfo::exists(path);
|
||||
}
|
||||
|
||||
void Plumber::plumb(const QString &text)
|
||||
bool Plumber::send(const QString &data, const QString &wdir)
|
||||
{
|
||||
if (text.isEmpty()) return;
|
||||
return sendTo(data, QString(), wdir);
|
||||
}
|
||||
|
||||
if (!available()) {
|
||||
emit plumbError("plumb binary not found");
|
||||
return;
|
||||
bool Plumber::sendTo(const QString &data, const QString &dst, const QString &wdir)
|
||||
{
|
||||
if (data.isEmpty())
|
||||
return false;
|
||||
|
||||
const QString path = socketPath();
|
||||
if (path.isEmpty()) {
|
||||
m_error = QStringLiteral("no plumb namespace ($NAMESPACE/$DISPLAY unset)");
|
||||
Q_EMIT plumbError(m_error);
|
||||
return false;
|
||||
}
|
||||
|
||||
QProcess *proc = new QProcess(this);
|
||||
connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this, [this, proc](int exitCode, QProcess::ExitStatus status) {
|
||||
if (exitCode != 0 || status != QProcess::NormalExit) {
|
||||
QString err = proc->readAllStandardError();
|
||||
if (!err.isEmpty()) {
|
||||
emit plumbError(err);
|
||||
}
|
||||
}
|
||||
proc->deleteLater();
|
||||
});
|
||||
|
||||
// plumb reads from stdin
|
||||
proc->start(m_plumbPath, QStringList());
|
||||
proc->write(text.toUtf8());
|
||||
proc->closeWriteChannel();
|
||||
}
|
||||
|
||||
void Plumber::plumbTo(const QString &text, const QString &dst)
|
||||
{
|
||||
if (text.isEmpty()) return;
|
||||
|
||||
if (!available()) {
|
||||
emit plumbError("plumb binary not found");
|
||||
return;
|
||||
NineP nine;
|
||||
if (!nine.connectAndAttach(path, currentUser())) {
|
||||
m_error = nine.errorString();
|
||||
Q_EMIT plumbError(m_error);
|
||||
return false;
|
||||
}
|
||||
static constexpr uint32_t SendFid = 1;
|
||||
if (!nine.walk(SendFid, QStringLiteral("send")) || !nine.open(SendFid, NineP::OWRITE)) {
|
||||
m_error = nine.errorString();
|
||||
Q_EMIT plumbError(m_error);
|
||||
return false;
|
||||
}
|
||||
|
||||
QProcess *proc = new QProcess(this);
|
||||
connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this, [this, proc](int exitCode, QProcess::ExitStatus status) {
|
||||
if (exitCode != 0 || status != QProcess::NormalExit) {
|
||||
QString err = proc->readAllStandardError();
|
||||
if (!err.isEmpty()) {
|
||||
emit plumbError(err);
|
||||
}
|
||||
}
|
||||
proc->deleteLater();
|
||||
});
|
||||
PlumbMsg m;
|
||||
m.src = QStringLiteral("ollie");
|
||||
m.dst = dst;
|
||||
m.wdir = wdir.isEmpty() ? QDir::currentPath() : wdir;
|
||||
m.type = QStringLiteral("text");
|
||||
m.data = data.toUtf8();
|
||||
|
||||
// plumb -d destination reads from stdin
|
||||
proc->start(m_plumbPath, QStringList() << "-d" << dst);
|
||||
proc->write(text.toUtf8());
|
||||
proc->closeWriteChannel();
|
||||
const QByteArray packed = m.pack();
|
||||
const int w = nine.write(SendFid, 0, packed);
|
||||
if (w != packed.size()) {
|
||||
m_error = nine.errorString().isEmpty()
|
||||
? QStringLiteral("short plumb write (%1/%2)").arg(w).arg(packed.size())
|
||||
: nine.errorString();
|
||||
Q_EMIT plumbError(m_error);
|
||||
return false;
|
||||
}
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
#include "plumber.moc"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,30 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Plumber — ollie-gui's bridge to the plan9port plumbing system.
|
||||
*
|
||||
* Full plumb client:
|
||||
* - SEND: send() opens the "send" port on the plumber and writes a packed
|
||||
* 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.
|
||||
*
|
||||
* The namespace socket is $NAMESPACE/plumb, or /tmp/ns.$USER.$DISPLAY/plumb
|
||||
* when $NAMESPACE is unset — the same resolution plan9's getns() performs.
|
||||
*/
|
||||
#ifndef PLUMBER_H
|
||||
#define PLUMBER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QString>
|
||||
#include <QQmlEngine>
|
||||
|
||||
// Plumber wraps plan9port's plumb command for context-aware actions.
|
||||
class PlumbReader; // reader living on its own thread
|
||||
|
||||
// Plumber wraps plan9port plumbing for context-aware actions.
|
||||
// B3-click on text → plumber routes to appropriate handler (editor, browser, etc.)
|
||||
class Plumber : public QObject
|
||||
{
|
||||
|
|
@ -15,21 +34,53 @@ class Plumber : public QObject
|
|||
|
||||
public:
|
||||
explicit Plumber(QObject *parent = nullptr);
|
||||
~Plumber() override;
|
||||
|
||||
// Plumb text to the plan9port plumber daemon
|
||||
Q_INVOKABLE void plumb(const QString &text);
|
||||
/*! Resolve the plumb socket path ($NAMESPACE/plumb or display fallback). */
|
||||
static QString socketPath();
|
||||
|
||||
// Plumb with explicit source/destination
|
||||
Q_INVOKABLE void plumbTo(const QString &text, const QString &dst);
|
||||
/*!
|
||||
* Build and send a plumb message carrying \a data (a token, path or URL)
|
||||
* with working directory \a wdir. Returns true if the plumber accepted the
|
||||
* write. On failure errorString() explains why.
|
||||
*/
|
||||
Q_INVOKABLE bool send(const QString &data, const QString &wdir = QString());
|
||||
|
||||
// Check if plumber is available
|
||||
/*! Plumb text to a specific destination port. */
|
||||
Q_INVOKABLE bool sendTo(const QString &data, const QString &dst, const QString &wdir = QString());
|
||||
|
||||
/*! Last send() error. */
|
||||
Q_INVOKABLE QString errorString() const { return m_error; }
|
||||
|
||||
/*! Check if plumber namespace is available. */
|
||||
Q_INVOKABLE bool available() const;
|
||||
|
||||
signals:
|
||||
// Legacy compatibility aliases
|
||||
Q_INVOKABLE void plumb(const QString &text) { send(text); }
|
||||
Q_INVOKABLE void plumbTo(const QString &text, const QString &dst) { sendTo(text, dst); }
|
||||
|
||||
/*!
|
||||
* Start the edit-port reader thread. Safe to call once; further calls are
|
||||
* no-ops. Reader failures are reported via readerError().
|
||||
*/
|
||||
Q_INVOKABLE void startReader();
|
||||
|
||||
Q_SIGNALS:
|
||||
/*!
|
||||
* A plumb message arrived on the edit port: open \a file and, if \a addr is
|
||||
* non-empty, jump to it. Emitted on the GUI (Plumber's) thread.
|
||||
*/
|
||||
void edit(const QString &file, const QString &addr, const QString &wdir);
|
||||
|
||||
/*! The reader stopped with an error (e.g. plumber not running). */
|
||||
void readerError(const QString &message);
|
||||
|
||||
/*! Send failed. */
|
||||
void plumbError(const QString &error);
|
||||
|
||||
private:
|
||||
QString m_plumbPath;
|
||||
PlumbReader *m_reader = nullptr;
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
#endif // PLUMBER_H
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
#include "plumbmsg.h"
|
||||
|
||||
QString PlumbMsg::lookup(const QString &name) const
|
||||
{
|
||||
for (const PlumbAttr &a : attr) {
|
||||
if (a.name == name)
|
||||
return a.value;
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void PlumbMsg::setAttr(const QString &name, const QString &value)
|
||||
{
|
||||
for (PlumbAttr &a : attr) {
|
||||
if (a.name == name) {
|
||||
a.value = value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
attr.append(PlumbAttr{name, value});
|
||||
}
|
||||
|
||||
// Quote one attribute value if it contains space, ', = or tab
|
||||
static QString quoteAttrValue(const QString &v)
|
||||
{
|
||||
bool needsQuote = false;
|
||||
for (const QChar c : v) {
|
||||
if (c == QLatin1Char(' ') || c == QLatin1Char('\'')
|
||||
|| c == QLatin1Char('=') || c == QLatin1Char('\t')) {
|
||||
needsQuote = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!needsQuote)
|
||||
return v;
|
||||
QString out;
|
||||
out.reserve(v.size() + 2);
|
||||
out.append(QLatin1Char('\''));
|
||||
for (const QChar c : v) {
|
||||
out.append(c);
|
||||
if (c == QLatin1Char('\''))
|
||||
out.append(c); // double it
|
||||
}
|
||||
out.append(QLatin1Char('\''));
|
||||
return out;
|
||||
}
|
||||
|
||||
QByteArray packPlumbAttr(const QList<PlumbAttr> &attr)
|
||||
{
|
||||
QString s;
|
||||
bool first = true;
|
||||
for (const PlumbAttr &a : attr) {
|
||||
if (!first)
|
||||
s.append(QLatin1Char(' '));
|
||||
first = false;
|
||||
s.append(a.name);
|
||||
s.append(QLatin1Char('='));
|
||||
s.append(quoteAttrValue(a.value));
|
||||
}
|
||||
return s.toUtf8();
|
||||
}
|
||||
|
||||
QList<PlumbAttr> unpackPlumbAttr(const QByteArray &line)
|
||||
{
|
||||
QList<PlumbAttr> out;
|
||||
const QString s = QString::fromUtf8(line);
|
||||
int i = 0;
|
||||
const int n = s.size();
|
||||
while (i < n) {
|
||||
while (i < n && (s[i] == QLatin1Char(' ') || s[i] == QLatin1Char('\t')))
|
||||
i++;
|
||||
if (i >= n)
|
||||
break;
|
||||
int nameStart = i;
|
||||
while (i < n && s[i] != QLatin1Char('=') && s[i] != QLatin1Char(' ')
|
||||
&& s[i] != QLatin1Char('\t'))
|
||||
i++;
|
||||
if (i >= n || s[i] != QLatin1Char('='))
|
||||
break;
|
||||
const QString name = s.mid(nameStart, i - nameStart);
|
||||
i++; // skip '='
|
||||
QString value;
|
||||
bool quoting = false;
|
||||
while (i < n) {
|
||||
const QChar c = s[i];
|
||||
if (quoting) {
|
||||
if (c == QLatin1Char('\'')) {
|
||||
if (i + 1 < n && s[i + 1] == QLatin1Char('\'')) {
|
||||
i++;
|
||||
} else {
|
||||
quoting = false;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (c == QLatin1Char(' ') || c == QLatin1Char('\t'))
|
||||
break;
|
||||
if (c == QLatin1Char('\'')) {
|
||||
quoting = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
value.append(c);
|
||||
i++;
|
||||
}
|
||||
out.append(PlumbAttr{name, value});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QByteArray PlumbMsg::pack() const
|
||||
{
|
||||
QByteArray buf;
|
||||
buf.append(src.toUtf8());
|
||||
buf.append('\n');
|
||||
buf.append(dst.toUtf8());
|
||||
buf.append('\n');
|
||||
buf.append(wdir.toUtf8());
|
||||
buf.append('\n');
|
||||
buf.append(type.toUtf8());
|
||||
buf.append('\n');
|
||||
buf.append(packPlumbAttr(attr));
|
||||
buf.append('\n');
|
||||
buf.append(QByteArray::number(data.size()));
|
||||
buf.append('\n');
|
||||
buf.append(data);
|
||||
return buf;
|
||||
}
|
||||
|
||||
bool PlumbMsg::unpack(const QByteArray &buf, PlumbMsg *out)
|
||||
{
|
||||
if (!out)
|
||||
return false;
|
||||
int pos = 0;
|
||||
const int n = buf.size();
|
||||
auto nextLine = [&](QByteArray *line) -> bool {
|
||||
int nl = buf.indexOf('\n', pos);
|
||||
if (nl < 0)
|
||||
return false;
|
||||
*line = buf.mid(pos, nl - pos);
|
||||
pos = nl + 1;
|
||||
return true;
|
||||
};
|
||||
|
||||
QByteArray srcL, dstL, wdirL, typeL, attrL, ndataL;
|
||||
if (!nextLine(&srcL) || !nextLine(&dstL) || !nextLine(&wdirL)
|
||||
|| !nextLine(&typeL) || !nextLine(&attrL) || !nextLine(&ndataL))
|
||||
return false;
|
||||
|
||||
PlumbMsg m;
|
||||
m.src = QString::fromUtf8(srcL);
|
||||
m.dst = QString::fromUtf8(dstL);
|
||||
m.wdir = QString::fromUtf8(wdirL);
|
||||
m.type = QString::fromUtf8(typeL);
|
||||
m.attr = unpackPlumbAttr(attrL);
|
||||
|
||||
bool ok = false;
|
||||
ndataL.trimmed().toInt(&ok);
|
||||
if (!ok)
|
||||
return false;
|
||||
m.data = buf.mid(pos, n - pos);
|
||||
|
||||
*out = m;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* PlumbMsg — a plan9port plumbing message and its wire codec.
|
||||
*
|
||||
* Wire format (plan9port libplumb/mesg.c):
|
||||
* src\n source application ("ollie")
|
||||
* dst\n destination port ("" lets the rules decide)
|
||||
* wdir\n working directory (resolves relative paths)
|
||||
* type\n data type ("text")
|
||||
* attr\n space-separated name=value attributes
|
||||
* ndata\n decimal byte count of the data that follows
|
||||
* <ndata bytes> the data (NOT newline-terminated)
|
||||
*/
|
||||
#ifndef OLLIE_PLUMBMSG_H
|
||||
#define OLLIE_PLUMBMSG_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
struct PlumbAttr {
|
||||
QString name;
|
||||
QString value;
|
||||
bool operator==(const PlumbAttr &o) const
|
||||
{
|
||||
return name == o.name && value == o.value;
|
||||
}
|
||||
};
|
||||
|
||||
struct PlumbMsg {
|
||||
QString src;
|
||||
QString dst;
|
||||
QString wdir;
|
||||
QString type;
|
||||
QList<PlumbAttr> attr;
|
||||
QByteArray data;
|
||||
|
||||
QString lookup(const QString &name) const;
|
||||
void setAttr(const QString &name, const QString &value);
|
||||
QByteArray pack() const;
|
||||
static bool unpack(const QByteArray &buf, PlumbMsg *out);
|
||||
};
|
||||
|
||||
QByteArray packPlumbAttr(const QList<PlumbAttr> &attr);
|
||||
QList<PlumbAttr> unpackPlumbAttr(const QByteArray &line);
|
||||
|
||||
#endif
|
||||
Loading…
Reference in New Issue