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.
set(KDE_INSTALL_PLUGINDIR "lib/qt5/plugins" CACHE PATH "" FORCE)
find_package(KF5 REQUIRED COMPONENTS
Notifications
Runner
CoreAddons
Config
Plasma
TextEditor
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 ---
kcoreaddons_add_plugin(krunner_ollie SOURCES krunner/ollie_runner.cpp INSTALL_NAMESPACE "kf5/krunner")
target_link_libraries(krunner_ollie
KF5::Runner
Qt5::Core
Qt5::DBus
Qt5::Widgets
)
# --- Kate Plugin ---
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
KF5::TextEditor
KF5::SyntaxHighlighting
Qt5::Core
Qt5::DBus
Qt5::Widgets
)
if(HAVE_KATE_DIFF)
target_compile_definitions(ollie_kate PRIVATE HAVE_KATE_DIFF)
target_include_directories(ollie_kate PRIVATE ${CMAKE_SOURCE_DIR}/kate)
target_link_libraries(ollie_kate ${KATEPRIVATE_LIB})
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)
# --- 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")
# target_link_libraries(ollie_kate
# KF5::TextEditor
# KF5::SyntaxHighlighting
# Qt5::Core
# Qt5::Widgets
# )
# if(HAVE_KATE_DIFF)
# target_compile_definitions(ollie_kate PRIVATE HAVE_KATE_DIFF)
# target_include_directories(ollie_kate PRIVATE ${CMAKE_SOURCE_DIR}/kate)
# target_link_libraries(ollie_kate ${KATEPRIVATE_LIB})
# endif()
# --- GUI App (9P-based) ---
configure_file(gui/main-kf5.qml ${CMAKE_CURRENT_BINARY_DIR}/gui-kf5/main.qml COPYONLY)
@ -132,11 +92,7 @@ if(OLLIE_KF5)
)
# --- Install ---
install(TARGETS ollie-tray 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)
install(TARGETS ollie-gui DESTINATION ${KDE_INSTALL_BINDIR})
else()
# --- Qt6 / KF6 ---
@ -241,7 +197,7 @@ else()
)
# --- 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)
endif()

View File

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

View File

