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:
parent
f4161fcabc
commit
e89b723627
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Reference in New Issue