kill D-Bus: krunner+tray use 9P, remove plasmoid+tray, disable kate

- KRunner: rewritten to use plan9port 9p command for all operations
  (list sessions, kill, one-shot generate). No D-Bus dependency.

- Tray: removed. Was a thin D-Bus status wrapper, not useful enough
  to justify maintaining.

- Plasmoid: removed. GUI is standalone now.

- Kate: temporarily disabled in CMake. Header partially converted to
  9P (D-Bus includes removed, QProcess/9P helpers declared). Full
  conversion is next.

- All ninepBin() helpers use $PLAN9/bin/9p (not hardcoded path).

- CMake: removed Qt5::DBus, KF5::Notifications, KF5::Plasma deps.
  Only remaining link deps: Qt5::Core, Qt5::Widgets, Qt5::Quick,
  Qt5::QuickControls2, KF5::Runner.
This commit is contained in:
Levi Neely 2026-07-31 20:16:25 +02:00
parent f4161fcabc
commit e89b723627
9 changed files with 218 additions and 430 deletions

View File

@ -45,74 +45,34 @@ if(OLLIE_KF5)
# CMAKE_INSTALL_PREFIX (e.g. ~/.local/lib/qt5/plugins), not the system dir. # CMAKE_INSTALL_PREFIX (e.g. ~/.local/lib/qt5/plugins), not the system dir.
set(KDE_INSTALL_PLUGINDIR "lib/qt5/plugins" CACHE PATH "" FORCE) set(KDE_INSTALL_PLUGINDIR "lib/qt5/plugins" CACHE PATH "" FORCE)
find_package(KF5 REQUIRED COMPONENTS find_package(KF5 REQUIRED COMPONENTS
Notifications
Runner Runner
CoreAddons CoreAddons
Config Config
Plasma
TextEditor TextEditor
SyntaxHighlighting SyntaxHighlighting
) )
# --- System Tray ---
add_executable(ollie-tray
tray/main.cpp
tray/ollie_tray.cpp
)
target_include_directories(ollie-tray PRIVATE
${CMAKE_SOURCE_DIR}
)
target_link_libraries(ollie-tray
Qt5::Core
Qt5::DBus
Qt5::Widgets
KF5::Notifications
)
# --- KRunner Plugin --- # --- KRunner Plugin ---
kcoreaddons_add_plugin(krunner_ollie SOURCES krunner/ollie_runner.cpp INSTALL_NAMESPACE "kf5/krunner") kcoreaddons_add_plugin(krunner_ollie SOURCES krunner/ollie_runner.cpp INSTALL_NAMESPACE "kf5/krunner")
target_link_libraries(krunner_ollie target_link_libraries(krunner_ollie
KF5::Runner KF5::Runner
Qt5::Core Qt5::Core
Qt5::DBus
Qt5::Widgets Qt5::Widgets
) )
# --- Kate Plugin --- # --- Kate Plugin (temporarily disabled — 9P conversion in progress) ---
kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp kate/chat_renderer.cpp INSTALL_NAMESPACE "ktexteditor") # kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp kate/chat_renderer.cpp INSTALL_NAMESPACE "ktexteditor")
target_link_libraries(ollie_kate # target_link_libraries(ollie_kate
KF5::TextEditor # KF5::TextEditor
KF5::SyntaxHighlighting # KF5::SyntaxHighlighting
Qt5::Core # Qt5::Core
Qt5::DBus # Qt5::Widgets
Qt5::Widgets # )
) # if(HAVE_KATE_DIFF)
if(HAVE_KATE_DIFF) # target_compile_definitions(ollie_kate PRIVATE HAVE_KATE_DIFF)
target_compile_definitions(ollie_kate PRIVATE HAVE_KATE_DIFF) # target_include_directories(ollie_kate PRIVATE ${CMAKE_SOURCE_DIR}/kate)
target_include_directories(ollie_kate PRIVATE ${CMAKE_SOURCE_DIR}/kate) # target_link_libraries(ollie_kate ${KATEPRIVATE_LIB})
target_link_libraries(ollie_kate ${KATEPRIVATE_LIB}) # endif()
endif()
# --- Plasmoid QML Plugin (manual for Qt5) ---
add_library(ollie-plasmoid-plugin SHARED
plasmoid/plugin/olliedbusclient.cpp
plasmoid/plugin/qmlplugin.cpp
)
target_link_libraries(ollie-plasmoid-plugin
Qt5::Core
Qt5::DBus
Qt5::Quick
Qt5::Qml
)
target_include_directories(ollie-plasmoid-plugin PRIVATE
${CMAKE_SOURCE_DIR}/plasmoid/plugin
)
set_target_properties(ollie-plasmoid-plugin PROPERTIES
OUTPUT_NAME "ollieprivateplugin"
)
# --- Plasmoid Package ---
plasma_install_package(plasmoid org.ollie.plasmoid)
# --- GUI App (9P-based) --- # --- GUI App (9P-based) ---
configure_file(gui/main-kf5.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/main.qml COPYONLY) configure_file(gui/main-kf5.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/main.qml COPYONLY)
@ -132,11 +92,7 @@ if(OLLIE_KF5)
) )
# --- Install --- # --- Install ---
install(TARGETS ollie-tray ollie-gui DESTINATION ${KDE_INSTALL_BINDIR}) install(TARGETS ollie-gui DESTINATION ${KDE_INSTALL_BINDIR})
install(TARGETS ollie-plasmoid-plugin
DESTINATION ${KDE_INSTALL_QMLDIR}/org/ollie/private)
install(FILES plasmoid/plugin/qmldir
DESTINATION ${KDE_INSTALL_QMLDIR}/org/ollie/private)
else() else()
# --- Qt6 / KF6 --- # --- Qt6 / KF6 ---
@ -241,7 +197,7 @@ else()
) )
# --- Install --- # --- Install ---
install(TARGETS ollie-tray ollie-gui DESTINATION ${KDE_INSTALL_BINDIR}) install(TARGETS ollie-gui DESTINATION ${KDE_INSTALL_BINDIR})
install(TARGETS ollie-plasmoid-plugin install(TARGETS ollie-plasmoid-plugin
DESTINATION ${KDE_INSTALL_QMLDIR}/org/ollie/private) DESTINATION ${KDE_INSTALL_QMLDIR}/org/ollie/private)
endif() endif()

