Compare commits

..

2 Commits

Author SHA1 Message Date
47f052f913 add popover component 2025-12-20 20:52:04 +01:00
3558fd3ebc get notifications from dbus 2025-12-20 18:53:19 +01:00
12 changed files with 401 additions and 63 deletions

View File

@@ -9,18 +9,6 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
# Some CMake versions may enable C++ modules and add compiler flags
# like `-fmodules-ts`. Older or certain `clang`/`clangd` builds may
# not accept this flag and will report "Unknown argument: '-fmodules-ts'".
# Strip that flag when using Clang to avoid diagnostics from clang/clangd.
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang")
string(REPLACE "-fmodules-ts" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
string(REPLACE "-fmodules-ts" "" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")
string(REPLACE "-fmodules-ts" "" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
string(REPLACE "-fmodules-ts" "" CMAKE_CXX_FLAGS_MINSIZEREL "${CMAKE_CXX_FLAGS_MINSIZEREL}")
string(REPLACE "-fmodules-ts" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
endif()
pkg_check_modules(GTKMM REQUIRED gtkmm-4.0) pkg_check_modules(GTKMM REQUIRED gtkmm-4.0)
pkg_check_modules(LAYERSHELL REQUIRED gtk4-layer-shell-0) pkg_check_modules(LAYERSHELL REQUIRED gtk4-layer-shell-0)
pkg_check_modules(WEBKIT REQUIRED webkitgtk-6.0) pkg_check_modules(WEBKIT REQUIRED webkitgtk-6.0)
@@ -40,7 +28,9 @@ target_sources(bar_lib
src/widgets/webWidget.cpp src/widgets/webWidget.cpp
src/services/hyprland.cpp src/services/hyprland.cpp
src/services/tray.cpp src/services/tray.cpp
src/services/notifications.cpp
src/widgets/tray.cpp src/widgets/tray.cpp
src/components/popover.cpp
) )
include_directories(bar_lib PRIVATE include_directories(bar_lib PRIVATE
include include

View File

@@ -6,6 +6,7 @@
#include "glibmm/refptr.h" #include "glibmm/refptr.h"
#include "gtkmm/application.h" #include "gtkmm/application.h"
#include "services/hyprland.hpp" #include "services/hyprland.hpp"
#include "services/notifications.hpp"
#include "services/tray.hpp" #include "services/tray.hpp"
class App { class App {
@@ -18,6 +19,7 @@ class App {
Glib::RefPtr<Gtk::Application> app; Glib::RefPtr<Gtk::Application> app;
std::vector<Bar *> bars; std::vector<Bar *> bars;
HyprlandService hyprlandService; HyprlandService hyprlandService;
NotificationService notificationService;
TrayService trayService; TrayService trayService;
void setupServices(); void setupServices();

View File

@@ -0,0 +1,19 @@
#pragma once
#include <gtkmm/button.h>
#include <gtkmm/popover.h>
#include <string>
class Popover: public Gtk::Button {
public:
Popover(std::string icon, std::string name);
~Popover() override;
protected:
void on_toggle_window();
Gtk::Popover* popover = nullptr;
void set_popover_child(Gtk::Widget& child) {
gtk_popover_set_child(popover->gobj(), child.gobj());
}
};

View File

@@ -0,0 +1,20 @@
#pragma once
#include <gio/gio.h>
class NotificationService {
public:
void intialize();
NotificationService() = default;
~NotificationService();
guint32 allocateNotificationId(guint32 replacesId);
GDBusConnection* getConnection() const { return connection; }
private:
GDBusConnection* connection = nullptr;
guint registrationId = 0;
GDBusNodeInfo* nodeInfo = nullptr;
guint32 nextNotificationId = 1;
};

View File

@@ -1,14 +1,11 @@
#pragma once #pragma once
#include "components/popover.hpp"
#include <gtkmm/button.h> #include <gtkmm/button.h>
#include <gtkmm/popover.h> #include <gtkmm/popover.h>
class WebWidget : public Gtk::Button { class WebWidget : public Popover {
public: public:
WebWidget(std::string icon, std::string title, std::string url); WebWidget(std::string icon, std::string title, std::string url);
~WebWidget() override;
private: private:
void on_toggle_window();
Gtk::Popover* popover = nullptr;
}; };

View File

@@ -44,8 +44,6 @@ window {
.workspace-pill-urgent { .workspace-pill-urgent {
background-color: #ff5555; background-color: #ff5555;
color: #fff; color: #fff;
/* base glow (will be animated) */
animation: workspace-blink 1s linear infinite; animation: workspace-blink 1s linear infinite;
} }

View File

@@ -6,6 +6,7 @@
App::App() { App::App() {
this->setupServices(); this->setupServices();
this->notificationService.intialize();
this->app = Gtk::Application::create("org.example.mybar"); this->app = Gtk::Application::create("org.example.mybar");

View File

@@ -70,7 +70,7 @@ void Bar::setup_ui() {
right_box.set_valign(Gtk::Align::CENTER); right_box.set_valign(Gtk::Align::CENTER);
workspaceIndicator = Gtk::make_managed<WorkspaceIndicator>(hyprlandService, monitorId); workspaceIndicator = Gtk::make_managed<WorkspaceIndicator>(hyprlandService, monitorId);
left_box.append(*workspaceIndicator); left_box.append(*workspaceIndicator);
clock.set_name("clock-label"); clock.set_name("clock-label");

View File

@@ -0,0 +1,35 @@
#include "components/popover.hpp"
#include "gtkmm/label.h"
#include "gtkmm/object.h"
Popover::Popover(std::string icon, std::string name) {
auto label = Gtk::make_managed<Gtk::Label>(icon);
label->add_css_class("icon-label");
set_child(*label);
signal_clicked().connect(
sigc::mem_fun(*this, &Popover::on_toggle_window));
popover = new Gtk::Popover();
popover->set_parent(*this);
popover->set_autohide(true);
popover->signal_closed().connect([this]() {
this->add_css_class("minimized");
this->remove_css_class("restored");
});
}
Popover::~Popover() {
delete popover;
}
void Popover::on_toggle_window() {
if (popover->get_visible()) {
popover->popdown();
} else {
this->remove_css_class("minimized");
this->add_css_class("restored");
popover->popup();
}
}

View File

@@ -31,7 +31,7 @@ HyprlandService::~HyprlandService() {
} }
void HyprlandService::on_hyprland_event(std::string event, std::string data) { void HyprlandService::on_hyprland_event(std::string event, std::string data) {
if (event == "urgent") { if (event == "urgent") {
onUrgentEvent(data); onUrgentEvent(data);
} }
@@ -39,7 +39,7 @@ void HyprlandService::on_hyprland_event(std::string event, std::string data) {
if (event == "activewindowv2") { if (event == "activewindowv2") {
onActiveWindowEvent(data); onActiveWindowEvent(data);
} }
if (event == "workspace" || event == "movewindow") { if (event == "workspace" || event == "movewindow") {
refresh_workspaces(); refresh_workspaces();
} }
@@ -47,7 +47,7 @@ void HyprlandService::on_hyprland_event(std::string event, std::string data) {
// use for // use for
// event == "focusedmon" // event == "focusedmon"
if (event == "monitoradded" || event == "monitorremoved") { if (event == "monitoradded" || event == "monitorremoved") {
refresh_monitors(); refresh_monitors();
} }
} }
@@ -179,9 +179,9 @@ void HyprlandService::refresh_monitors() {
wsState.label = std::to_string(slot); wsState.label = std::to_string(slot);
wsState.monitorId = monitor.id; wsState.monitorId = monitor.id;
int id = slot + monitor.id * HyprlandService::kWorkspaceSlotCount; int id = slot + monitor.id * HyprlandService::kWorkspaceSlotCount;
wsState.hyprId = id; wsState.hyprId = id;
this->workspaces[id] = new WorkspaceState(wsState); this->workspaces[id] = new WorkspaceState(wsState);
if (monitor.id >= 0) { if (monitor.id >= 0) {
this->monitors[monitor.id].workspaceStates[slot] = this->workspaces[id]; this->monitors[monitor.id].workspaceStates[slot] = this->workspaces[id];
} }
@@ -220,14 +220,14 @@ void HyprlandService::refresh_workspaces() {
} }
for (const auto &workspaceJson : workspacesJson) { for (const auto &workspaceJson : workspacesJson) {
const int workspaceId = workspaceJson.value("id", -1); const int workspaceId = workspaceJson.value("id", -1);
auto workspaceStateIt = this->workspaces.find(workspaceId); auto workspaceStateIt = this->workspaces.find(workspaceId);
if (workspaceStateIt == this->workspaces.end()) { if (workspaceStateIt == this->workspaces.end()) {
continue; continue;
} }
WorkspaceState *workspaceState = workspaceStateIt->second; WorkspaceState *workspaceState = workspaceStateIt->second;
auto mit = this->monitors.find(workspaceState->monitorId); auto mit = this->monitors.find(workspaceState->monitorId);
if (mit != this->monitors.end()) { if (mit != this->monitors.end()) {
workspaceState->focused = mit->second.focusedWorkspaceId == workspaceId; workspaceState->focused = mit->second.focusedWorkspaceId == workspaceId;
} else { } else {
@@ -270,7 +270,6 @@ void HyprlandService::onUrgentEvent(std::string windowAddress) {
it->second->urgentWindows.push_back(windowAddress); it->second->urgentWindows.push_back(windowAddress);
workspaceStateChanged.emit(); workspaceStateChanged.emit();
} }
} }
break; break;
@@ -287,10 +286,10 @@ void HyprlandService::onActiveWindowEvent(std::string windowAddress) {
if (addr == "0x" + windowAddress) { if (addr == "0x" + windowAddress) {
int workspaceId = clientJson["workspace"]["id"]; int workspaceId = clientJson["workspace"]["id"];
auto it = this->workspaces.find(workspaceId); auto it = this->workspaces.find(workspaceId);
if (it != this->workspaces.end() && it->second) { if (it != this->workspaces.end() && it->second) {
WorkspaceState *ws = it->second; WorkspaceState *ws = it->second;
auto uit = std::find(ws->urgentWindows.begin(), ws->urgentWindows.end(), windowAddress); auto uit = std::find(ws->urgentWindows.begin(), ws->urgentWindows.end(), windowAddress);
if (uit != ws->urgentWindows.end()) { if (uit != ws->urgentWindows.end()) {
ws->urgentWindows.erase(uit); ws->urgentWindows.erase(uit);
workspaceStateChanged.emit(); workspaceStateChanged.emit();

View File

@@ -0,0 +1,307 @@
#include "services/notifications.hpp"
#include <gio/gio.h>
#include <iostream>
static constexpr const char *kNotificationsObjectPath = "/org/freedesktop/Notifications";
static constexpr const char *kNotificationsInterface = "org.freedesktop.Notifications";
static const char *kNotificationsIntrospectionXml = R"XML(
<node>
<interface name="org.freedesktop.Notifications">
<method name="Notify">
<arg type="s" direction="in"/>
<arg type="u" direction="in"/>
<arg type="s" direction="in"/>
<arg type="s" direction="in"/>
<arg type="s" direction="in"/>
<arg type="as" direction="in"/>
<arg type="a{sv}" direction="in"/>
<arg type="i" direction="in"/>
<arg type="u" direction="out"/>
</method>
<method name="CloseNotification">
<arg type="u" direction="in"/>
</method>
<method name="GetCapabilities">
<arg type="as" direction="out"/>
</method>
<method name="GetServerInformation">
<arg type="s" direction="out"/>
<arg type="s" direction="out"/>
<arg type="s" direction="out"/>
<arg type="s" direction="out"/>
</method>
<signal name="NotificationClosed">
<arg type="u"/>
<arg type="u"/>
</signal>
<signal name="ActionInvoked">
<arg type="u"/>
<arg type="s"/>
</signal>
<signal name="ActivationToken">
<arg type="u"/>
<arg type="s"/>
</signal>
</interface>
</node>)XML";
static void on_method_call(GDBusConnection * /*connection*/,
const gchar * /*sender*/,
const gchar * /*object_path*/,
const gchar *interface_name,
const gchar *method_name,
GVariant *parameters,
GDBusMethodInvocation *invocation,
gpointer user_data) {
auto *self = static_cast<NotificationService *>(user_data);
if (g_strcmp0(interface_name, kNotificationsInterface) != 0) {
g_dbus_method_invocation_return_dbus_error(
invocation,
"org.freedesktop.DBus.Error.UnknownInterface",
"Unknown interface");
return;
}
if (g_strcmp0(method_name, "Notify") == 0) {
const gchar *app_name = "";
guint32 replaces_id = 0;
const gchar *app_icon = "";
const gchar *summary = "";
const gchar *body = "";
GVariant *actions = nullptr;
GVariant *hints = nullptr;
gint32 expire_timeout = -1;
g_variant_get(parameters, "(&su&s&s&s@as@a{sv}i)",
&app_name,
&replaces_id,
&app_icon,
&summary,
&body,
&actions,
&hints,
&expire_timeout);
std::cout << "--- Notification ---" << std::endl;
std::cout << "App: " << (app_name ? app_name : "") << std::endl;
std::cout << "Title: " << (summary ? summary : "") << std::endl;
std::cout << "Body: " << (body ? body : "") << std::endl;
if (actions)
g_variant_unref(actions);
if (hints)
g_variant_unref(hints);
guint32 id = self->allocateNotificationId(replaces_id);
g_dbus_method_invocation_return_value(invocation, g_variant_new("(u)", id));
return;
}
if (g_strcmp0(method_name, "GetCapabilities") == 0) {
// Advertise common capabilities so clients don't disable notifications.
// (Many apps probe this first and may skip Notify if it's empty.)
const gchar *caps[] = {
"body",
"actions",
"body-markup",
"icon-static",
"persistence",
nullptr};
GVariant *capsV = g_variant_new_strv(caps, -1);
g_dbus_method_invocation_return_value(invocation, g_variant_new("(@as)", capsV));
return;
}
if (g_strcmp0(method_name, "GetServerInformation") == 0) {
g_dbus_method_invocation_return_value(
invocation,
g_variant_new("(ssss)", "bar", "bar", "0.1", "1.2"));
return;
}
if (g_strcmp0(method_name, "CloseNotification") == 0) {
guint32 id = 0;
g_variant_get(parameters, "(u)", &id);
// reason: 3 = closed by call to CloseNotification
if (self && self->getConnection()) {
g_dbus_connection_emit_signal(
self->getConnection(),
nullptr,
kNotificationsObjectPath,
kNotificationsInterface,
"NotificationClosed",
g_variant_new("(uu)", id, 3u),
nullptr);
}
g_dbus_method_invocation_return_value(invocation, nullptr);
return;
}
g_dbus_method_invocation_return_dbus_error(
invocation,
"org.freedesktop.DBus.Error.UnknownMethod",
"Unknown method");
}
guint32 NotificationService::allocateNotificationId(guint32 replacesId) {
if (replacesId != 0)
return replacesId;
return this->nextNotificationId++;
}
static const GDBusInterfaceVTable kVTable = {
.method_call = on_method_call,
.get_property = nullptr,
.set_property = nullptr,
};
NotificationService::~NotificationService() {
if (this->connection) {
// Best-effort release of the well-known name.
{
GError *error = nullptr;
GVariant *releaseResult = g_dbus_connection_call_sync(
this->connection,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"ReleaseName",
g_variant_new("(s)", "org.freedesktop.Notifications"),
G_VARIANT_TYPE("(u)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
nullptr,
&error);
if (releaseResult)
g_variant_unref(releaseResult);
if (error)
g_error_free(error);
}
if (this->registrationId != 0) {
g_dbus_connection_unregister_object(this->connection, this->registrationId);
this->registrationId = 0;
}
g_object_unref(this->connection);
this->connection = nullptr;
}
if (this->nodeInfo) {
g_dbus_node_info_unref(this->nodeInfo);
this->nodeInfo = nullptr;
}
}
void NotificationService::intialize() {
GError *error = nullptr;
if (this->connection) {
return;
}
gchar *address = g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SESSION, nullptr, &error);
if (!address) {
std::cerr << "Failed to get session bus address: " << (error ? error->message : "unknown error") << std::endl;
if (error)
g_error_free(error);
return;
}
if (error) {
g_error_free(error);
error = nullptr;
}
this->connection = g_dbus_connection_new_for_address_sync(
address,
static_cast<GDBusConnectionFlags>(
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION),
nullptr,
nullptr,
&error);
g_free(address);
if (!this->connection) {
std::cerr << "Failed to connect to session bus: " << (error ? error->message : "unknown error") << std::endl;
if (error)
g_error_free(error);
return;
}
if (error) {
g_error_free(error);
error = nullptr;
}
this->nodeInfo = g_dbus_node_info_new_for_xml(kNotificationsIntrospectionXml, &error);
if (!this->nodeInfo) {
std::cerr << "Failed to create introspection data: " << (error ? error->message : "unknown error") << std::endl;
if (error)
g_error_free(error);
return;
}
GDBusInterfaceInfo *iface = g_dbus_node_info_lookup_interface(this->nodeInfo, kNotificationsInterface);
if (!iface) {
std::cerr << "Missing interface info for org.freedesktop.Notifications" << std::endl;
return;
}
this->registrationId = g_dbus_connection_register_object(
this->connection,
kNotificationsObjectPath,
iface,
&kVTable,
this,
nullptr,
&error);
if (this->registrationId == 0) {
std::cerr << "Failed to register notifications object: " << (error ? error->message : "unknown error") << std::endl;
if (error)
g_error_free(error);
return;
}
// Request the well-known name synchronously so we can detect conflicts.
// Reply codes: 1=PRIMARY_OWNER, 2=IN_QUEUE, 3=EXISTS, 4=ALREADY_OWNER
GVariant *requestResult = g_dbus_connection_call_sync(
this->connection,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"RequestName",
g_variant_new("(su)", "org.freedesktop.Notifications", 0u),
G_VARIANT_TYPE("(u)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
nullptr,
&error);
if (!requestResult) {
std::cerr << "Failed to RequestName(org.freedesktop.Notifications): "
<< (error ? error->message : "unknown error") << std::endl;
if (error)
g_error_free(error);
return;
}
guint32 reply = 0;
g_variant_get(requestResult, "(u)", &reply);
g_variant_unref(requestResult);
if (reply != 1u && reply != 4u) {
std::cerr << "org.freedesktop.Notifications is already owned (RequestName reply=" << reply
<< "). Stop your existing notification daemon (e.g. dunst/mako/swaync) or allow replacement." << std::endl;
return;
}
std::cout << "Notifications daemon active (org.freedesktop.Notifications)." << std::endl;
}

View File

@@ -3,23 +3,7 @@
#include <gtkmm/label.h> #include <gtkmm/label.h>
#include <webkit/webkit.h> #include <webkit/webkit.h>
WebWidget::WebWidget(std::string icon, std::string title, std::string url) { WebWidget::WebWidget(std::string icon, std::string name, std::string url) : Popover(icon, name) {
auto label = Gtk::make_managed<Gtk::Label>(icon);
label->add_css_class("icon-label");
set_child(*label);
signal_clicked().connect(
sigc::mem_fun(*this, &WebWidget::on_toggle_window));
popover = new Gtk::Popover();
popover->set_parent(*this);
popover->set_autohide(true);
popover->signal_closed().connect([this]() {
this->add_css_class("minimized");
this->remove_css_class("restored");
});
auto webview = webkit_web_view_new(); auto webview = webkit_web_view_new();
gtk_widget_set_hexpand(webview, true); gtk_widget_set_hexpand(webview, true);
gtk_widget_set_vexpand(webview, true); gtk_widget_set_vexpand(webview, true);
@@ -31,19 +15,5 @@ WebWidget::WebWidget(std::string icon, std::string title, std::string url) {
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webview), url.c_str()); webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webview), url.c_str());
gtk_popover_set_child(popover->gobj(), webview); this->set_popover_child(*Glib::wrap(webview));
}
WebWidget::~WebWidget() {
delete popover;
}
void WebWidget::on_toggle_window() {
if (popover->get_visible()) {
popover->popdown();
} else {
popover->popup();
this->remove_css_class("minimized");
this->add_css_class("restored");
}
} }