phase 3: system tray icon

- KStatusNotifierItem with status indication (passive/active/attention)
- Tooltip: session count and active count
- Right-click menu: New Session, Sessions submenu (per-session kill),
  Stop All, Quit Daemon
- Live updates via D-Bus signals (SessionCreated, SessionKilled,
  StateChanged)
- Builds as ollie-tray (104KB), links KF6StatusNotifierItem + Qt6
This commit is contained in:
Levi Neely 2026-07-05 22:50:42 +02:00
parent 8531671556
commit 238efabcb5
5 changed files with 323 additions and 3 deletions

View File

@ -5,18 +5,23 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Core DBus)
find_package(ECM REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
find_package(Qt6 REQUIRED COMPONENTS Core DBus Widgets)
find_package(KF6StatusNotifierItem REQUIRED)
# libollie.so built by Go (expected in build/ after `make lib`)
set(OLLIE_LIB "${CMAKE_SOURCE_DIR}/build/libollie.so")
set(OLLIE_HEADER "${CMAKE_SOURCE_DIR}/build/libollie.h")
# --- Daemon ---
# Generate D-Bus adaptor from XML
qt6_add_dbus_adaptor(DBUS_ADAPTOR_SRCS
daemon/org.ollie.SessionManager.xml
daemon/session_manager.h SessionManager)
# Daemon
add_executable(ollie-kde
daemon/main.cpp
daemon/session_manager.cpp
@ -33,4 +38,22 @@ target_link_libraries(ollie-kde
${OLLIE_LIB}
)
install(TARGETS ollie-kde DESTINATION bin)
# --- 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
Qt6::Core
Qt6::DBus
Qt6::Widgets
KF6::StatusNotifierItem
)
# --- Install ---
install(TARGETS ollie-kde ollie-tray DESTINATION bin)

View File

@ -23,6 +23,10 @@ daemon: lib
run: daemon
LD_LIBRARY_PATH=$(LIB_DIR):$$LD_LIBRARY_PATH ./$(BUILD_DIR)/ollie-kde
# Run the system tray (daemon must be running)
tray: daemon
./$(BUILD_DIR)/ollie-tray
clean:
rm -rf $(BUILD_DIR)
rm -f $(LIB_DIR)/libollie.so $(LIB_DIR)/libollie.h

31
tray/main.cpp Normal file
View File

@ -0,0 +1,31 @@
#include <QApplication>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDebug>
#include "ollie_tray.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
app.setApplicationName("ollie-tray");
app.setOrganizationName("ollie");
app.setQuitOnLastWindowClosed(false);
// Verify the daemon is reachable
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
qCritical() << "Cannot connect to D-Bus session bus";
return 1;
}
if (!bus.interface()->isServiceRegistered("org.ollie.SessionManager")) {
qCritical() << "ollie-kde daemon not running (org.ollie.SessionManager not registered)";
return 1;
}
OllieTray tray;
qDebug() << "ollie-tray started";
return app.exec();
}

218
tray/ollie_tray.cpp Normal file
View File

@ -0,0 +1,218 @@
#include "ollie_tray.h"
#include <QApplication>
#include <QDBusReply>
#include <QDBusConnection>
#include <QDir>
#include <QIcon>
#include <QDebug>
static const char *SERVICE = "org.ollie.SessionManager";
static const char *OBJ_PATH = "/org/ollie/SessionManager";
static const char *IFACE = "org.ollie.SessionManager";
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->setStatus(KStatusNotifierItem::Active);
setupMenu();
updateStatus();
updateTooltip();
// 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)));
}
OllieTray::~OllieTray()
{
}
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("Stop All", this, &OllieTray::stopAll);
m_menu->addSeparator();
m_menu->addAction("Quit Daemon", this, &OllieTray::quitDaemon);
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");
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()
{
m_sessionsMenu->clear();
QStringList sessions = callListSessions();
if (sessions.isEmpty()) {
m_sessionsMenu->addAction("(none)")->setEnabled(false);
return;
}
for (const QString &entry : sessions) {
QStringList parts = entry.split('\t');
if (parts.size() < 4) continue;
QString id = parts[0];
QString state = parts[1];
QString model = parts[2];
QString agent = parts[3];
// 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.left(12), agent, model);
QMenu *sessionMenu = m_sessionsMenu->addMenu(label);
QString capturedId = id;
sessionMenu->addAction("Kill", this, [this, capturedId]() {
callKillSession(capturedId);
});
}
}
// --- 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();
}

44
tray/ollie_tray.h Normal file
View File

@ -0,0 +1,44 @@
#ifndef OLLIE_TRAY_H
#define OLLIE_TRAY_H
#include <QObject>
#include <QMenu>
#include <QDBusInterface>
#include <QDBusConnection>
#include <KStatusNotifierItem>
class OllieTray : public QObject
{
Q_OBJECT
public:
explicit OllieTray(QObject *parent = nullptr);
~OllieTray() override;
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();
private:
void setupMenu();
void updateStatus();
void updateTooltip();
QStringList callListSessions();
bool callKillSession(const QString &id);
KStatusNotifierItem *m_sni;
QMenu *m_menu;
QMenu *m_sessionsMenu;
QDBusInterface *m_iface;
int m_totalSessions = 0;
int m_activeSessions = 0;
};
#endif // OLLIE_TRAY_H