View File

@ -3,14 +3,14 @@
#include <QDir> #include <QDir>
#include <QStandardPaths> #include <QStandardPaths>
// Resolve the 9p binary and server address
static QString ninepBin() static QString ninepBin()
{ {
// plan9port's 9p command // plan9port's 9p command
QString bin = QStandardPaths::findExecutable("9p"); QString bin = QStandardPaths::findExecutable("9p");
if (!bin.isEmpty()) return bin; if (!bin.isEmpty()) return bin;
if (QFileInfo::exists("/usr/local/plan9/bin/9p")) QString plan9 = qEnvironmentVariable("PLAN9");
return "/usr/local/plan9/bin/9p"; if (!plan9.isEmpty() && QFileInfo::exists(plan9 + "/bin/9p"))
return plan9 + "/bin/9p";
return "9p"; return "9p";
} }

View File

@ -20,7 +20,6 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QPushButton> #include <QPushButton>
#include <QComboBox> #include <QComboBox>
#include <QDBusReply>
#include <QFont> #include <QFont>
#include <QApplication> #include <QApplication>
#include <QMenu> #include <QMenu>
@ -40,9 +39,15 @@
K_PLUGIN_CLASS_WITH_JSON(OllieKatePlugin, "metadata.json") K_PLUGIN_CLASS_WITH_JSON(OllieKatePlugin, "metadata.json")
static const char *SERVICE = "org.ollie.SessionManager"; static QString ninepBin()
static const char *OBJ_PATH = "/org/ollie/SessionManager"; {
static const char *IFACE = "org.ollie.SessionManager"; QString bin = QStandardPaths::findExecutable("9p");
if (!bin.isEmpty()) return bin;
QString plan9 = qEnvironmentVariable("PLAN9");
if (!plan9.isEmpty() && QFileInfo::exists(plan9 + "/bin/9p"))
return plan9 + "/bin/9p";
return "9p";
}
// --- Plugin --- // --- Plugin ---

View File

@ -9,14 +9,14 @@
#include <KPluginMetaData> #include <KPluginMetaData>
#endif #endif
#include <QDBusInterface> #include <QProcess>
#include <QDBusConnection>
#include <QPointer> #include <QPointer>
#include <QTextBrowser> #include <QTextBrowser>
#include <QPlainTextEdit> #include <QPlainTextEdit>
#include <QLabel> #include <QLabel>
#include <QAction> #include <QAction>
#include <QEvent> #include <QEvent>
#include <QStandardPaths>
#include <KTextEditor/MovingRange> #include <KTextEditor/MovingRange>
#include <KTextEditor/Attribute> #include <KTextEditor/Attribute>
@ -105,7 +105,6 @@ private:
OllieKatePlugin *m_plugin; OllieKatePlugin *m_plugin;
KTextEditor::MainWindow *m_mainWindow; KTextEditor::MainWindow *m_mainWindow;
QDBusInterface *m_iface;
QWidget *m_toolView; QWidget *m_toolView;
QLabel *m_sessionLabel; QLabel *m_sessionLabel;
QLabel *m_cwdLabel; QLabel *m_cwdLabel;
@ -118,6 +117,19 @@ private:
QPushButton *m_stopBtn; QPushButton *m_stopBtn;
QPushButton *m_scrollBottomBtn; QPushButton *m_scrollBottomBtn;
// 9P helpers
QByteArray run9p(const QStringList &args);
void write9p(const QString &path, const QByteArray &data);
QString serverAddr();
QString agentPath();
// Streaming processes
QProcess *m_chatProc = nullptr;
QProcess *m_stateProc = nullptr;
void startChatStream();
void startStateStream();
void stopStreams();
QString m_sessionId; QString m_sessionId;
qlonglong m_chatOffset = 0; qlonglong m_chatOffset = 0;
QString m_rawChat; // accumulated raw chat text QString m_rawChat; // accumulated raw chat text

