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:
parent
739fba2462
commit
18b36c09cd
|
|
@ -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
|
||||
|
|
|
|||
7
justfile
7
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 ==='
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
Reference in New Issue