diff --git a/CMakeLists.txt b/CMakeLists.txt index 87465a3..41b41ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ if(OLLIE_KF5) Runner CoreAddons Config + KIO TextEditor SyntaxHighlighting ) @@ -77,6 +78,25 @@ if(OLLIE_KF5) $ENV{HOME}/.local/lib/libollie9p.so ) + # --- KIO Worker (ollie:// protocol) --- + kcoreaddons_add_plugin(kio_ollie SOURCES + kio/ollie_kio.cpp + gui/lib9pclient.cpp + INSTALL_NAMESPACE "kf5/kio" + ) + target_include_directories(kio_ollie PRIVATE + ${CMAKE_SOURCE_DIR}/gui + ${CMAKE_SOURCE_DIR}/kio + $ENV{HOME}/.local/include + ) + target_link_libraries(kio_ollie + KF5::KIOCore + Qt5::Core + Qt5::Network + $ENV{HOME}/.local/lib/libollie9p.so + ) + install(FILES kio/ollie.protocol DESTINATION ${KDE_INSTALL_KSERVICESDIR}) + # --- GUI App (9P-based) --- # Strip Qt6-only grouped palette syntax (palette.active.*, palette.inactive.*, # palette.disabled.*) — Qt5 QML doesn't support color-group qualifiers. @@ -125,6 +145,7 @@ else() find_package(Qt6 REQUIRED COMPONENTS Core Widgets Quick QuickControls2 Network) find_package(KF6 REQUIRED COMPONENTS Runner CoreAddons) find_package(KF6Config REQUIRED) + find_package(KF6KIO REQUIRED) # KDE's KDE_INSTALL_PLUGINDIR defaults to lib64/plugins/ but Qt6 plugins # live under lib64/qt6/plugins/. Override so kcoreaddons_add_plugin installs @@ -159,6 +180,24 @@ else() $ENV{HOME}/.local/lib/libollie9p.so ) + # --- KIO Worker (ollie:// protocol) --- + kcoreaddons_add_plugin(kio_ollie SOURCES + kio/ollie_kio.cpp + gui/lib9pclient.cpp + INSTALL_NAMESPACE "kf6/kio" + ) + target_include_directories(kio_ollie PRIVATE + ${CMAKE_SOURCE_DIR}/gui + ${CMAKE_SOURCE_DIR}/kio + $ENV{HOME}/.local/include + ) + target_link_libraries(kio_ollie + KF6::KIOCore + Qt6::Core + Qt6::Network + $ENV{HOME}/.local/lib/libollie9p.so + ) + # --- GUI App (9P-based, no D-Bus) --- find_package(KF6SyntaxHighlighting REQUIRED) add_executable(ollie-gui diff --git a/justfile b/justfile index 30552e7..8c4f7db 100644 --- a/justfile +++ b/justfile @@ -56,9 +56,11 @@ uninstall: rm -f ~/.local/share/applications/org.ollie.gui.desktop rm -f ~/.local/share/icons/hicolor/scalable/apps/ollie.svg rm -f ~/.local/lib64/qt6/plugins/kf6/krunner/krunner_ollie.so - rm -f ~/.local/lib64/qt6/plugins/ktexteditor/ollie_kate.so + rm -f ~/.local/lib64/qt6/plugins/kf6/ktexteditor/ollie_kate.so + rm -f ~/.local/lib64/qt6/plugins/kf6/kio/kio_ollie.so rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop rm -f ~/.config/plasma-workspace/env/99-ollie.sh + kbuildsycoca6 --noincremental 2>/dev/null; true kquitapp6 krunner 2>/dev/null; true @echo '=== KDE uninstall complete ===' @@ -70,7 +72,10 @@ uninstall-kf5: rm -f ~/.local/share/icons/hicolor/scalable/apps/ollie.svg rm -f ~/.local/lib/qt5/plugins/kf5/krunner/krunner_ollie.so rm -f ~/.local/lib/qt5/plugins/ktexteditor/ollie_kate.so + rm -f ~/.local/lib/qt5/plugins/kf5/kio/kio_ollie.so rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop + rm -f ~/.local/share/kservices5/ollie.protocol rm -f ~/.config/plasma-workspace/env/99-ollie-kf5.sh + kbuildsycoca5 --noincremental 2>/dev/null; true kquitapp5 krunner 2>/dev/null; true @echo '=== KDE KF5 uninstall complete ===' \ No newline at end of file diff --git a/kio/ollie.json b/kio/ollie.json new file mode 100644 index 0000000..e6d358e --- /dev/null +++ b/kio/ollie.json @@ -0,0 +1,22 @@ +{ + "KDE-KIO-Protocols": { + "ollie": { + "Class": ":local", + "X-DocPath": "kio_ollie/index.html", + "exec": "kio_ollie", + "input": "none", + "output": "filesystem", + "protocol": "ollie", + "reading": true, + "writing": true, + "makedir": true, + "deleting": true, + "renaming": true, + "listing": "Name,Type,Size,Date,Access", + "opening": false, + "Icon": "ollie", + "Description": "Browse the Ollie 9P namespace", + "Description[x-test]": "xxBrowse the Ollie 9P namespacexx" + } + } +} diff --git a/kio/ollie.protocol b/kio/ollie.protocol new file mode 100644 index 0000000..db30a77 --- /dev/null +++ b/kio/ollie.protocol @@ -0,0 +1,15 @@ +[Protocol] +exec=kio_ollie +protocol=ollie +input=none +output=filesystem +reading=true +writing=true +makedir=true +deleting=true +renaming=true +listing=Name,Type,Size,Date,Access +opening=false +Icon=ollie +Class=:local +Description=Browse the Ollie 9P namespace diff --git a/kio/ollie_kio.cpp b/kio/ollie_kio.cpp new file mode 100644 index 0000000..73fb64a --- /dev/null +++ b/kio/ollie_kio.cpp @@ -0,0 +1,339 @@ +#include "ollie_kio.h" + +#include +#include +#include +#include +#include + +#include +#include + +using namespace KIO; + +// Plugin factory — KIO loads this .so and calls the appropriate create method +#if QT_VERSION_MAJOR >= 6 +class OllieWorkerFactory : public KIO::WorkerFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.kio.worker.ollie" FILE "ollie.json") +public: + using WorkerFactory::WorkerFactory; + std::unique_ptr createWorker(const QByteArray &pool, const QByteArray &app) override + { + return std::make_unique(pool, app); + } +}; +#else +class OllieWorkerFactory : public KIO::RealWorkerFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.kio.worker.ollie" FILE "ollie.json") +public: + using RealWorkerFactory::RealWorkerFactory; + std::unique_ptr createWorker(const QByteArray &pool, const QByteArray &app) override + { + Q_UNUSED(pool); Q_UNUSED(app); + return nullptr; // unused — KIO calls createRealWorker for RealWorkerFactory + } + std::unique_ptr createRealWorker(const QByteArray &pool, const QByteArray &app) override + { + return std::make_unique(pool, app); + } +}; +#endif + +// kdemain entry point — KF5 KIO launches workers by calling this +extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + app.setApplicationName(QStringLiteral("kio_ollie")); + + if (argc != 4) { + fprintf(stderr, "Usage: kio_ollie protocol pool_socket app_socket\n"); + return -1; + } + + OllieWorker worker(argv[2], argv[3]); + worker.dispatchLoop(); + return 0; +} + +OllieWorker::OllieWorker(const QByteArray &pool, const QByteArray &app) + : WorkerBase("ollie", pool, app) +{ +} + +OllieWorker::~OllieWorker() +{ + m_client.disconnect(); +} + +bool OllieWorker::ensureConnected() +{ + if (m_client.isConnected()) + return true; + return m_client.connectDefault(); +} + +QString OllieWorker::ninepPath(const QUrl &url) const +{ + QString path = url.path(); + // Remove leading slash — 9P root is "" + if (path.startsWith(QLatin1Char('/'))) + path = path.mid(1); + // Remove trailing slash + if (path.endsWith(QLatin1Char('/')) && path.length() > 1) + path.chop(1); + return path; +} + +bool OllieWorker::isDirectory(const QString &mode) const +{ + // 9P mode: starts with 'd' for directory, + // or numeric with Plan 9 DMDIR bit (0x80000000) + if (mode.startsWith(QLatin1Char('d'))) + return true; + bool ok; + uint modeVal = mode.toUInt(&ok, 0); + if (ok && (modeVal & 0x80000000)) + return true; + return false; +} + +mode_t OllieWorker::parseMode(const QString &modeStr) const +{ + // Try numeric first + bool ok; + uint modeVal = modeStr.toUInt(&ok, 0); + if (ok) { + return static_cast(modeVal & 0777); + } + // Parse rwx string (e.g. "drwxr-xr-x") + QString perms = modeStr; + if (perms.startsWith(QLatin1Char('d'))) + perms = perms.mid(1); + mode_t m = 0; + if (perms.length() >= 9) { + if (perms[0] == QLatin1Char('r')) m |= 0400; + if (perms[1] == QLatin1Char('w')) m |= 0200; + if (perms[2] == QLatin1Char('x')) m |= 0100; + if (perms[3] == QLatin1Char('r')) m |= 0040; + if (perms[4] == QLatin1Char('w')) m |= 0020; + if (perms[5] == QLatin1Char('x')) m |= 0010; + if (perms[6] == QLatin1Char('r')) m |= 0004; + if (perms[7] == QLatin1Char('w')) m |= 0002; + if (perms[8] == QLatin1Char('x')) m |= 0001; + } else { + m = 0644; + } + return m; +} + +KIO::UDSEntry OllieWorker::entryFromStat(const QString &path, const QString &name) +{ + UDSEntry entry; + auto st = m_client.stat(path); + + if (st.valid) { + bool dir = isDirectory(st.mode); + entry.fastInsert(UDSEntry::UDS_NAME, name.isEmpty() ? st.name : name); + entry.fastInsert(UDSEntry::UDS_SIZE, dir ? 0 : st.size); + entry.fastInsert(UDSEntry::UDS_MODIFICATION_TIME, st.mtime); + entry.fastInsert(UDSEntry::UDS_ACCESS_TIME, st.mtime); + + mode_t perm = parseMode(st.mode); + if (dir) { + entry.fastInsert(UDSEntry::UDS_FILE_TYPE, S_IFDIR); + entry.fastInsert(UDSEntry::UDS_ACCESS, perm | 0111); + } else { + entry.fastInsert(UDSEntry::UDS_FILE_TYPE, S_IFREG); + entry.fastInsert(UDSEntry::UDS_ACCESS, perm); + } + + if (!dir) { + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFile(name.isEmpty() ? st.name : name, + QMimeDatabase::MatchExtension); + if (mime.isValid() && !mime.isDefault()) { + entry.fastInsert(UDSEntry::UDS_MIME_TYPE, mime.name()); + } else { + entry.fastInsert(UDSEntry::UDS_MIME_TYPE, + QStringLiteral("text/plain")); + } + } + } else { + entry.fastInsert(UDSEntry::UDS_NAME, name); + } + + return entry; +} + +WorkerResult OllieWorker::listDir(const QUrl &url) +{ + if (!ensureConnected()) { + return WorkerResult::fail(ERR_CANNOT_CONNECT, + QStringLiteral("Cannot connect to Ollie 9P server: ") + + m_client.lastError()); + } + + QString path = ninepPath(url); + QStringList entries = m_client.ls(path); + if (entries.isEmpty() && !m_client.lastError().isEmpty()) { + return WorkerResult::fail(ERR_CANNOT_ENTER_DIRECTORY, + m_client.lastError()); + } + + // Emit "." entry for the directory itself + UDSEntry dot; + dot.fastInsert(UDSEntry::UDS_NAME, QStringLiteral(".")); + dot.fastInsert(UDSEntry::UDS_FILE_TYPE, S_IFDIR); + dot.fastInsert(UDSEntry::UDS_ACCESS, 0755); + listEntry(dot); + + for (const QString &name : entries) { + QString childPath = path.isEmpty() ? name : (path + QLatin1Char('/') + name); + UDSEntry entry = entryFromStat(childPath, name); + listEntry(entry); + } + + return WorkerResult::pass(); +} + +WorkerResult OllieWorker::stat(const QUrl &url) +{ + if (!ensureConnected()) { + return WorkerResult::fail(ERR_CANNOT_CONNECT, + QStringLiteral("Cannot connect to Ollie 9P server: ") + + m_client.lastError()); + } + + QString path = ninepPath(url); + QString name = url.fileName(); + if (name.isEmpty()) + name = QStringLiteral("."); + + UDSEntry entry = entryFromStat(path, name); + if (!entry.contains(UDSEntry::UDS_FILE_TYPE)) { + if (path.isEmpty()) { + // Root directory + entry.fastInsert(UDSEntry::UDS_NAME, QStringLiteral(".")); + entry.fastInsert(UDSEntry::UDS_FILE_TYPE, S_IFDIR); + entry.fastInsert(UDSEntry::UDS_ACCESS, 0755); + entry.fastInsert(UDSEntry::UDS_MIME_TYPE, + QStringLiteral("inode/directory")); + } else { + return WorkerResult::fail(ERR_DOES_NOT_EXIST, + m_client.lastError()); + } + } + + statEntry(entry); + return WorkerResult::pass(); +} + +WorkerResult OllieWorker::get(const QUrl &url) +{ + if (!ensureConnected()) { + return WorkerResult::fail(ERR_CANNOT_CONNECT, + QStringLiteral("Cannot connect to Ollie 9P server: ") + + m_client.lastError()); + } + + QString path = ninepPath(url); + QByteArray fileData = m_client.read(path); + if (fileData.isNull() && !m_client.lastError().isEmpty()) { + return WorkerResult::fail(ERR_CANNOT_READ, + m_client.lastError()); + } + + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFileNameAndData(url.fileName(), fileData); + mimeType(mime.name()); + + totalSize(fileData.size()); + data(fileData); + return WorkerResult::pass(); +} + +WorkerResult OllieWorker::put(const QUrl &url, int /*permissions*/, KIO::JobFlags /*flags*/) +{ + if (!ensureConnected()) { + return WorkerResult::fail(ERR_CANNOT_CONNECT, + QStringLiteral("Cannot connect to Ollie 9P server: ") + + m_client.lastError()); + } + + // Read incoming data from the job + QByteArray allData; + int result; + do { + QByteArray buffer; + dataReq(); + result = readData(buffer); + if (result > 0) + allData.append(buffer); + } while (result > 0); + + QString path = ninepPath(url); + if (!m_client.write(path, allData)) { + return WorkerResult::fail(ERR_CANNOT_WRITE, + m_client.lastError()); + } + + return WorkerResult::pass(); +} + +WorkerResult OllieWorker::mkdir(const QUrl &url, int /*permissions*/) +{ + if (!ensureConnected()) { + return WorkerResult::fail(ERR_CANNOT_CONNECT, + QStringLiteral("Cannot connect to Ollie 9P server: ") + + m_client.lastError()); + } + + QString path = ninepPath(url); + if (!m_client.mkdir(path)) { + return WorkerResult::fail(ERR_CANNOT_MKDIR, + m_client.lastError()); + } + + return WorkerResult::pass(); +} + +WorkerResult OllieWorker::del(const QUrl &url, bool /*isFile*/) +{ + if (!ensureConnected()) { + return WorkerResult::fail(ERR_CANNOT_CONNECT, + QStringLiteral("Cannot connect to Ollie 9P server: ") + + m_client.lastError()); + } + + QString path = ninepPath(url); + if (!m_client.remove(path)) { + return WorkerResult::fail(ERR_CANNOT_DELETE, + m_client.lastError()); + } + + return WorkerResult::pass(); +} + +WorkerResult OllieWorker::rename(const QUrl &src, const QUrl &dest, KIO::JobFlags /*flags*/) +{ + if (!ensureConnected()) { + return WorkerResult::fail(ERR_CANNOT_CONNECT, + QStringLiteral("Cannot connect to Ollie 9P server: ") + + m_client.lastError()); + } + + QString srcPath = ninepPath(src); + QString newName = dest.fileName(); + if (!m_client.rename(srcPath, newName)) { + return WorkerResult::fail(ERR_CANNOT_RENAME, + m_client.lastError()); + } + + return WorkerResult::pass(); +} + +#include "ollie_kio.moc" diff --git a/kio/ollie_kio.h b/kio/ollie_kio.h new file mode 100644 index 0000000..d30dc3a --- /dev/null +++ b/kio/ollie_kio.h @@ -0,0 +1,33 @@ +#ifndef OLLIE_KIO_H +#define OLLIE_KIO_H + +#include +#include "lib9pclient.h" + +// KIO worker for ollie:// URLs. +// Maps ollie:///path directly to the Ollie 9P namespace. +class OllieWorker : public KIO::WorkerBase +{ +public: + OllieWorker(const QByteArray &pool, const QByteArray &app); + ~OllieWorker() override; + + KIO::WorkerResult listDir(const QUrl &url) override; + KIO::WorkerResult stat(const QUrl &url) override; + KIO::WorkerResult get(const QUrl &url) override; + KIO::WorkerResult put(const QUrl &url, int permissions, KIO::JobFlags flags) override; + KIO::WorkerResult mkdir(const QUrl &url, int permissions) override; + KIO::WorkerResult del(const QUrl &url, bool isFile) override; + KIO::WorkerResult rename(const QUrl &src, const QUrl &dest, KIO::JobFlags flags) override; + +private: + bool ensureConnected(); + QString ninepPath(const QUrl &url) const; + KIO::UDSEntry entryFromStat(const QString &path, const QString &name); + bool isDirectory(const QString &mode) const; + mode_t parseMode(const QString &mode) const; + + Lib9pClient m_client; +}; + +#endif // OLLIE_KIO_H