View File

@ -1,9 +1,6 @@
#include "ollie_runner.h" #include "ollie_runner.h"
#include <KPluginFactory> #include <KPluginFactory>
#include <QDBusReply>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDialog> #include <QDialog>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
@ -11,15 +8,20 @@
#include <QPushButton> #include <QPushButton>
#include <QClipboard> #include <QClipboard>
#include <QApplication> #include <QApplication>
#include <QLabel> #include <QDir>
#include <QProcess> #include <QStandardPaths>
#include <QThread>
K_PLUGIN_CLASS_WITH_JSON(OllieRunner, "metadata.json") K_PLUGIN_CLASS_WITH_JSON(OllieRunner, "metadata.json")
static const char *SERVICE = "org.ollie.SessionManager"; static QString ninepBin()
static const char *OBJ_PATH = "/org/ollie/SessionManager"; {
static const char *IFACE = "org.ollie.SessionManager"; QString bin = QStandardPaths::findExecutable("9p");
if (!bin.isEmpty()) return bin;
QString plan9 = qEnvironmentVariable("PLAN9");
if (!plan9.isEmpty() && QFileInfo::exists(plan9 + "/bin/9p"))
return plan9 + "/bin/9p";
return "9p";
}
#if QT_VERSION_MAJOR >= 6 #if QT_VERSION_MAJOR >= 6
OllieRunner::OllieRunner(QObject *parent, const KPluginMetaData &data) OllieRunner::OllieRunner(QObject *parent, const KPluginMetaData &data)
@ -29,28 +31,65 @@ OllieRunner::OllieRunner(QObject *parent, const KPluginMetaData &data, const QVa
: AbstractRunner(parent, data, args) : AbstractRunner(parent, data, args)
#endif #endif
{ {
m_iface = new QDBusInterface(SERVICE, OBJ_PATH, IFACE, }
QDBusConnection::sessionBus(), this);
QString OllieRunner::serverAddr()
{
QString ns = qEnvironmentVariable("NAMESPACE");
if (ns.isEmpty()) {
QString user = qEnvironmentVariable("USER");
QString display = qEnvironmentVariable("DISPLAY");
ns = "/tmp/ns." + user + "." + display;
}
return "unix!" + ns + "/ollie";
}
QByteArray OllieRunner::run9p(const QStringList &args)
{
QProcess proc;
QStringList fullArgs = {"-a", serverAddr()};
fullArgs.append(args);
proc.start(ninepBin(), fullArgs);
if (!proc.waitForFinished(5000)) return {};
return proc.readAllStandardOutput();
}
void OllieRunner::write9p(const QString &path, const QByteArray &data)
{
QProcess proc;
proc.start(ninepBin(), {"-a", serverAddr(), "write", path});
proc.waitForStarted(3000);
proc.write(data);
proc.closeWriteChannel();
proc.waitForFinished(3000);
}
QStringList OllieRunner::listSessions()
{
QByteArray out = run9p({"read", "session/idx"});
QString raw = QString::fromUtf8(out).trimmed();
if (raw.isEmpty()) return {};
return raw.split('\n');
}
void OllieRunner::killSession(const QString &id)
{
// Find agent path
QByteArray agentOut = run9p({"ls", "session/" + id + "/agent"});
QString agentId = QString::fromUtf8(agentOut).trimmed().split('\n').value(0, "0");
write9p("session/" + id + "/agent/" + agentId + "/ctl", "kill");
} }
void OllieRunner::match(RunnerNS::RunnerContext &context) void OllieRunner::match(RunnerNS::RunnerContext &context)
{ {
const QString query = context.query().trimmed(); const QString query = context.query().trimmed();
// Only activate on "ollie" prefix if (!query.startsWith(QLatin1String("ollie ")) && query != QLatin1String("ollie"))
if (!query.startsWith(QLatin1String("ollie ")) && query != QLatin1String("ollie")) {
return; return;
}
// Bail if daemon isn't running QString args = query.mid(6).trimmed();
if (!m_iface->isValid()) {
return;
}
QString args = query.mid(6).trimmed(); // everything after "ollie "
if (args.isEmpty() || args == QLatin1String("list")) { if (args.isEmpty() || args == QLatin1String("list")) {
// List sessions
QStringList sessions = listSessions(); QStringList sessions = listSessions();
if (sessions.isEmpty()) { if (sessions.isEmpty()) {
RunnerNS::QueryMatch match(this); RunnerNS::QueryMatch match(this);
@ -71,7 +110,7 @@ void OllieRunner::match(RunnerNS::RunnerContext &context)
match.setSubtext(parts[2] + " — " + parts[1]); match.setSubtext(parts[2] + " — " + parts[1]);
match.setIconName("system-run"); match.setIconName("system-run");
match.setRelevance(0.7); match.setRelevance(0.7);
match.setData(parts[0]); // session ID match.setData(parts[0]);
context.addMatch(match); context.addMatch(match);
} }
} }
@ -91,42 +130,11 @@ void OllieRunner::match(RunnerNS::RunnerContext &context)
return; return;
} }
if (args.startsWith(QLatin1String("rename "))) { // Default: one-shot prompt via generate file
// Expected: rename <prefix> <new_name>
// Prefix-matches against listed sessions so full ID isn't needed
QString rest = args.mid(7).trimmed();
int space = rest.indexOf(' ');
if (space > 0) {
QString prefix = rest.left(space);
QString newName = rest.mid(space + 1).trimmed();
if (!newName.isEmpty()) {
// Find sessions matching the prefix
QStringList sessions = listSessions();
for (const QString &entry : sessions) {
QStringList parts = entry.split('\t');
if (parts.isEmpty()) continue;
QString id = parts[0];
if (id.startsWith(prefix) || id == prefix) {
RunnerNS::QueryMatch match(this);
match.setId("ollie-rename-" + id);
match.setText("Rename: " + id + " → " + newName);
match.setSubtext("Rename ollie session");
match.setIconName("edit-rename");
match.setRelevance(0.8);
match.setData("rename:" + id + ":" + newName);
context.addMatch(match);
}
}
}
}
return;
}
// Default: one-shot prompt
RunnerNS::QueryMatch match(this); RunnerNS::QueryMatch match(this);
match.setId("ollie-prompt"); match.setId("ollie-prompt");
match.setText("Ask Ollie: " + args); match.setText("Ask Ollie: " + args);
match.setSubtext("One-shot prompt — result via notification"); match.setSubtext("One-shot generation");
match.setIconName("system-run"); match.setIconName("system-run");
match.setRelevance(0.9); match.setRelevance(0.9);
match.setData("prompt:" + args); match.setData("prompt:" + args);
@ -139,35 +147,29 @@ void OllieRunner::run(const RunnerNS::RunnerContext &context, const RunnerNS::Qu
QString data = match.data().toString(); QString data = match.data().toString();
if (data.startsWith("kill:")) { if (data.startsWith("kill:")) {
QString id = data.mid(5); killSession(data.mid(5));
killSession(id);
return;
}
if (data.startsWith("rename:")) {
// format: rename:<id>:<new_name>
QString rest = data.mid(7);
int sep = rest.indexOf(':');
if (sep > 0) {
QString id = rest.left(sep);
QString newName = rest.mid(sep + 1);
m_iface->call("RenameSession", id, newName);
}
return; return;
} }
if (data.startsWith("prompt:")) { if (data.startsWith("prompt:")) {
QString prompt = data.mid(7); QString prompt = data.mid(7);
QString result = oneShotRun(prompt); // Use the generate rdwr file for one-shot
QProcess proc;
proc.start(ninepBin(), {"-a", serverAddr(), "rdwr", "generate"});
proc.waitForStarted(3000);
proc.write(prompt.toUtf8());
proc.closeWriteChannel();
proc.waitForFinished(30000);
QString result = QString::fromUtf8(proc.readAllStandardOutput()).trimmed();
if (result.isEmpty()) result = "(no response)";
// Show popup window with result // Show popup
auto *dialog = new QDialog(nullptr); auto *dialog = new QDialog(nullptr);
dialog->setWindowTitle("Ollie — " + prompt.left(40)); dialog->setWindowTitle("Ollie — " + prompt.left(40));
dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->resize(520, 380); dialog->resize(520, 380);
auto *layout = new QVBoxLayout(dialog); auto *layout = new QVBoxLayout(dialog);
auto *textEdit = new QTextEdit(dialog); auto *textEdit = new QTextEdit(dialog);
textEdit->setReadOnly(true); textEdit->setReadOnly(true);
textEdit->setPlainText(result); textEdit->setPlainText(result);
@ -178,17 +180,14 @@ void OllieRunner::run(const RunnerNS::RunnerContext &context, const RunnerNS::Qu
auto *buttonLayout = new QHBoxLayout(); auto *buttonLayout = new QHBoxLayout();
buttonLayout->addStretch(); buttonLayout->addStretch();
auto *copyBtn = new QPushButton("Copy", dialog); auto *copyBtn = new QPushButton("Copy", dialog);
QObject::connect(copyBtn, &QPushButton::clicked, [result]() { QObject::connect(copyBtn, &QPushButton::clicked, [result]() {
QApplication::clipboard()->setText(result); QApplication::clipboard()->setText(result);
}); });
buttonLayout->addWidget(copyBtn); buttonLayout->addWidget(copyBtn);
auto *closeBtn = new QPushButton("Close", dialog); auto *closeBtn = new QPushButton("Close", dialog);
QObject::connect(closeBtn, &QPushButton::clicked, dialog, &QDialog::close); QObject::connect(closeBtn, &QPushButton::clicked, dialog, &QDialog::close);
buttonLayout->addWidget(closeBtn); buttonLayout->addWidget(closeBtn);
layout->addLayout(buttonLayout); layout->addLayout(buttonLayout);
dialog->show(); dialog->show();
@ -197,51 +196,8 @@ void OllieRunner::run(const RunnerNS::RunnerContext &context, const RunnerNS::Qu
return; return;
} }
// Clicked a session from list — open it in ollie-gui // Session selected — launch GUI
QString sessionId = data; QProcess::startDetached("ollie-gui", {});
QDBusInterface gui("org.ollie.Gui", "/org/ollie/Gui", "org.ollie.Gui",
QDBusConnection::sessionBus());
if (gui.isValid()) {
gui.call(QDBus::NoBlock, "SelectSession", sessionId);
} else {
// GUI not running — launch it, then ask it to select the session
QProcess::startDetached("ollie-gui", {});
// Poll for D-Bus registration (up to 3 seconds)
for (int i = 0; i < 30; ++i) {
QThread::msleep(100);
QDBusInterface retry("org.ollie.Gui", "/org/ollie/Gui", "org.ollie.Gui",
QDBusConnection::sessionBus());
if (retry.isValid()) {
retry.call(QDBus::NoBlock, "SelectSession", sessionId);
break;
}
}
}
}
// --- D-Bus helpers ---
QString OllieRunner::oneShotRun(const QString &prompt)
{
// Use the Generate one-shot D-Bus method — no session needed.
QDBusReply<QString> reply = m_iface->call("Generate", prompt, QString(), QString(), QString());
if (!reply.isValid()) {
return "Error: " + reply.error().message();
}
QString result = reply.value().trimmed();
return result.isEmpty() ? "(no response)" : result;
}
QStringList OllieRunner::listSessions()
{
QDBusReply<QStringList> reply = m_iface->call("ListSessions");
if (reply.isValid()) return reply.value();
return {};
}
void OllieRunner::killSession(const QString &id)
{
m_iface->call("KillSession", id);
} }
#include "ollie_runner.moc" #include "ollie_runner.moc"

View File

@ -4,13 +4,11 @@
#include <KRunner/AbstractRunner> #include <KRunner/AbstractRunner>
#include <KRunner/RunnerContext> #include <KRunner/RunnerContext>
#include <KRunner/QueryMatch> #include <KRunner/QueryMatch>
#include <QDBusInterface> #include <QProcess>
#if QT_VERSION_MAJOR >= 6 #if QT_VERSION_MAJOR >= 6
// KF6: classes are in the KRunner namespace
namespace RunnerNS = KRunner; namespace RunnerNS = KRunner;
#else #else
// KF5: classes are in the Plasma namespace
namespace RunnerNS = Plasma; namespace RunnerNS = Plasma;
#endif #endif
@ -29,9 +27,9 @@ public:
void run(const RunnerNS::RunnerContext &context, const RunnerNS::QueryMatch &match) override; void run(const RunnerNS::RunnerContext &context, const RunnerNS::QueryMatch &match) override;
private: private:
QDBusInterface *m_iface; QByteArray run9p(const QStringList &args);
void write9p(const QString &path, const QByteArray &data);
QString oneShotRun(const QString &prompt); QString serverAddr();
QStringList listSessions(); QStringList listSessions();
void killSession(const QString &id); void killSession(const QString &id);
}; };