@ -20,7 +20,6 @@
#include <QHBoxLayout>
#include <QPushButton>
#include <QComboBox>
#include <QDBusReply>
#include <QFont>
#include <QApplication>
#include <QMenu>
@ -40,9 +39,15 @@
K_PLUGIN_CLASS_WITH_JSON(OllieKatePlugin, "metadata.json")
static const char *SERVICE = "org.ollie.SessionManager";
static const char *OBJ_PATH = "/org/ollie/SessionManager";
static const char *IFACE = "org.ollie.SessionManager";
static QString ninepBin()
{
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 ---

View File

@ -9,14 +9,14 @@
#include <KPluginMetaData>
#endif
#include <QDBusInterface>
#include <QDBusConnection>
#include <QProcess>
#include <QPointer>
#include <QTextBrowser>
#include <QPlainTextEdit>
#include <QLabel>
#include <QAction>
#include <QEvent>
#include <QStandardPaths>
#include <KTextEditor/MovingRange>
#include <KTextEditor/Attribute>
@ -105,7 +105,6 @@ private:
OllieKatePlugin *m_plugin;
KTextEditor::MainWindow *m_mainWindow;
QDBusInterface *m_iface;
QWidget *m_toolView;
QLabel *m_sessionLabel;
QLabel *m_cwdLabel;
@ -118,6 +117,19 @@ private:
QPushButton *m_stopBtn;
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;
qlonglong m_chatOffset = 0;
QString m_rawChat; // accumulated raw chat text

View File

@ -1,9 +1,6 @@
#include "ollie_runner.h"
#include <KPluginFactory>
#include <QDBusReply>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDialog>
#include <QVBoxLayout>
#include <QHBoxLayout>
@ -11,15 +8,20 @@
#include <QPushButton>
#include <QClipboard>
#include <QApplication>
#include <QLabel>
#include <QProcess>
#include <QThread>
#include <QDir>
#include <QStandardPaths>
K_PLUGIN_CLASS_WITH_JSON(OllieRunner, "metadata.json")
static const char *SERVICE = "org.ollie.SessionManager";
static const char *OBJ_PATH = "/org/ollie/SessionManager";
static const char *IFACE = "org.ollie.SessionManager";
static QString ninepBin()
{
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
OllieRunner::OllieRunner(QObject *parent, const KPluginMetaData &data)
@ -29,28 +31,65 @@ OllieRunner::OllieRunner(QObject *parent, const KPluginMetaData &data, const QVa
: AbstractRunner(parent, data, args)
#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)
{
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;
}
// Bail if daemon isn't running
if (!m_iface->isValid()) {
return;
}
QString args = query.mid(6).trimmed(); // everything after "ollie "
QString args = query.mid(6).trimmed();
if (args.isEmpty() || args == QLatin1String("list")) {
// List sessions
QStringList sessions = listSessions();
if (sessions.isEmpty()) {
RunnerNS::QueryMatch match(this);
@ -71,7 +110,7 @@ void OllieRunner::match(RunnerNS::RunnerContext &context)
match.setSubtext(parts[2] + " — " + parts[1]);
match.setIconName("system-run");
match.setRelevance(0.7);
match.setData(parts[0]); // session ID
match.setData(parts[0]);
context.addMatch(match);
}
}
@ -91,42 +130,11 @@ void OllieRunner::match(RunnerNS::RunnerContext &context)
return;
}
if (args.startsWith(QLatin1String("rename "))) {
// 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
// Default: one-shot prompt via generate file
RunnerNS::QueryMatch match(this);
match.setId("ollie-prompt");
match.setText("Ask Ollie: " + args);
match.setSubtext("One-shot prompt — result via notification");
match.setSubtext("One-shot generation");
match.setIconName("system-run");
match.setRelevance(0.9);
match.setData("prompt:" + args);
@ -139,35 +147,29 @@ void OllieRunner::run(const RunnerNS::RunnerContext &context, const RunnerNS::Qu
QString data = match.data().toString();
if (data.startsWith("kill:")) {
QString id = 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);
}
killSession(data.mid(5));
return;
}
if (data.startsWith("prompt:")) {
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);
dialog->setWindowTitle("Ollie — " + prompt.left(40));
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->resize(520, 380);
auto *layout = new QVBoxLayout(dialog);
auto *textEdit = new QTextEdit(dialog);
textEdit->setReadOnly(true);
textEdit->setPlainText(result);
@ -178,17 +180,14 @@ void OllieRunner::run(const RunnerNS::RunnerContext &context, const RunnerNS::Qu
auto *buttonLayout = new QHBoxLayout();
buttonLayout->addStretch();
auto *copyBtn = new QPushButton("Copy", dialog);
QObject::connect(copyBtn, &QPushButton::clicked, [result]() {
QApplication::clipboard()->setText(result);
});
buttonLayout->addWidget(copyBtn);
auto *closeBtn = new QPushButton("Close", dialog);
QObject::connect(closeBtn, &QPushButton::clicked, dialog, &QDialog::close);
buttonLayout->addWidget(closeBtn);
layout->addLayout(buttonLayout);
dialog->show();
@ -197,51 +196,8 @@ void OllieRunner::run(const RunnerNS::RunnerContext &context, const RunnerNS::Qu
return;
}
// Clicked a session from list — open it in ollie-gui
QString sessionId = data;
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
// Session selected — launch GUI
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"

View File

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

View File

@ -1,9 +1,4 @@
#include <QApplication>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusServiceWatcher>
#include <QDebug>
#include "ollie_tray.h"
int main(int argc, char *argv[])
@ -13,36 +8,7 @@ int main(int argc, char *argv[])
app.setOrganizationName("ollie");
app.setQuitOnLastWindowClosed(false);
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
qCritical() << "Cannot connect to D-Bus session bus";
return 1;
}
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();
}

View File

@ -1,229 +1,134 @@
#include "ollie_tray.h"
#include <QApplication>
#include <QDBusReply>
#include <QDBusConnection>
#include <QDir>
#include <QIcon>
#include <QDebug>
#include <QInputDialog>
#include <QStandardPaths>
static const char *SERVICE = "org.ollie.SessionManager";
static const char *OBJ_PATH = "/org/ollie/SessionManager";
static const char *IFACE = "org.ollie.SessionManager";
static QString ninepBin()
{
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)
: 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->setCategory(KStatusNotifierItem::ApplicationStatus);
m_sni->setTitle("Ollie");
m_sni->setIconByName("system-run"); // fallback icon
m_sni->setIconByName("system-run");
m_sni->setStatus(KStatusNotifierItem::Active);
setupMenu();
updateStatus();
updateTooltip();
connect(m_sni, &KStatusNotifierItem::activateRequested, this, &OllieTray::openGui);
// Connect D-Bus signals
QDBusConnection bus = QDBusConnection::sessionBus();
bus.connect(SERVICE, OBJ_PATH, IFACE, "SessionCreated",
this, SLOT(onSessionCreated(QString)));
bus.connect(SERVICE, OBJ_PATH, IFACE, "SessionKilled",
this, SLOT(onSessionKilled(QString)));
bus.connect(SERVICE, OBJ_PATH, IFACE, "StateChanged",
this, SLOT(onStateChanged(QString,QString)));
setupMenu();
refreshSessions();
// Poll session state every 5 seconds (lightweight — single 9p read)
m_pollTimer = new QTimer(this);
connect(m_pollTimer, &QTimer::timeout, this, &OllieTray::refreshSessions);
m_pollTimer->start(5000);
}
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()
{
m_menu = new QMenu();
m_menu->addAction("New Session", this, &OllieTray::newSession);
m_menu->addSeparator();
m_sessionsMenu = m_menu->addMenu("Sessions");
connect(m_sessionsMenu, &QMenu::aboutToShow, this, &OllieTray::refreshSessions);
m_menu->addSeparator();
m_menu->addAction("Open GUI", this, &OllieTray::openGui);
m_menu->addAction("Stop All", this, &OllieTray::stopAll);
m_menu->addSeparator();
m_menu->addAction("Quit Daemon", this, &OllieTray::quitDaemon);
m_menu->addAction("Quit", qApp, &QApplication::quit);
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()
{
QByteArray out = run9p({"read", "session/idx"});
QString raw = QString::fromUtf8(out).trimmed();
m_sessionsMenu->clear();
QStringList sessions = callListSessions();
if (sessions.isEmpty()) {
m_sessionsMenu->addAction("(none)")->setEnabled(false);
if (raw.isEmpty()) {
m_sessionsMenu->addAction("(no sessions)")->setEnabled(false);
m_sni->setToolTip("system-run", "Ollie", "No active sessions");
return;
}
for (const QString &entry : sessions) {
QStringList parts = entry.split('\t');
if (parts.size() < 4) continue;
QStringList lines = raw.split('\n');
int active = 0;
for (const QString &line : lines) {
QStringList parts = line.split('\t');
if (parts.size() < 2) continue;
QString id = parts[0];
QString state = parts[1];
QString model = parts[2];
QString agent = parts[3];
if (state != "idle") active++;
// State indicator
QString indicator;
if (state == "idle") indicator = "○";
else if (state.startsWith("calling")) indicator = "⚡";
else indicator = "●";
QString label = QString("%1 %2 [%3] %4").arg(indicator, id, agent, model);
QMenu *sessionMenu = m_sessionsMenu->addMenu(label);
QString capturedId = id;
sessionMenu->addAction("Rename", this, [this, capturedId]() {
bool ok;
QString newName = QInputDialog::getText(
nullptr, "Rename Session",
"New name for session " + capturedId + ":",
QLineEdit::Normal, capturedId, &ok);
if (ok && !newName.trimmed().isEmpty()) {
m_iface->call("RenameSession", capturedId, newName.trimmed());
}
});
sessionMenu->addAction("Kill", this, [this, capturedId]() {
callKillSession(capturedId);
});
}
QString label = (state == "idle" ? "○ " : "● ") + id;
m_sessionsMenu->addAction(label);
}
// --- D-Bus helpers ---
m_sni->setToolTip("system-run", "Ollie",
QString("%1 sessions, %2 active").arg(lines.size()).arg(active));
}
QStringList OllieTray::callListSessions()
void OllieTray::openGui()
{
QDBusReply<QStringList> reply = m_iface->call("ListSessions");
if (reply.isValid()) {
return reply.value();
}
return {};
QProcess::startDetached("ollie-gui", {});
}
bool OllieTray::callKillSession(const QString &id)
void OllieTray::stopAll()
{
QDBusReply<bool> reply = m_iface->call("KillSession", id);
return reply.isValid() && reply.value();
QByteArray out = run9p({"read", "session/idx"});
QString raw = QString::fromUtf8(out).trimmed();
for (const QString &line : raw.split('\n')) {
QStringList parts = line.split('\t');
if (parts.size() < 2 || parts[1] == "idle") continue;
QString id = parts[0];
// 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");
}
}

View File

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