Skip to content
API Documentation
BastionGuard™
TrayIcon.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 *
17 * BastionGuard™ is a trademark of Calogero Scarnà.
18 * The BastionGuard™ name and branding are not licensed under the GPL.
19 */
20
21#include "TrayIcon.hpp"
22
23#include <arpa/inet.h>
24#include <chrono>
25#include <cstring>
26#include <random>
27#include <thread>
28#include <unistd.h>
29#include <vector>
30#include <glibmm/main.h>
31#include <string>
32#include <gdk-pixbuf/gdk-pixbuf.h>
33#include <cstdio>
34#include <glib.h>
35#include <glib/gi18n.h>
36#include <inttypes.h>
37
38#define TI_LOG(fmt, ...) \
39 std::fprintf(stderr, "[TrayIcon] " fmt "\n", ##__VA_ARGS__)
40
41static gboolean invoke_std_function(gpointer data) {
42 auto* fn = static_cast<std::function<void()>*>(data);
43 (*fn)();
44 delete fn;
45 return G_SOURCE_REMOVE;
46}
47
48
49
50static const sd_bus_vtable sni_vtable[] = {
51 SD_BUS_VTABLE_START(0),
52
53 SD_BUS_METHOD("Activate", "ii", "", TrayIcon::sni_method_activate, SD_BUS_VTABLE_UNPRIVILEGED),
54 SD_BUS_METHOD("ContextMenu", "ii", "", TrayIcon::sni_method_context_menu, SD_BUS_VTABLE_UNPRIVILEGED),
55 SD_BUS_METHOD("SecondaryActivate", "ii", "", TrayIcon::sni_method_secondary_activate, SD_BUS_VTABLE_UNPRIVILEGED),
56
57 SD_BUS_PROPERTY("Category", "s", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_CONST),
58 SD_BUS_PROPERTY("Id", "s", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_CONST),
59 SD_BUS_PROPERTY("Title", "s", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
60 SD_BUS_PROPERTY("Status", "s", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
61 SD_BUS_PROPERTY("IconName", "s", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
62 SD_BUS_PROPERTY("IconPixmap", "a(iiay)", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
63 SD_BUS_PROPERTY("ToolTip", "(sa(iiay)ss)", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
64 SD_BUS_PROPERTY("Menu", "o", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_CONST),
65
66 SD_BUS_SIGNAL("NewIcon", "", 0),
67 SD_BUS_SIGNAL("NewTitle", "", 0),
68 SD_BUS_SIGNAL("NewStatus", "", 0),
69 SD_BUS_SIGNAL("NewToolTip", "", 0),
70
71 SD_BUS_VTABLE_END
72};
73
74
75static const sd_bus_vtable dbusmenu_vtable[] = {
76 SD_BUS_VTABLE_START(0),
77
78 SD_BUS_METHOD("GetLayout", "iias", "u(ia{sv}av)", TrayIcon::dbusmenu_method_get_layout, SD_BUS_VTABLE_UNPRIVILEGED),
79 SD_BUS_METHOD("GetChildren", "i", "ai", TrayIcon::dbusmenu_method_get_children, SD_BUS_VTABLE_UNPRIVILEGED),
80 SD_BUS_METHOD("GetGroupProperties", "aias", "a(ia{sv})", TrayIcon::dbusmenu_method_get_group_properties, SD_BUS_VTABLE_UNPRIVILEGED),
81 SD_BUS_METHOD("GetProperty", "is", "v", TrayIcon::dbusmenu_method_get_property, SD_BUS_VTABLE_UNPRIVILEGED),
82 SD_BUS_METHOD("GetStatus", "", "s", TrayIcon::dbusmenu_method_get_status, SD_BUS_VTABLE_UNPRIVILEGED),
83 SD_BUS_METHOD("Event", "isvu", "", TrayIcon::dbusmenu_method_event, SD_BUS_VTABLE_UNPRIVILEGED),
84 SD_BUS_METHOD("AboutToShow", "i", "b", TrayIcon::dbusmenu_method_about_to_show, SD_BUS_VTABLE_UNPRIVILEGED),
85 // ↓↓ SPOSTATI QUI da sni_vtable ↓↓
86 SD_BUS_METHOD("EventGroup", "a(isvu)", "ai", TrayIcon::dbusmenu_method_event_group, SD_BUS_VTABLE_UNPRIVILEGED),
87 SD_BUS_METHOD("AboutToShowGroup", "ai", "aiai", TrayIcon::dbusmenu_method_about_to_show_group, SD_BUS_VTABLE_UNPRIVILEGED),
88
89 SD_BUS_PROPERTY("Version", "u", TrayIcon::dbusmenu_prop_get, 0, SD_BUS_VTABLE_PROPERTY_CONST),
90
91 SD_BUS_SIGNAL("LayoutUpdated", "ui", 0),
92 SD_BUS_SIGNAL("ItemsPropertiesUpdated", "a(ia{sv})a(ias)", 0),
93
94 SD_BUS_VTABLE_END
95};
96
97static inline const char* label_for_menu_id(int id) {
98 switch (id) {
99 case 1: return _("Mostra/Maximizza finestra");
100 case 2: return _("Impostazioni");
101 case 3: return _("Esci");
102 default: return "";
103 }
104}
105
106static inline bool enabled_for_menu_id(int) {
107 return true;
108}
109
110
111std::string TrayIcon::make_service_name() {
112 std::random_device rd;
113 std::mt19937 gen(rd());
114 std::uniform_int_distribution<int> dis(1000, 9999);
115 return "org.kde.StatusNotifierItem-" + std::to_string(getpid()) + "-" + std::to_string(dis(gen));
116}
117
119 service_name_ = make_service_name();
120 id_ = "BastionGuard";
121 title_ = "BastionGuard";
122 status_ = "Active";
123 icon_name_ = "security-high";
124 tooltip_ = "BastionGuard";
125}
126
128 stop();
129 if (ui_ctx_) {
130 g_main_context_unref(ui_ctx_);
131 ui_ctx_ = nullptr;
132 }
133}
134
136 if (running_.exchange(true)) return true;
137
138 if (!acquire_bus()) { running_ = false; return false; }
139 if (!request_name()) { release_bus(); running_ = false; return false; }
140 if (!export_objects()) { release_bus(); running_ = false; return false; }
141
142 bus_thread_ = std::thread([this]() {
143 bool ok = register_to_watcher();
144 std::fprintf(stderr, "[TrayIcon] register_to_watcher: %s (service: %s)\n",
145 ok ? "OK" : "FAILED", service_name_.c_str());
146
147 std::this_thread::sleep_for(std::chrono::milliseconds(150));
148 emit_dbusmenu_layout_updated();
149
150 bus_loop();
151 });
152
153 return true;
154}
155
156int TrayIcon::dbusmenu_method_about_to_show_group(sd_bus_message* m, void* userdata, sd_bus_error*) {
157 auto* self = static_cast<TrayIcon*>(userdata);
158
159 TI_LOG("AboutToShowGroup() called");
160
161 int r = sd_bus_message_enter_container(m, 'a', "i");
162 if (r < 0) return r;
163
164 std::vector<int32_t> ids;
165 for (;;) {
166 int32_t id = 0;
167 r = sd_bus_message_read(m, "i", &id);
168 if (r < 0) return r;
169 if (r == 0) break;
170 ids.push_back(id);
171 }
172
173 r = sd_bus_message_exit_container(m);
174 if (r < 0) return r;
175
176 TI_LOG("AboutToShowGroup ids count=%zu", ids.size());
177
178 sd_bus_message* reply = nullptr;
179 r = sd_bus_message_new_method_return(m, &reply);
180 if (r < 0) return r;
181
182 r = sd_bus_message_open_container(reply, 'a', "i");
183 if (r < 0) { sd_bus_message_unref(reply); return r; }
184 r = sd_bus_message_close_container(reply);
185 if (r < 0) { sd_bus_message_unref(reply); return r; }
186
187 r = sd_bus_message_open_container(reply, 'a', "i");
188 if (r < 0) { sd_bus_message_unref(reply); return r; }
189 r = sd_bus_message_close_container(reply);
190 if (r < 0) { sd_bus_message_unref(reply); return r; }
191
192 sd_bus* bus = sd_bus_message_get_bus(m);
193 r = sd_bus_send(bus, reply, nullptr);
194 sd_bus_message_unref(reply);
195 return (r < 0) ? r : 1;
196}
197
198int TrayIcon::dbusmenu_method_event_group(sd_bus_message* m, void* userdata, sd_bus_error*) {
199 auto* self = static_cast<TrayIcon*>(userdata);
200
201 TI_LOG("EventGroup() called");
202
203 int r = sd_bus_message_enter_container(m, 'a', "(isvu)");
204 if (r < 0) return r;
205
206 struct Ev { int32_t id; std::string ev; uint32_t ts; };
207 std::vector<Ev> events;
208
209 for (;;) {
210 r = sd_bus_message_enter_container(m, 'r', "isvu");
211 if (r < 0) return r;
212 if (r == 0) break;
213
214 int32_t id = 0;
215 const char* ev_c = nullptr;
216 uint32_t ts = 0;
217
218 r = sd_bus_message_read(m, "is", &id, &ev_c);
219 if (r < 0) return r;
220
221 r = sd_bus_message_skip(m, "v");
222 if (r < 0) return r;
223
224 r = sd_bus_message_read(m, "u", &ts);
225 if (r < 0) return r;
226
227 r = sd_bus_message_exit_container(m);
228 if (r < 0) return r;
229
230 std::string ev = ev_c ? ev_c : "";
231 TI_LOG("EventGroup item: id=%d ev='%s' ts=%" PRIu32, (int)id, ev.c_str(), ts);
232 events.push_back({id, ev, ts});
233 }
234
235 r = sd_bus_message_exit_container(m);
236 if (r < 0) return r;
237
238 if (!self->ui_ctx_) {
239 TI_LOG("WARNING: ui_ctx_ null (bind_ui_context not called?)");
240 } else {
241 for (const auto& e : events) {
242 const int32_t id = e.id;
243 g_main_context_invoke(self->ui_ctx_, invoke_std_function,
244 new std::function<void()>([self, id]() {
245 TI_LOG("Dispatch on UI: id=%d", (int)id);
246
247 if (id == MENU_SHOW) {
248 TI_LOG("-> MENU_SHOW");
249 if (self->on_activate_cb_) self->on_activate_cb_();
250 else TI_LOG("on_activate_cb_ is null");
251 } else if (id == MENU_SETTINGS) {
252 TI_LOG("-> MENU_SETTINGS");
253 if (self->on_settings_cb_) self->on_settings_cb_();
254 else TI_LOG("on_settings_cb_ is null");
255 } else if (id == MENU_QUIT) {
256 TI_LOG("-> MENU_QUIT");
257 if (self->on_quit_cb_) self->on_quit_cb_();
258 else TI_LOG("on_quit_cb_ is null");
259 } else {
260 TI_LOG("Unknown menu id=%d", (int)id);
261 }
262 })
263 );
264 }
265 }
266
267 sd_bus_message* reply = nullptr;
268 r = sd_bus_message_new_method_return(m, &reply);
269 if (r < 0) return r;
270
271 r = sd_bus_message_open_container(reply, 'a', "i");
272 if (r < 0) { sd_bus_message_unref(reply); return r; }
273 r = sd_bus_message_close_container(reply);
274 if (r < 0) { sd_bus_message_unref(reply); return r; }
275
276 sd_bus* bus = sd_bus_message_get_bus(m);
277 r = sd_bus_send(bus, reply, nullptr);
278 sd_bus_message_unref(reply);
279 return (r < 0) ? r : 1;
280}
281
283 if (!running_.exchange(false)) return;
284
285 if (bus_thread_.joinable() &&
286 std::this_thread::get_id() == bus_thread_.get_id()) {
287 return;
288 }
289
290 if (bus_thread_.joinable())
291 bus_thread_.join();
292
293 unexport_objects();
294 release_bus();
295}
296
298 if (ui_ctx_) return;
299 ui_ctx_ = g_main_context_ref(g_main_context_default());
300}
301
302void TrayIcon::set_icon_file(const std::string& png_path, int target_px) {
303 GError* err = nullptr;
304 GdkPixbuf* pb = gdk_pixbuf_new_from_file(png_path.c_str(), &err);
305 if (!pb) {
306 std::fprintf(stderr, "[TrayIcon] cannot load icon '%s': %s\n",
307 png_path.c_str(), err ? err->message : "unknown error");
308 if (err) g_error_free(err);
309 have_pixmap_ = false;
310 emit_sni_signal("NewIcon");
311 return;
312 }
313
314 if (target_px > 0) {
315 int w = gdk_pixbuf_get_width(pb);
316 int h = gdk_pixbuf_get_height(pb);
317 if (w != target_px || h != target_px) {
318 GdkPixbuf* scaled = gdk_pixbuf_scale_simple(pb, target_px, target_px, GDK_INTERP_BILINEAR);
319 g_object_unref(pb);
320 pb = scaled;
321 }
322 }
323
324 GdkPixbuf* rgba = gdk_pixbuf_add_alpha(pb, FALSE, 0, 0, 0);
325 g_object_unref(pb);
326
327 pix_w_ = gdk_pixbuf_get_width(rgba);
328 pix_h_ = gdk_pixbuf_get_height(rgba);
329
330 const int rowstride = gdk_pixbuf_get_rowstride(rgba);
331 const int nchan = gdk_pixbuf_get_n_channels(rgba);
332 const guchar* pixels = gdk_pixbuf_get_pixels(rgba);
333
334 pix_rgba_.resize((size_t)pix_w_ * (size_t)pix_h_ * 4u);
335
336 for (int y = 0; y < pix_h_; ++y) {
337 const guchar* src = pixels + y * rowstride;
338 uint8_t* dst = pix_rgba_.data() + (size_t)y * (size_t)pix_w_ * 4u;
339
340 for (int x = 0; x < pix_w_; ++x) {
341 uint8_t r = src[x * nchan + 0];
342 uint8_t g = src[x * nchan + 1];
343 uint8_t b = src[x * nchan + 2];
344 uint8_t a = (nchan >= 4) ? src[x * nchan + 3] : 255u;
345
346 uint32_t argb = ((uint32_t)a << 24) |
347 ((uint32_t)r << 16) |
348 ((uint32_t)g << 8) |
349 (uint32_t)b;
350 argb = htobe32(argb);
351 std::memcpy(dst + x * 4, &argb, 4);
352 }
353 }
354
355 g_object_unref(rgba);
356
357 have_pixmap_ = true;
358 icon_name_.clear();
359
360 emit_sni_signal("NewIcon");
361}
362
363void TrayIcon::set_icon(const std::string& icon_name) {
364 icon_name_ = icon_name.empty() ? "security-high" : icon_name;
365 emit_sni_signal("NewIcon");
366}
367
368void TrayIcon::set_tooltip(const std::string& text) {
369 tooltip_ = text;
370 emit_sni_signal("NewToolTip");
371}
372
373void TrayIcon::set_title(const std::string& title) {
374 title_ = title;
375 emit_sni_signal("NewTitle");
376}
377
378bool TrayIcon::acquire_bus() {
379 if (sd_bus_open_user(&bus_) < 0) {
380 bus_ = nullptr;
381 return false;
382 }
383 return true;
384}
385
386bool TrayIcon::request_name() {
387 int r = sd_bus_request_name(bus_, service_name_.c_str(), 0);
388 return r >= 0;
389}
390
391void TrayIcon::release_bus() {
392 if (!bus_) return;
393 sd_bus_unref(bus_);
394 bus_ = nullptr;
395}
396
397bool TrayIcon::export_objects() {
398 int r1 = sd_bus_add_object_vtable(
399 bus_, &sni_slot_,
400 "/StatusNotifierItem",
401 "org.kde.StatusNotifierItem",
403 this
404 );
405 if (r1 < 0) return false;
406
407 int r2 = sd_bus_add_object_vtable(
408 bus_, &menu_slot_,
409 "/Menu",
410 "com.canonical.dbusmenu",
412 this
413 );
414 if (r2 < 0) return false;
415
416 return true;
417}
418
419void TrayIcon::unexport_objects() {
420 if (sni_slot_) { sd_bus_slot_unref(sni_slot_); sni_slot_ = nullptr; }
421 if (menu_slot_) { sd_bus_slot_unref(menu_slot_); menu_slot_ = nullptr; }
422}
423
424bool TrayIcon::register_to_watcher() {
425 sd_bus_message* m = nullptr;
426 sd_bus_error er = SD_BUS_ERROR_NULL;
427
428 int r = sd_bus_message_new_method_call(
429 bus_, &m,
430 "org.kde.StatusNotifierWatcher",
431 "/StatusNotifierWatcher",
432 "org.kde.StatusNotifierWatcher",
433 "RegisterStatusNotifierItem"
434 );
435 if (r < 0) return false;
436
437 r = sd_bus_message_append(m, "s", service_name_.c_str());
438 if (r < 0) { sd_bus_message_unref(m); return false; }
439
440 r = sd_bus_call(bus_, m, 0, &er, nullptr);
441 if (r < 0)
442 std::fprintf(stderr, "[TrayIcon] RegisterStatusNotifierItem error: %s\n",
443 er.message ? er.message : "(unknown)");
444
445 sd_bus_error_free(&er);
446 sd_bus_message_unref(m);
447 return r >= 0;
448}
449
450void TrayIcon::bus_loop() {
451 while (running_) {
452 for (;;) {
453 int r = sd_bus_process(bus_, nullptr);
454 if (r < 0) {
455 running_ = false;
456 break;
457 }
458 if (r == 0) break;
459 }
460
461 if (!running_) break;
462
463 (void)sd_bus_wait(bus_, 100000);
464 }
465}
466
467void TrayIcon::emit_sni_signal(const char* signal_name) {
468 if (!bus_) return;
469 (void)sd_bus_emit_signal(
470 bus_,
471 "/StatusNotifierItem",
472 "org.kde.StatusNotifierItem",
473 signal_name,
474 ""
475 );
476}
477
478void TrayIcon::emit_dbusmenu_layout_updated() {
479 if (!bus_) return;
480 const uint32_t revision = 1u;
481 const int32_t parent = MENU_ROOT;
482 (void)sd_bus_emit_signal(
483 bus_,
484 "/Menu",
485 "com.canonical.dbusmenu",
486 "LayoutUpdated",
487 "ui",
488 revision,
489 parent
490 );
491}
492
493int TrayIcon::sni_method_activate(sd_bus_message* m, void* userdata, sd_bus_error*) {
494 auto* self = static_cast<TrayIcon*>(userdata);
495
496 std::fprintf(stderr, "[TrayIcon] SNI Activate received\n");
497
498 if (!self->ui_ctx_) {
499 std::fprintf(stderr, "[TrayIcon] WARNING: ui_ctx_ null in Activate -> fallback default\n");
500 self->ui_ctx_ = g_main_context_ref(g_main_context_default());
501 }
502
503 if (self->on_activate_cb_) {
504 g_main_context_invoke(self->ui_ctx_, invoke_std_function,
505 new std::function<void()>([self]() {
506 std::fprintf(stderr, "[TrayIcon] SNI Activate dispatch on UI thread\n");
507 self->on_activate_cb_();
508 })
509 );
510 }
511
512 return sd_bus_reply_method_return(m, "");
513}
514
515int TrayIcon::sni_method_context_menu(sd_bus_message* m, void*, sd_bus_error*) {
516 return sd_bus_reply_method_return(m, "");
517}
518
519int TrayIcon::sni_method_secondary_activate(sd_bus_message* m, void*, sd_bus_error*) {
520 return sd_bus_reply_method_return(m, "");
521}
522
523int TrayIcon::sni_prop_get(sd_bus*, const char*, const char*,
524 const char* property, sd_bus_message* reply,
525 void* userdata, sd_bus_error* ret_error) {
526 auto* self = static_cast<TrayIcon*>(userdata);
527
528 if (std::strcmp(property, "Category") == 0)
529 return sd_bus_message_append(reply, "s", "ApplicationStatus");
530
531 if (std::strcmp(property, "Id") == 0)
532 return sd_bus_message_append(reply, "s", self->id_.c_str());
533
534 if (std::strcmp(property, "Title") == 0)
535 return sd_bus_message_append(reply, "s", self->title_.c_str());
536
537 if (std::strcmp(property, "Status") == 0)
538 return sd_bus_message_append(reply, "s", self->status_.c_str());
539
540 if (std::strcmp(property, "IconName") == 0) {
541 if (self->have_pixmap_)
542 return sd_bus_message_append(reply, "s", "");
543 return sd_bus_message_append(reply, "s", self->icon_name_.c_str());
544 }
545
546 if (std::strcmp(property, "IconPixmap") == 0) {
547 int r = sd_bus_message_open_container(reply, 'a', "(iiay)");
548 if (r < 0) return r;
549
550 if (self->have_pixmap_ && self->pix_w_ > 0 && self->pix_h_ > 0
551 && !self->pix_rgba_.empty())
552 {
553 r = sd_bus_message_open_container(reply, 'r', "iiay");
554 if (r < 0) return r;
555
556 r = sd_bus_message_append(reply, "ii", self->pix_w_, self->pix_h_);
557 if (r < 0) return r;
558
559 r = sd_bus_message_append_array(reply, 'y',
560 self->pix_rgba_.data(),
561 self->pix_rgba_.size());
562 if (r < 0) return r;
563
564 r = sd_bus_message_close_container(reply);
565 if (r < 0) return r;
566 }
567
568 return sd_bus_message_close_container(reply);
569 }
570
571 if (std::strcmp(property, "Menu") == 0)
572 return sd_bus_message_append(reply, "o", "/Menu");
573
574 if (std::strcmp(property, "ToolTip") == 0) {
575 int r = sd_bus_message_open_container(reply, 'r', "sa(iiay)ss");
576 if (r < 0) return r;
577
578 r = sd_bus_message_append(reply, "s", self->icon_name_.c_str());
579 if (r < 0) return r;
580
581 r = sd_bus_message_open_container(reply, 'a', "(iiay)");
582 if (r < 0) return r;
583 r = sd_bus_message_close_container(reply);
584 if (r < 0) return r;
585
586 r = sd_bus_message_append(reply, "s", self->title_.c_str());
587 if (r < 0) return r;
588 r = sd_bus_message_append(reply, "s", self->tooltip_.c_str());
589 if (r < 0) return r;
590
591 r = sd_bus_message_close_container(reply);
592 if (r < 0) return r;
593 return 0;
594 }
595
596 return sd_bus_error_setf(ret_error, SD_BUS_ERROR_UNKNOWN_PROPERTY,
597 "Unknown property '%s'", property);
598}
599
600int TrayIcon::dbusmenu_prop_get(sd_bus*, const char*, const char*,
601 const char* /*property*/, sd_bus_message* reply,
602 void*, sd_bus_error*) {
603 return sd_bus_message_append(reply, "u", 3u);
604}
605
606static int append_menu_item(sd_bus_message* m, int id,
607 const char* label, bool enabled = true) {
608 int r = 0;
609 r = sd_bus_message_open_container(m, 'r', "ia{sv}av"); if (r < 0) return r;
610 r = sd_bus_message_append(m, "i", id); if (r < 0) return r;
611
612 r = sd_bus_message_open_container(m, 'a', "{sv}"); if (r < 0) return r;
613
614 r = sd_bus_message_open_container(m, 'e', "sv"); if (r < 0) return r;
615 r = sd_bus_message_append(m, "s", "label"); if (r < 0) return r;
616 r = sd_bus_message_open_container(m, 'v', "s"); if (r < 0) return r;
617 r = sd_bus_message_append(m, "s", label); if (r < 0) return r;
618 r = sd_bus_message_close_container(m); if (r < 0) return r;
619 r = sd_bus_message_close_container(m); if (r < 0) return r;
620
621 r = sd_bus_message_open_container(m, 'e', "sv"); if (r < 0) return r;
622 r = sd_bus_message_append(m, "s", "enabled"); if (r < 0) return r;
623 r = sd_bus_message_open_container(m, 'v', "b"); if (r < 0) return r;
624 r = sd_bus_message_append(m, "b", enabled ? 1 : 0); if (r < 0) return r;
625 r = sd_bus_message_close_container(m); if (r < 0) return r;
626 r = sd_bus_message_close_container(m); if (r < 0) return r;
627
628 r = sd_bus_message_close_container(m); if (r < 0) return r;
629
630 r = sd_bus_message_open_container(m, 'a', "v"); if (r < 0) return r;
631 r = sd_bus_message_close_container(m); if (r < 0) return r;
632
633 return sd_bus_message_close_container(m);
634}
635
636static int append_item_properties(sd_bus_message* m, int id,
637 const char* label, bool enabled) {
638 int r = 0;
639 r = sd_bus_message_open_container(m, 'r', "ia{sv}"); if (r < 0) return r;
640 r = sd_bus_message_append(m, "i", id); if (r < 0) return r;
641
642 r = sd_bus_message_open_container(m, 'a', "{sv}"); if (r < 0) return r;
643
644 r = sd_bus_message_open_container(m, 'e', "sv"); if (r < 0) return r;
645 r = sd_bus_message_append(m, "s", "label"); if (r < 0) return r;
646 r = sd_bus_message_open_container(m, 'v', "s"); if (r < 0) return r;
647 r = sd_bus_message_append(m, "s", label); if (r < 0) return r;
648 r = sd_bus_message_close_container(m); if (r < 0) return r;
649 r = sd_bus_message_close_container(m); if (r < 0) return r;
650
651 r = sd_bus_message_open_container(m, 'e', "sv"); if (r < 0) return r;
652 r = sd_bus_message_append(m, "s", "enabled"); if (r < 0) return r;
653 r = sd_bus_message_open_container(m, 'v', "b"); if (r < 0) return r;
654 r = sd_bus_message_append(m, "b", enabled ? 1 : 0); if (r < 0) return r;
655 r = sd_bus_message_close_container(m); if (r < 0) return r;
656 r = sd_bus_message_close_container(m); if (r < 0) return r;
657
658 r = sd_bus_message_close_container(m); if (r < 0) return r;
659 return sd_bus_message_close_container(m);
660}
661
662int TrayIcon::dbusmenu_method_get_status(sd_bus_message* m, void*, sd_bus_error*) {
663 return sd_bus_reply_method_return(m, "s", "normal");
664}
665
666int TrayIcon::dbusmenu_method_get_children(sd_bus_message* m, void* userdata, sd_bus_error*) {
667 auto* self = static_cast<TrayIcon*>(userdata);
668
669 int32_t parent = 0;
670 int r = sd_bus_message_read(m, "i", &parent);
671 if (r < 0) return r;
672
673 sd_bus_message* reply = nullptr;
674 r = sd_bus_message_new_method_return(m, &reply);
675 if (r < 0) return r;
676
677 r = sd_bus_message_open_container(reply, 'a', "i");
678 if (r < 0) { sd_bus_message_unref(reply); return r; }
679
680 if (parent == MENU_ROOT) {
681 int32_t ids[] = { MENU_SHOW, MENU_SETTINGS, MENU_QUIT };
682 for (int32_t id : ids) {
683 r = sd_bus_message_append(reply, "i", id);
684 if (r < 0) { sd_bus_message_unref(reply); return r; }
685 }
686 }
687
688 r = sd_bus_message_close_container(reply);
689 if (r < 0) { sd_bus_message_unref(reply); return r; }
690
691 sd_bus* bus = sd_bus_message_get_bus(m);
692 r = sd_bus_send(bus, reply, nullptr);
693 sd_bus_message_unref(reply);
694 return (r < 0) ? r : 1;
695}
696
697int TrayIcon::dbusmenu_method_get_layout(sd_bus_message* m, void* userdata, sd_bus_error*) {
698 auto* self = static_cast<TrayIcon*>(userdata);
699
700 int32_t parent_id = 0, depth = 0;
701 int r = sd_bus_message_read(m, "ii", &parent_id, &depth);
702 if (r < 0) return r;
703
704 r = sd_bus_message_skip(m, "as");
705 if (r < 0) return r;
706
707 sd_bus_message* reply = nullptr;
708 r = sd_bus_message_new_method_return(m, &reply);
709 if (r < 0) return r;
710
711 r = sd_bus_message_append(reply, "u", (uint32_t)1);
712 if (r < 0) { sd_bus_message_unref(reply); return r; }
713
714 r = sd_bus_message_open_container(reply, 'r', "ia{sv}av");
715 if (r < 0) { sd_bus_message_unref(reply); return r; }
716
717 r = sd_bus_message_append(reply, "i", MENU_ROOT);
718 if (r < 0) { sd_bus_message_unref(reply); return r; }
719
720 r = sd_bus_message_open_container(reply, 'a', "{sv}");
721 if (r < 0) { sd_bus_message_unref(reply); return r; }
722 r = sd_bus_message_close_container(reply);
723 if (r < 0) { sd_bus_message_unref(reply); return r; }
724
725 r = sd_bus_message_open_container(reply, 'a', "v");
726 if (r < 0) { sd_bus_message_unref(reply); return r; }
727
728 bool include_children = (depth != 0);
729
730 if (include_children) {
731 auto push_child = [&](int id) -> int {
732 int rr = sd_bus_message_open_container(reply, 'v', "(ia{sv}av)");
733 if (rr < 0) return rr;
734 rr = append_menu_item(reply, id,
737 if (rr < 0) return rr;
738 return sd_bus_message_close_container(reply);
739 };
740 r = push_child(MENU_SHOW); if (r < 0) { sd_bus_message_unref(reply); return r; }
741 r = push_child(MENU_SETTINGS); if (r < 0) { sd_bus_message_unref(reply); return r; }
742 r = push_child(MENU_QUIT); if (r < 0) { sd_bus_message_unref(reply); return r; }
743 }
744
745 r = sd_bus_message_close_container(reply);
746 if (r < 0) { sd_bus_message_unref(reply); return r; }
747
748 r = sd_bus_message_close_container(reply);
749 if (r < 0) { sd_bus_message_unref(reply); return r; }
750
751 sd_bus* bus = sd_bus_message_get_bus(m);
752 r = sd_bus_send(bus, reply, nullptr);
753 sd_bus_message_unref(reply);
754 return (r < 0) ? r : 1;
755}
756
757int TrayIcon::dbusmenu_method_event(sd_bus_message* m, void* userdata, sd_bus_error*) {
758 auto* self = static_cast<TrayIcon*>(userdata);
759
760 int32_t id = 0;
761 const char* event_id_c = nullptr;
762
763 int r = sd_bus_message_read(m, "is", &id, &event_id_c);
764 if (r < 0)
765 return sd_bus_reply_method_return(m, "");
766
767 std::string ev = event_id_c ? event_id_c : "";
768
769 (void)sd_bus_message_skip(m, "vu");
770
771 std::fprintf(stderr, "[TrayIcon] Event: id=%d ev='%s'\n", (int)id, ev.c_str());
772
773 if (!self->ui_ctx_) {
774 std::fprintf(stderr, "[TrayIcon] WARNING: ui_ctx_ null (bind_ui_context?)\n");
775 return sd_bus_reply_method_return(m, "");
776 }
777
778 g_main_context_invoke(self->ui_ctx_, invoke_std_function,
779 new std::function<void()>([self, id]() {
780 std::fprintf(stderr, "[TrayIcon] Dispatch on UI: id=%d\n", (int)id);
781
782 if (id == MENU_SHOW) {
783 if (self->on_activate_cb_) self->on_activate_cb_();
784 } else if (id == MENU_SETTINGS) {
785 if (self->on_settings_cb_) self->on_settings_cb_();
786 } else if (id == MENU_QUIT) {
787 if (self->on_quit_cb_) self->on_quit_cb_();
788 } else {
789 std::fprintf(stderr, "[TrayIcon] Unknown menu id=%d\n", (int)id);
790 }
791 })
792 );
793
794 return sd_bus_reply_method_return(m, "");
795}
796
797int TrayIcon::dbusmenu_method_about_to_show(sd_bus_message* m, void*, sd_bus_error*) {
798 int32_t id = 0;
799 int r = sd_bus_message_read(m, "i", &id);
800 if (r < 0) return r;
801 return sd_bus_reply_method_return(m, "b", 0);
802}
803
804int TrayIcon::dbusmenu_method_get_group_properties(sd_bus_message* m, void* userdata, sd_bus_error*) {
805 auto* self = static_cast<TrayIcon*>(userdata);
806
807 int r = sd_bus_message_enter_container(m, 'a', "i");
808 if (r < 0) return r;
809
810 std::vector<int32_t> ids;
811 for (;;) {
812 int32_t id = 0;
813 r = sd_bus_message_read(m, "i", &id);
814 if (r < 0) return r;
815 if (r == 0) break;
816 ids.push_back(id);
817 }
818
819 r = sd_bus_message_exit_container(m);
820 if (r < 0) return r;
821
822 r = sd_bus_message_skip(m, "as");
823 if (r < 0) return r;
824
825 sd_bus_message* reply = nullptr;
826 r = sd_bus_message_new_method_return(m, &reply);
827 if (r < 0) return r;
828
829 r = sd_bus_message_open_container(reply, 'a', "(ia{sv})");
830 if (r < 0) { sd_bus_message_unref(reply); return r; }
831
832 for (auto id : ids) {
833 r = append_item_properties(reply, id,
836 if (r < 0) { sd_bus_message_unref(reply); return r; }
837 }
838
839 r = sd_bus_message_close_container(reply);
840 if (r < 0) { sd_bus_message_unref(reply); return r; }
841
842 sd_bus* bus = sd_bus_message_get_bus(m);
843 r = sd_bus_send(bus, reply, nullptr);
844 sd_bus_message_unref(reply);
845 return (r < 0) ? r : 1;
846}
847
848int TrayIcon::dbusmenu_method_get_property(sd_bus_message* m, void* userdata, sd_bus_error*) {
849 auto* self = static_cast<TrayIcon*>(userdata);
850
851 int32_t id = 0;
852 const char* prop = nullptr;
853 int r = sd_bus_message_read(m, "is", &id, &prop);
854 if (r < 0) return r;
855
856 std::string prop_str = prop ? prop : "";
857
858 sd_bus_message* reply = nullptr;
859 r = sd_bus_message_new_method_return(m, &reply);
860 if (r < 0) return r;
861
862 if (prop_str == "label") {
863 r = sd_bus_message_open_container(reply, 'v', "s");
864 if (r < 0) { sd_bus_message_unref(reply); return r; }
865 r = sd_bus_message_append(reply, "s", label_for_menu_id(id));
866 if (r < 0) { sd_bus_message_unref(reply); return r; }
867 r = sd_bus_message_close_container(reply);
868 if (r < 0) { sd_bus_message_unref(reply); return r; }
869 } else if (prop_str == "enabled") {
870 r = sd_bus_message_open_container(reply, 'v', "b");
871 if (r < 0) { sd_bus_message_unref(reply); return r; }
872 r = sd_bus_message_append(reply, "b", enabled_for_menu_id(id) ? 1 : 0);
873 if (r < 0) { sd_bus_message_unref(reply); return r; }
874 r = sd_bus_message_close_container(reply);
875 if (r < 0) { sd_bus_message_unref(reply); return r; }
876 } else {
877 r = sd_bus_message_open_container(reply, 'v', "s");
878 if (r < 0) { sd_bus_message_unref(reply); return r; }
879 r = sd_bus_message_append(reply, "s", "");
880 if (r < 0) { sd_bus_message_unref(reply); return r; }
881 r = sd_bus_message_close_container(reply);
882 if (r < 0) { sd_bus_message_unref(reply); return r; }
883 }
884
885 sd_bus* bus = sd_bus_message_get_bus(m);
886 r = sd_bus_send(bus, reply, nullptr);
887 sd_bus_message_unref(reply);
888 return (r < 0) ? r : 1;
889}
#define _(s)
static constexpr int MENU_ROOT
Definition TrayIcon.hpp:120
static constexpr int MENU_SETTINGS
Definition TrayIcon.hpp:122
void bind_ui_context()
Definition TrayIcon.cpp:297
bool have_pixmap_
Definition TrayIcon.hpp:101
void set_title(const std::string &title)
Definition TrayIcon.cpp:373
void set_icon(const std::string &icon_name)
Definition TrayIcon.cpp:363
std::vector< uint8_t > pix_rgba_
Definition TrayIcon.hpp:104
std::thread bus_thread_
Definition TrayIcon.hpp:117
bool start()
Definition TrayIcon.cpp:135
void stop()
Definition TrayIcon.cpp:282
std::string tooltip_
Definition TrayIcon.hpp:97
std::string service_name_
Definition TrayIcon.hpp:93
GMainContext * ui_ctx_
Definition TrayIcon.hpp:99
std::atomic_bool running_
Definition TrayIcon.hpp:118
std::string status_
Definition TrayIcon.hpp:98
void set_icon_file(const std::string &png_path, int target_px=22)
Definition TrayIcon.cpp:302
std::string title_
Definition TrayIcon.hpp:95
void set_tooltip(const std::string &text)
Definition TrayIcon.cpp:368
static constexpr int MENU_QUIT
Definition TrayIcon.hpp:123
static constexpr int MENU_SHOW
Definition TrayIcon.hpp:121
std::string id_
Definition TrayIcon.hpp:94
std::string icon_name_
Definition TrayIcon.hpp:96
static const sd_bus_vtable dbusmenu_vtable[]
Definition TrayIcon.cpp:75
static int append_menu_item(sd_bus_message *m, int id, const char *label, bool enabled=true)
Definition TrayIcon.cpp:606
#define TI_LOG(fmt,...)
Definition TrayIcon.cpp:38
static const sd_bus_vtable sni_vtable[]
Definition TrayIcon.cpp:50
static const char * label_for_menu_id(int id)
Definition TrayIcon.cpp:97
static gboolean invoke_std_function(gpointer data)
Definition TrayIcon.cpp:41
static int append_item_properties(sd_bus_message *m, int id, const char *label, bool enabled)
Definition TrayIcon.cpp:636
static bool enabled_for_menu_id(int)
Definition TrayIcon.cpp:106