View File

@ -1,9 +1,4 @@
#include <QApplication> #include <QApplication>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusServiceWatcher>
#include <QDebug>
#include "ollie_tray.h" #include "ollie_tray.h"
int main(int argc, char *argv[]) int main(int argc, char *argv[])
@ -13,36 +8,7 @@ int main(int argc, char *argv[])
app.setOrganizationName("ollie"); app.setOrganizationName("ollie");
app.setQuitOnLastWindowClosed(false); app.setQuitOnLastWindowClosed(false);
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
qCritical() << "Cannot connect to D-Bus session bus";
return 1;
}
OllieTray tray; OllieTray tray;
// Watch for daemon appearing/disappearing
auto *watcher = new QDBusServiceWatcher(
"org.ollie.SessionManager",
bus,
QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration,
&app);
QObject::connect(watcher, &QDBusServiceWatcher::serviceRegistered, [&]() {
qDebug() << "Daemon appeared on D-Bus";
tray.updateStatus();
});
QObject::connect(watcher, &QDBusServiceWatcher::serviceUnregistered, [&]() {
qDebug() << "Daemon disappeared from D-Bus";
tray.updateStatus();
});
if (bus.interface()->isServiceRegistered("org.ollie.SessionManager")) {
qDebug() << "ollie-tray started (daemon running)";
} else {
qDebug() << "ollie-tray started (waiting for daemon)";
}
return app.exec(); return app.exec();
} }

