diff --git a/CMakeLists.txt b/CMakeLists.txt index 495ff72..439f371 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,13 +87,17 @@ if(OLLIE_KF5) add_executable(ollie-gui gui/main.cpp gui/ollie9pclient.cpp + gui/lib9pclient.cpp gui/ninepconnection.cpp gui/chatblockmodel.cpp gui/thememanager.cpp gui/streamfsm.cpp ${GUI_KF5_QRC} ) - target_include_directories(ollie-gui PRIVATE ${CMAKE_SOURCE_DIR}/gui) + target_include_directories(ollie-gui PRIVATE + ${CMAKE_SOURCE_DIR}/gui + $ENV{HOME}/.local/include + ) target_link_libraries(ollie-gui Qt5::Core Qt5::Widgets @@ -101,6 +105,7 @@ if(OLLIE_KF5) Qt5::QuickControls2 KF5::ConfigCore KF5::SyntaxHighlighting + $ENV{HOME}/.local/lib/libollie9p.so ) # --- Install --- @@ -148,6 +153,7 @@ else() add_executable(ollie-gui gui/main.cpp gui/ollie9pclient.cpp + gui/lib9pclient.cpp gui/ninepconnection.cpp gui/chatblockmodel.cpp gui/thememanager.cpp @@ -155,6 +161,7 @@ else() ) target_include_directories(ollie-gui PRIVATE ${CMAKE_SOURCE_DIR}/gui + $ENV{HOME}/.local/include ) target_link_libraries(ollie-gui Qt6::Core @@ -163,6 +170,7 @@ else() Qt6::QuickControls2 KF6::ConfigCore KF6::SyntaxHighlighting + $ENV{HOME}/.local/lib/libollie9p.so ) qt_add_resources(ollie-gui "gui" PREFIX "/" diff --git a/gui/lib9pclient.cpp b/gui/lib9pclient.cpp new file mode 100644 index 0000000..924102c --- /dev/null +++ b/gui/lib9pclient.cpp @@ -0,0 +1,245 @@ +#include "lib9pclient.h" + +Lib9pClient::Lib9pClient(QObject *parent) + : QObject(parent) +{ +} + +Lib9pClient::~Lib9pClient() +{ + disconnect(); +} + +void Lib9pClient::updateLastError() +{ + char *err = ollie9p_last_error(); + if (err) { + m_lastError = QString::fromUtf8(err); + ollie9p_free(err); + } else { + m_lastError.clear(); + } +} + +bool Lib9pClient::connect(const QString &addr, const QString &user, const QString &aname) +{ + if (m_handle > 0) { + disconnect(); + } + + QByteArray addrUtf8 = addr.toUtf8(); + QByteArray userUtf8 = user.toUtf8(); + QByteArray anameUtf8 = aname.toUtf8(); + + m_handle = ollie9p_connect(addrUtf8.data(), userUtf8.data(), anameUtf8.data()); + if (m_handle < 0) { + updateLastError(); + return false; + } + m_lastError.clear(); + return true; +} + +bool Lib9pClient::connectDefault() +{ + if (m_handle > 0) { + disconnect(); + } + + m_handle = ollie9p_connect_default(); + if (m_handle < 0) { + updateLastError(); + return false; + } + m_lastError.clear(); + return true; +} + +void Lib9pClient::disconnect() +{ + if (m_handle > 0) { + ollie9p_close(m_handle); + m_handle = -1; + } +} + +QByteArray Lib9pClient::read(const QString &path) +{ + if (m_handle <= 0) { + m_lastError = QStringLiteral("not connected"); + return {}; + } + + QByteArray pathUtf8 = path.toUtf8(); + int len = 0; + char *data = ollie9p_read(m_handle, pathUtf8.data(), &len); + if (!data) { + updateLastError(); + return {}; + } + + QByteArray result(data, len); + ollie9p_free(data); + m_lastError.clear(); + return result; +} + +bool Lib9pClient::write(const QString &path, const QByteArray &data) +{ + if (m_handle <= 0) { + m_lastError = QStringLiteral("not connected"); + return false; + } + + QByteArray pathUtf8 = path.toUtf8(); + int written = ollie9p_write(m_handle, pathUtf8.data(), + const_cast(data.data()), data.size()); + if (written < 0) { + updateLastError(); + return false; + } + m_lastError.clear(); + return true; +} + +QByteArray Lib9pClient::rdwr(const QString &path, const QByteArray &data) +{ + if (m_handle <= 0) { + m_lastError = QStringLiteral("not connected"); + return {}; + } + + QByteArray pathUtf8 = path.toUtf8(); + int outLen = 0; + char *resp = ollie9p_rdwr(m_handle, pathUtf8.data(), + const_cast(data.data()), data.size(), &outLen); + if (!resp) { + updateLastError(); + return {}; + } + + QByteArray result(resp, outLen); + ollie9p_free(resp); + m_lastError.clear(); + return result; +} + +QStringList Lib9pClient::ls(const QString &path) +{ + if (m_handle <= 0) { + m_lastError = QStringLiteral("not connected"); + return {}; + } + + QByteArray pathUtf8 = path.toUtf8(); + char *names = ollie9p_ls(m_handle, pathUtf8.data()); + if (!names) { + updateLastError(); + return {}; + } + + QString str = QString::fromUtf8(names); + ollie9p_free(names); + m_lastError.clear(); + + if (str.isEmpty()) { + return {}; + } + return str.split(QLatin1Char('\n'), Qt::SkipEmptyParts); +} + +Lib9pClient::StatResult Lib9pClient::stat(const QString &path) +{ + StatResult result; + if (m_handle <= 0) { + m_lastError = QStringLiteral("not connected"); + return result; + } + + QByteArray pathUtf8 = path.toUtf8(); + char *data = ollie9p_stat(m_handle, pathUtf8.data()); + if (!data) { + updateLastError(); + return result; + } + + // Parse "mode\tsize\tmtime\tname" + QString str = QString::fromUtf8(data); + ollie9p_free(data); + + QStringList parts = str.split(QLatin1Char('\t')); + if (parts.size() >= 4) { + result.mode = parts[0]; + result.size = parts[1].toLongLong(); + result.mtime = parts[2].toLongLong(); + result.name = parts[3]; + result.valid = true; + } + m_lastError.clear(); + return result; +} + +bool Lib9pClient::remove(const QString &path) +{ + if (m_handle <= 0) { + m_lastError = QStringLiteral("not connected"); + return false; + } + + QByteArray pathUtf8 = path.toUtf8(); + if (ollie9p_remove(m_handle, pathUtf8.data()) < 0) { + updateLastError(); + return false; + } + m_lastError.clear(); + return true; +} + +bool Lib9pClient::create(const QString &path, uint perm) +{ + if (m_handle <= 0) { + m_lastError = QStringLiteral("not connected"); + return false; + } + + QByteArray pathUtf8 = path.toUtf8(); + if (ollie9p_create(m_handle, pathUtf8.data(), perm) < 0) { + updateLastError(); + return false; + } + m_lastError.clear(); + return true; +} + +bool Lib9pClient::mkdir(const QString &path) +{ + if (m_handle <= 0) { + m_lastError = QStringLiteral("not connected"); + return false; + } + + QByteArray pathUtf8 = path.toUtf8(); + if (ollie9p_mkdir(m_handle, pathUtf8.data()) < 0) { + updateLastError(); + return false; + } + m_lastError.clear(); + return true; +} + +bool Lib9pClient::rename(const QString &oldPath, const QString &newName) +{ + if (m_handle <= 0) { + m_lastError = QStringLiteral("not connected"); + return false; + } + + QByteArray oldPathUtf8 = oldPath.toUtf8(); + QByteArray newNameUtf8 = newName.toUtf8(); + if (ollie9p_rename(m_handle, oldPathUtf8.data(), newNameUtf8.data()) < 0) { + updateLastError(); + return false; + } + m_lastError.clear(); + return true; +} diff --git a/gui/lib9pclient.h b/gui/lib9pclient.h new file mode 100644 index 0000000..4ec48cd --- /dev/null +++ b/gui/lib9pclient.h @@ -0,0 +1,58 @@ +#ifndef LIB9PCLIENT_H +#define LIB9PCLIENT_H + +#include +#include +#include +#include + +// Qt wrapper for libollie9p - provides a clean C++ interface to the Go 9P library. +// Single persistent connection, thread-safe (Go handles internal locking). +class Lib9pClient : public QObject +{ + Q_OBJECT +public: + explicit Lib9pClient(QObject *parent = nullptr); + ~Lib9pClient() override; + + // Connection management + bool connect(const QString &addr, const QString &user, const QString &aname); + bool connectDefault(); // Uses NAMESPACE and USER env vars + void disconnect(); + bool isConnected() const { return m_handle > 0; } + + // File operations - all return empty/false on error, check lastError() + QByteArray read(const QString &path); + bool write(const QString &path, const QByteArray &data); + QByteArray rdwr(const QString &path, const QByteArray &data); // Write then read + + // Directory operations + QStringList ls(const QString &path); + + // Metadata + struct StatResult { + QString mode; + qint64 size; + qint64 mtime; + QString name; + bool valid = false; + }; + StatResult stat(const QString &path); + + // File management + bool remove(const QString &path); + bool create(const QString &path, uint perm = 0666); + bool mkdir(const QString &path); + bool rename(const QString &oldPath, const QString &newName); + + // Error handling + QString lastError() const { return m_lastError; } + +private: + void updateLastError(); + + int m_handle = -1; + QString m_lastError; +}; + +#endif // LIB9PCLIENT_H diff --git a/gui/ollie9pclient.cpp b/gui/ollie9pclient.cpp index b615bf6..e6da079 100644 --- a/gui/ollie9pclient.cpp +++ b/gui/ollie9pclient.cpp @@ -81,12 +81,22 @@ Ollie9pClient::Ollie9pClient(QObject *parent) m_daemon = new NinePConnection(this); connect(m_daemon, &NinePConnection::connected, this, [this]() { setDaemonConnected(true); + // Initialize native 9P client for fast operations + if (!m_9p) { + m_9p = new Lib9pClient(this); + } + if (!m_9p->isConnected()) { + m_9p->connectDefault(); + } refreshSessions(); ensureRootDataLoaded(); startAgentConnections(); }); connect(m_daemon, &NinePConnection::disconnected, this, [this]() { setDaemonConnected(false); + if (m_9p) { + m_9p->disconnect(); + } stopAgentConnections(); }); connect(m_daemon, &NinePConnection::readyRead, this, [this](const QByteArray &data) { @@ -342,7 +352,14 @@ QString Ollie9pClient::agentState(const QString &sessionId, const QString &agent void Ollie9pClient::refreshSessions() { if (!m_daemonConnected) return; - QByteArray out = run9p({"read", "session/idx"}); + + // Use native 9P client if available (fast path) + QByteArray out; + if (m_9p && m_9p->isConnected()) { + out = m_9p->read(QStringLiteral("session/idx")); + } else { + out = run9p({"read", "session/idx"}); + } QString raw = QString::fromUtf8(out); m_sessions.clear(); @@ -615,7 +632,12 @@ void Ollie9pClient::switchAgent(const QString &sessionId, const QString &agentId void Ollie9pClient::loadRootBackends() { if (m_rootBackendsLoaded) return; - QByteArray out = run9p({"read", "backends"}); + QByteArray out; + if (m_9p && m_9p->isConnected()) { + out = m_9p->read(QStringLiteral("backends")); + } else { + out = run9p({"read", "backends"}); + } QString raw = QString::fromUtf8(out).trimmed(); if (!raw.isEmpty()) { m_availableBackends = raw.split('\n', Qt::SkipEmptyParts); @@ -628,7 +650,12 @@ void Ollie9pClient::loadRootBackends() void Ollie9pClient::loadRootAgents() { if (m_rootAgentsLoaded) return; - QByteArray out = run9p({"read", "agents"}); + QByteArray out; + if (m_9p && m_9p->isConnected()) { + out = m_9p->read(QStringLiteral("agents")); + } else { + out = run9p({"read", "agents"}); + } QString raw = QString::fromUtf8(out).trimmed(); if (!raw.isEmpty()) { m_availableAgents = raw.split('\n', Qt::SkipEmptyParts); diff --git a/gui/ollie9pclient.h b/gui/ollie9pclient.h index 4175dcc..dde6b3f 100644 --- a/gui/ollie9pclient.h +++ b/gui/ollie9pclient.h @@ -11,6 +11,7 @@ #include "streamfsm.h" #include "ninepconnection.h" +#include "lib9pclient.h" // 9P-based client for ollie. Uses ollie-9p subprocess for transport. // Streaming files (chat, statewait) use StreamFsm instances for lifecycle. @@ -114,7 +115,7 @@ private: void stopAgentStreams(); void stopStreams(); QString agentPath() const; - QByteArray run9p(const QStringList &args); + QByteArray run9p(const QStringList &args); // Fallback for streaming (uses subprocess) void ensureRootDataLoaded(); void setDaemonConnected(bool connected); void startAgentConnections(); @@ -123,6 +124,9 @@ private: void stopAgentConnections(); void handleEvent(const QString &eventLine); + // Native 9P client (persistent connection, no subprocess overhead) + Lib9pClient *m_9p = nullptr; + QVariantList m_sessions; QString m_activeSessionId; QString m_activeState = "idle";