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:
parent
8531671556
commit
238efabcb5
|
|
@ -5,18 +5,23 @@ set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_AUTOMOC 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`)
|
# libollie.so built by Go (expected in build/ after `make lib`)
|
||||||
set(OLLIE_LIB "${CMAKE_SOURCE_DIR}/build/libollie.so")
|
set(OLLIE_LIB "${CMAKE_SOURCE_DIR}/build/libollie.so")
|
||||||
set(OLLIE_HEADER "${CMAKE_SOURCE_DIR}/build/libollie.h")
|
set(OLLIE_HEADER "${CMAKE_SOURCE_DIR}/build/libollie.h")
|
||||||
|
|
||||||
|
# --- Daemon ---
|
||||||
|
|
||||||
# Generate D-Bus adaptor from XML
|
# Generate D-Bus adaptor from XML
|
||||||
qt6_add_dbus_adaptor(DBUS_ADAPTOR_SRCS
|
qt6_add_dbus_adaptor(DBUS_ADAPTOR_SRCS
|
||||||
daemon/org.ollie.SessionManager.xml
|
daemon/org.ollie.SessionManager.xml
|
||||||
daemon/session_manager.h SessionManager)
|
daemon/session_manager.h SessionManager)
|
||||||
|
|
||||||
# Daemon
|
|
||||||
add_executable(ollie-kde
|
add_executable(ollie-kde
|
||||||
daemon/main.cpp
|
daemon/main.cpp
|
||||||
daemon/session_manager.cpp
|
daemon/session_manager.cpp
|
||||||
|
|
@ -33,4 +38,22 @@ target_link_libraries(ollie-kde
|
||||||
${OLLIE_LIB}
|
${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)
|
||||||
|
|
|
||||||
4
Makefile
4
Makefile
|
|
@ -23,6 +23,10 @@ daemon: lib
|
||||||
run: daemon
|
run: daemon
|
||||||
LD_LIBRARY_PATH=$(LIB_DIR):$$LD_LIBRARY_PATH ./$(BUILD_DIR)/ollie-kde
|
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:
|
clean:
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
rm -f $(LIB_DIR)/libollie.so $(LIB_DIR)/libollie.h
|
rm -f $(LIB_DIR)/libollie.so $(LIB_DIR)/libollie.h
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
Reference in New Issue