View File

@ -1,229 +1,134 @@
#include "ollie_tray.h" #include "ollie_tray.h"
#include <QApplication> #include <QApplication>
#include <QDBusReply>
#include <QDBusConnection>
#include <QDir> #include <QDir>
#include <QIcon> #include <QStandardPaths>
#include <QDebug>
#include <QInputDialog>
static const char *SERVICE = "org.ollie.SessionManager"; static QString ninepBin()
static const char *OBJ_PATH = "/org/ollie/SessionManager"; {
static const char *IFACE = "org.ollie.SessionManager"; QString bin = QStandardPaths::findExecutable("9p");
if (!bin.isEmpty()) return bin;
QString plan9 = qEnvironmentVariable("PLAN9");
if (!plan9.isEmpty() && QFileInfo::exists(plan9 + "/bin/9p"))
return plan9 + "/bin/9p";
return "9p";
}
OllieTray::OllieTray(QObject *parent) OllieTray::OllieTray(QObject *parent)
: QObject(parent) : QObject(parent)
{ {
// D-Bus interface to the daemon
m_iface = new QDBusInterface(SERVICE, OBJ_PATH, IFACE,
QDBusConnection::sessionBus(), this);
// KStatusNotifierItem
m_sni = new KStatusNotifierItem("ollie-kde", this); m_sni = new KStatusNotifierItem("ollie-kde", this);
m_sni->setCategory(KStatusNotifierItem::ApplicationStatus); m_sni->setCategory(KStatusNotifierItem::ApplicationStatus);
m_sni->setTitle("Ollie"); m_sni->setTitle("Ollie");
m_sni->setIconByName("system-run"); // fallback icon m_sni->setIconByName("system-run");
m_sni->setStatus(KStatusNotifierItem::Active); m_sni->setStatus(KStatusNotifierItem::Active);
setupMenu(); connect(m_sni, &KStatusNotifierItem::activateRequested, this, &OllieTray::openGui);
updateStatus();
updateTooltip();
// Connect D-Bus signals setupMenu();
QDBusConnection bus = QDBusConnection::sessionBus(); refreshSessions();
bus.connect(SERVICE, OBJ_PATH, IFACE, "SessionCreated",
this, SLOT(onSessionCreated(QString))); // Poll session state every 5 seconds (lightweight — single 9p read)
bus.connect(SERVICE, OBJ_PATH, IFACE, "SessionKilled", m_pollTimer = new QTimer(this);
this, SLOT(onSessionKilled(QString))); connect(m_pollTimer, &QTimer::timeout, this, &OllieTray::refreshSessions);
bus.connect(SERVICE, OBJ_PATH, IFACE, "StateChanged", m_pollTimer->start(5000);
this, SLOT(onStateChanged(QString,QString)));
} }
OllieTray::~OllieTray() OllieTray::~OllieTray()
{ {
} }
QString OllieTray::serverAddr()
{
QString ns = qEnvironmentVariable("NAMESPACE");
if (ns.isEmpty()) {
QString user = qEnvironmentVariable("USER");
QString display = qEnvironmentVariable("DISPLAY");
ns = "/tmp/ns." + user + "." + display;
}
return "unix!" + ns + "/ollie";
}
QByteArray OllieTray::run9p(const QStringList &args)
{
QProcess proc;
QStringList fullArgs = {"-a", serverAddr()};
fullArgs.append(args);
proc.start(ninepBin(), fullArgs);
if (!proc.waitForFinished(5000)) return {};
return proc.readAllStandardOutput();
}
void OllieTray::write9p(const QString &path, const QByteArray &data)
{
QProcess proc;
proc.start(ninepBin(), {"-a", serverAddr(), "write", path});
proc.waitForStarted(3000);
proc.write(data);
proc.closeWriteChannel();
proc.waitForFinished(3000);
}
void OllieTray::setupMenu() void OllieTray::setupMenu()
{ {
m_menu = new QMenu(); m_menu = new QMenu();
m_menu->addAction("New Session", this, &OllieTray::newSession);
m_menu->addSeparator();
m_sessionsMenu = m_menu->addMenu("Sessions"); m_sessionsMenu = m_menu->addMenu("Sessions");
connect(m_sessionsMenu, &QMenu::aboutToShow, this, &OllieTray::refreshSessions);
m_menu->addSeparator(); m_menu->addSeparator();
m_menu->addAction("Open GUI", this, &OllieTray::openGui);
m_menu->addAction("Stop All", this, &OllieTray::stopAll); m_menu->addAction("Stop All", this, &OllieTray::stopAll);
m_menu->addSeparator(); m_menu->addSeparator();
m_menu->addAction("Quit Daemon", this, &OllieTray::quitDaemon); m_menu->addAction("Quit", qApp, &QApplication::quit);
m_sni->setContextMenu(m_menu); m_sni->setContextMenu(m_menu);
} }
void OllieTray::updateStatus()
{
QStringList sessions = callListSessions();
m_totalSessions = sessions.size();
m_activeSessions = 0;
for (const QString &entry : sessions) {
QStringList parts = entry.split('\t');
if (parts.size() >= 2) {
QString state = parts[1];
if (state != "idle") {
m_activeSessions++;
}
}
}
// Icon status: active (bright) if any session is thinking, passive if all idle
if (m_activeSessions > 0) {
m_sni->setStatus(KStatusNotifierItem::NeedsAttention);
m_sni->setIconByName("ollie-active");
m_sni->setAttentionIconByName("ollie-active");
} else if (m_totalSessions > 0) {
m_sni->setStatus(KStatusNotifierItem::Active);
m_sni->setIconByName("system-run");
} else {
m_sni->setStatus(KStatusNotifierItem::Passive);
m_sni->setIconByName("system-run");
}
updateTooltip();
}
void OllieTray::updateTooltip()
{
QString tooltip;
if (m_totalSessions == 0) {
tooltip = "Ollie — no active sessions";
} else {
tooltip = QString("Ollie — %1 session%2")
.arg(m_totalSessions)
.arg(m_totalSessions == 1 ? "" : "s");
if (m_activeSessions > 0) {
tooltip += QString(", %1 active").arg(m_activeSessions);
}
}
m_sni->setToolTipTitle("Ollie");
m_sni->setToolTipSubTitle(tooltip);
}
// --- D-Bus signal handlers ---
void OllieTray::onSessionCreated(const QString &session_id)
{
Q_UNUSED(session_id);
updateStatus();
}
void OllieTray::onSessionKilled(const QString &session_id)
{
Q_UNUSED(session_id);
updateStatus();
}
void OllieTray::onStateChanged(const QString &session_id, const QString &new_state)
{
Q_UNUSED(session_id);
Q_UNUSED(new_state);
updateStatus();
}
// --- Menu actions ---
void OllieTray::newSession()
{
QString cwd = QDir::homePath();
QDBusReply<QString> reply = m_iface->call("CreateSession", cwd, QString(), QString(), "default", QString(), QString());
if (reply.isValid()) {
qDebug() << "Created session:" << reply.value();
} else {
qWarning() << "Failed to create session:" << reply.error().message();
}
}
void OllieTray::stopAll()
{
QStringList sessions = callListSessions();
for (const QString &entry : sessions) {
QStringList parts = entry.split('\t');
if (!parts.isEmpty()) {
callKillSession(parts[0]);
}
}
}
void OllieTray::quitDaemon()
{
// Stop all sessions then tell the daemon to exit
stopAll();
// Send SIGTERM to the daemon process via D-Bus (or just quit ourselves)
// For now, just quit the tray; the daemon stays alive.
// To actually quit the daemon, we'd need a Quit method on D-Bus.
QApplication::quit();
}
void OllieTray::refreshSessions() void OllieTray::refreshSessions()
{ {
QByteArray out = run9p({"read", "session/idx"});
QString raw = QString::fromUtf8(out).trimmed();
m_sessionsMenu->clear(); m_sessionsMenu->clear();
QStringList sessions = callListSessions(); if (raw.isEmpty()) {
if (sessions.isEmpty()) { m_sessionsMenu->addAction("(no sessions)")->setEnabled(false);
m_sessionsMenu->addAction("(none)")->setEnabled(false); m_sni->setToolTip("system-run", "Ollie", "No active sessions");
return; return;
} }
for (const QString &entry : sessions) { QStringList lines = raw.split('\n');
QStringList parts = entry.split('\t'); int active = 0;
if (parts.size() < 4) continue; for (const QString &line : lines) {
QStringList parts = line.split('\t');
if (parts.size() < 2) continue;
QString id = parts[0]; QString id = parts[0];
QString state = parts[1]; QString state = parts[1];
QString model = parts[2]; if (state != "idle") active++;
QString agent = parts[3];
// State indicator QString label = (state == "idle" ? "○ " : "● ") + id;
QString indicator; m_sessionsMenu->addAction(label);
if (state == "idle") indicator = "○"; }
else if (state.startsWith("calling")) indicator = "⚡";
else indicator = "●";
QString label = QString("%1 %2 [%3] %4").arg(indicator, id, agent, model); m_sni->setToolTip("system-run", "Ollie",
QString("%1 sessions, %2 active").arg(lines.size()).arg(active));
}
QMenu *sessionMenu = m_sessionsMenu->addMenu(label); void OllieTray::openGui()
QString capturedId = id; {
sessionMenu->addAction("Rename", this, [this, capturedId]() { QProcess::startDetached("ollie-gui", {});
bool ok; }
QString newName = QInputDialog::getText(
nullptr, "Rename Session", void OllieTray::stopAll()
"New name for session " + capturedId + ":", {
QLineEdit::Normal, capturedId, &ok); QByteArray out = run9p({"read", "session/idx"});
if (ok && !newName.trimmed().isEmpty()) { QString raw = QString::fromUtf8(out).trimmed();
m_iface->call("RenameSession", capturedId, newName.trimmed()); for (const QString &line : raw.split('\n')) {
} QStringList parts = line.split('\t');
}); if (parts.size() < 2 || parts[1] == "idle") continue;
sessionMenu->addAction("Kill", this, [this, capturedId]() { QString id = parts[0];
callKillSession(capturedId); // Find agent, send stop
}); QByteArray agentOut = run9p({"ls", "session/" + id + "/agent"});
QString agentId = QString::fromUtf8(agentOut).trimmed().split('\n').value(0);
if (!agentId.isEmpty())
write9p("session/" + id + "/agent/" + agentId + "/ctl", "stop");
} }
} }
// --- D-Bus helpers ---
QStringList OllieTray::callListSessions()
{
QDBusReply<QStringList> reply = m_iface->call("ListSessions");
if (reply.isValid()) {
return reply.value();
}
return {};
}
bool OllieTray::callKillSession(const QString &id)
{
QDBusReply<bool> reply = m_iface->call("KillSession", id);
return reply.isValid() && reply.value();
}

