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_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)
|
||||
|
|
|
|||
4
Makefile
4
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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