Add ollie:// KIO worker for Dolphin

Implements a KIO worker that maps ollie:///path to the Ollie 9P
namespace. Supports listing, reading, writing, mkdir, delete, and
rename — all operations Dolphin needs for full file manager integration.

Works with both KF5 (RealWorkerFactory) and KF6 (WorkerFactory).
Type ollie:/// in Dolphin's location bar to browse sessions and agents.
This commit is contained in:
Levi Neely 2026-08-13 12:26:15 +02:00
parent 739fba2462
commit 18b36c09cd
6 changed files with 454 additions and 1 deletions

View File

@ -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

View File

@ -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 ==='

22
kio/ollie.json Normal file
View File

@ -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"
}
}
}

15
kio/ollie.protocol Normal file
View File

@ -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

339
kio/ollie_kio.cpp Normal file
View File

@ -0,0 +1,339 @@
#include "ollie_kio.h"
#include <QCoreApplication>
#include <QUrl>
#include <QDateTime>
#include <QMimeDatabase>
#include <QMimeType>
#include <KIO/UDSEntry>
#include <KIO/WorkerFactory>
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<WorkerBase> createWorker(const QByteArray &pool, const QByteArray &app) override
{
return std::make_unique<OllieWorker>(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<SlaveBase> 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<WorkerBase> createRealWorker(const QByteArray &pool, const QByteArray &app) override
{
return std::make_unique<OllieWorker>(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<mode_t>(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"

33
kio/ollie_kio.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef OLLIE_KIO_H
#define OLLIE_KIO_H
#include <KIO/WorkerBase>
#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