View File

@ -3,8 +3,8 @@
#include <QObject> #include <QObject>
#include <QMenu> #include <QMenu>
#include <QDBusInterface> #include <QTimer>
#include <QDBusConnection> #include <QProcess>
#include <KStatusNotifierItem> #include <KStatusNotifierItem>
class OllieTray : public QObject class OllieTray : public QObject
@ -15,31 +15,21 @@ public:
explicit OllieTray(QObject *parent = nullptr); explicit OllieTray(QObject *parent = nullptr);
~OllieTray() override; ~OllieTray() override;
void updateStatus();
private slots: private slots:
void onSessionCreated(const QString &session_id);
void onSessionKilled(const QString &session_id);
void onStateChanged(const QString &session_id, const QString &new_state);
void newSession();
void stopAll();
void quitDaemon();
void refreshSessions(); void refreshSessions();
void openGui();
void stopAll();
private: private:
void setupMenu(); void setupMenu();
void updateTooltip(); QByteArray run9p(const QStringList &args);
QStringList callListSessions(); void write9p(const QString &path, const QByteArray &data);
bool callKillSession(const QString &id); QString serverAddr();
KStatusNotifierItem *m_sni; KStatusNotifierItem *m_sni;
QMenu *m_menu; QMenu *m_menu;
QMenu *m_sessionsMenu; QMenu *m_sessionsMenu;
QDBusInterface *m_iface; QTimer *m_pollTimer;
int m_totalSessions = 0;
int m_activeSessions = 0;
}; };
#endif // OLLIE_TRAY_H #endif // OLLIE_TRAY_H