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 <signal.h>
30#include <vector>
31#include <glibmm/main.h>
32#include <string>
33#include <gdk-pixbuf/gdk-pixbuf.h>
34#include <cstdio>
35#include <glib.h>
36#include <glib/gi18n.h>
37#include <inttypes.h>
38
39#define TI_LOG(fmt, ...) \
40std::fprintf(stderr, "[TrayIcon] " fmt "\n", ##__VA_ARGS__)
41
42static gboolean invoke_std_function(gpointer data) {
43 auto* fn = static_cast<std::function<void()>*>(data);
44 (*fn)();
45 delete fn;
46 return G_SOURCE_REMOVE;
47}
48
49
50
51static const sd_bus_vtable sni_vtable[] = {
52 SD_BUS_VTABLE_START(0),
53
54 SD_BUS_METHOD("Activate", "ii", "", TrayIcon::sni_method_activate, SD_BUS_VTABLE_UNPRIVILEGED),
55 SD_BUS_METHOD("ContextMenu", "ii", "", TrayIcon::sni_method_context_menu, SD_BUS_VTABLE_UNPRIVILEGED),
56 SD_BUS_METHOD("SecondaryActivate", "ii", "", TrayIcon::sni_method_secondary_activate, SD_BUS_VTABLE_UNPRIVILEGED),
57
58 SD_BUS_PROPERTY("Category", "s", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_CONST),
59 SD_BUS_PROPERTY("Id", "s", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_CONST),
60 SD_BUS_PROPERTY("Title", "s", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
61 SD_BUS_PROPERTY("Status", "s", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
62 SD_BUS_PROPERTY("IconName", "s", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
63 SD_BUS_PROPERTY("IconPixmap", "a(iiay)", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
64 SD_BUS_PROPERTY("ToolTip", "(sa(iiay)ss)", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
65 SD_BUS_PROPERTY("Menu", "o", TrayIcon::sni_prop_get, 0, SD_BUS_VTABLE_PROPERTY_CONST),
66
67 SD_BUS_SIGNAL("NewIcon", "", 0),
68 SD_BUS_SIGNAL("NewTitle", "", 0),
69 SD_BUS_SIGNAL("NewStatus", "", 0),
70 SD_BUS_SIGNAL("NewToolTip", "", 0),
71
72 SD_BUS_VTABLE_END
73};
74
75
76static const sd_bus_vtable dbusmenu_vtable[] = {
77 SD_BUS_VTABLE_START(0),
78
79 SD_BUS_METHOD("GetLayout", "iias", "u(ia{sv}av)", TrayIcon::dbusmenu_method_get_layout, SD_BUS_VTABLE_UNPRIVILEGED),
80 SD_BUS_METHOD("GetChildren", "i", "ai", TrayIcon::dbusmenu_method_get_children, SD_BUS_VTABLE_UNPRIVILEGED),
81 SD_BUS_METHOD("GetGroupProperties", "aias", "a(ia{sv})", TrayIcon::dbusmenu_method_get_group_properties, SD_BUS_VTABLE_UNPRIVILEGED),
82 SD_BUS_METHOD("GetProperty", "is", "v", TrayIcon::dbusmenu_method_get_property, SD_BUS_VTABLE_UNPRIVILEGED),
83 SD_BUS_METHOD("GetStatus", "", "s", TrayIcon::dbusmenu_method_get_status, SD_BUS_VTABLE_UNPRIVILEGED),
84 SD_BUS_METHOD("Event", "isvu", "", TrayIcon::dbusmenu_method_event, SD_BUS_VTABLE_UNPRIVILEGED),
85 SD_BUS_METHOD("AboutToShow", "i", "b", TrayIcon::dbusmenu_method_about_to_show, SD_BUS_VTABLE_UNPRIVILEGED),
86 // ↓↓ SPOSTATI QUI da sni_vtable ↓↓
87 SD_BUS_METHOD("EventGroup", "a(isvu)", "ai", TrayIcon::dbusmenu_method_event_group, SD_BUS_VTABLE_UNPRIVILEGED),
88 SD_BUS_METHOD("AboutToShowGroup", "ai", "aiai", TrayIcon::dbusmenu_method_about_to_show_group, SD_BUS_VTABLE_UNPRIVILEGED),
89
90 SD_BUS_PROPERTY("Version", "u", TrayIcon::dbusmenu_prop_get, 0, SD_BUS_VTABLE_PROPERTY_CONST),
91
92 SD_BUS_SIGNAL("LayoutUpdated", "ui", 0),
93 SD_BUS_SIGNAL("ItemsPropertiesUpdated", "a(ia{sv})a(ias)", 0),
94
95 SD_BUS_VTABLE_END
96};
97
98static inline const char* label_for_menu_id(int id) {
99 switch (id) {
100 case 1: return _("Mostra/Nascondi finestra");
101 case 2: return _("Rendi persistente avvio");
102 case 3: return _("Esci");
103 default: return "";
104 }
105}
106
107static inline bool enabled_for_menu_id(int) {
108 return true;
109}
110
111
112std::string TrayIcon::make_service_name() {
113 std::random_device rd;
114 std::mt19937 gen(rd());
115 std::uniform_int_distribution<int> dis(1000, 9999);
116 return "org.kde.StatusNotifierItem-" + std::to_string(getpid()) + "-" + std::to_string(dis(gen));
117}
118
120 service_name_ = make_service_name();
121 id_ = "bastionguard-sc";
122 title_ = "BastionGuard™ Secure Connection";
123 status_ = "Active";
124 // Usa il nome dell'icona installata nel tema hicolor.
125 // Il file SVG deve essere in:
126 // /usr/share/icons/hicolor/scalable/apps/bastionguard-sc.svg
127 // Il pannello (KDE Plasma, GNOME Shell, etc.) lo carica automaticamente.
128 // Fallback su "security-high" se l'icona non è nel tema.
129 icon_name_ = "bastionguard-sc";
130 tooltip_ = "BastionGuard™ Secure Connection";
131}
132
134 stop();
135 if (ui_ctx_) {
136 g_main_context_unref(ui_ctx_);
137 ui_ctx_ = nullptr;
138 }
139}
140
141bool TrayIcon::start() {
142 if (running_.exchange(true)) return true;
143
144 if (!acquire_bus()) { running_ = false; return false; }
145 if (!request_name()) { release_bus(); running_ = false; return false; }
146 if (!export_objects()) { release_bus(); running_ = false; return false; }
147
148 bus_thread_ = std::thread([this]() {
149 bool ok = register_to_watcher();
150 std::fprintf(stderr, "[TrayIcon] register_to_watcher: %s (service: %s)\n",
151 ok ? "OK" : "FAILED", service_name_.c_str());
152
153 std::this_thread::sleep_for(std::chrono::milliseconds(150));
154 emit_dbusmenu_layout_updated();
155
156 bus_loop();
157 });
158
159 return true;
160}
161
162int TrayIcon::dbusmenu_method_about_to_show_group(sd_bus_message* m, void* userdata, sd_bus_error*) {
163 [[maybe_unused]] auto* self = static_cast<TrayIcon*>(userdata);
164
165 TI_LOG("AboutToShowGroup() called");
166
167 int r = sd_bus_message_enter_container(m, 'a', "i");
168 if (r < 0) return r;
169
170 std::vector<int32_t> ids;
171 for (;;) {
172 int32_t id = 0;
173 r = sd_bus_message_read(m, "i", &id);
174 if (r < 0) return r;
175 if (r == 0) break;
176 ids.push_back(id);
177 }
178
179 r = sd_bus_message_exit_container(m);
180 if (r < 0) return r;
181
182 TI_LOG("AboutToShowGroup ids count=%zu", ids.size());
183
184 sd_bus_message* reply = nullptr;
185 r = sd_bus_message_new_method_return(m, &reply);
186 if (r < 0) return r;
187
188 r = sd_bus_message_open_container(reply, 'a', "i");
189 if (r < 0) { sd_bus_message_unref(reply); return r; }
190 r = sd_bus_message_close_container(reply);
191 if (r < 0) { sd_bus_message_unref(reply); return r; }
192
193 r = sd_bus_message_open_container(reply, 'a', "i");
194 if (r < 0) { sd_bus_message_unref(reply); return r; }
195 r = sd_bus_message_close_container(reply);
196 if (r < 0) { sd_bus_message_unref(reply); return r; }
197
198 sd_bus* bus = sd_bus_message_get_bus(m);
199 r = sd_bus_send(bus, reply, nullptr);
200 sd_bus_message_unref(reply);
201 return (r < 0) ? r : 1;
202}
203
204int TrayIcon::dbusmenu_method_event_group(sd_bus_message* m, void* userdata, sd_bus_error*) {
205 auto* self = static_cast<TrayIcon*>(userdata);
206
207 TI_LOG("EventGroup() called");
208
209 int r = sd_bus_message_enter_container(m, 'a', "(isvu)");
210 if (r < 0) return r;
211
212 struct Ev { int32_t id; std::string ev; uint32_t ts; };
213 std::vector<Ev> events;
214
215 for (;;) {
216 r = sd_bus_message_enter_container(m, 'r', "isvu");
217 if (r < 0) return r;
218 if (r == 0) break;
219
220 int32_t id = 0;
221 const char* ev_c = nullptr;
222 uint32_t ts = 0;
223
224 r = sd_bus_message_read(m, "is", &id, &ev_c);
225 if (r < 0) return r;
226
227 r = sd_bus_message_skip(m, "v");
228 if (r < 0) return r;
229
230 r = sd_bus_message_read(m, "u", &ts);
231 if (r < 0) return r;
232
233 r = sd_bus_message_exit_container(m);
234 if (r < 0) return r;
235
236 std::string ev = ev_c ? ev_c : "";
237 TI_LOG("EventGroup item: id=%d ev='%s' ts=%" PRIu32, (int)id, ev.c_str(), ts);
238 events.push_back({id, ev, ts});
239 }
240
241 r = sd_bus_message_exit_container(m);
242 if (r < 0) return r;
243
244 if (!self->ui_ctx_) {
245 TI_LOG("WARNING: ui_ctx_ null (bind_ui_context not called?)");
246 } else {
247 for (const auto& e : events) {
248 const int32_t id = e.id;
249 if (id == MENU_QUIT) {
250 // Termina il processo immediatamente con SIGTERM.
251 // Qualunque approccio basato su callback→stop()→join() crea
252 // deadlock perché il bus_thread non può fare join su se stesso.
253 // SIGTERM è il modo più pulito: GTK riceve il segnale,
254 // chiama i distruttori e termina ordinatamente.
255 TI_LOG("-> MENU_QUIT: SIGTERM");
256 ::kill(::getpid(), SIGTERM);
257 } else {
258 g_main_context_invoke(self->ui_ctx_, invoke_std_function,
259 new std::function<void()>([self, id]() {
260 TI_LOG("Dispatch on UI: id=%d", (int)id);
261 if (id == MENU_SHOW) {
262 TI_LOG("-> MENU_SHOW");
263 if (self->on_activate_cb_) self->on_activate_cb_();
264 else TI_LOG("on_activate_cb_ is null");
265 } else if (id == MENU_PERSIST_STARTUP) {
266 TI_LOG("-> MENU_PERSIST_STARTUP");
267 if (self->on_make_startup_persistent_cb_) self->on_make_startup_persistent_cb_();
268 else TI_LOG("on_make_startup_persistent_cb_ is null");
269 } else {
270 TI_LOG("Unknown menu id=%d", (int)id);
271 }
272 })
273 );
274 }
275 }
276 }
277
278 sd_bus_message* reply = nullptr;
279 r = sd_bus_message_new_method_return(m, &reply);
280 if (r < 0) return r;
281
282 r = sd_bus_message_open_container(reply, 'a', "i");
283 if (r < 0) { sd_bus_message_unref(reply); return r; }
284 r = sd_bus_message_close_container(reply);
285 if (r < 0) { sd_bus_message_unref(reply); return r; }
286
287 sd_bus* bus = sd_bus_message_get_bus(m);
288 r = sd_bus_send(bus, reply, nullptr);
289 sd_bus_message_unref(reply);
290 return (r < 0) ? r : 1;
291}
292
293void TrayIcon::stop() {
294 if (!running_.exchange(false)) return;
295
296 // Non chiamare stop() dall'interno del bus_thread_ stesso
297 if (bus_thread_.joinable() &&
298 std::this_thread::get_id() == bus_thread_.get_id()) {
299 return;
300 }
301
302 // Sveglia subito sd_bus_wait() scrivendo sul pipe di wakeup del bus,
303 // altrimenti join() aspetterebbe fino a 100 ms per ogni iterazione.
304 if (bus_) {
305 // sd_bus_get_fd restituisce il fd principale; scrivere su di esso
306 // non è corretto — usiamo invece sd_bus_flush per drenare la coda
307 // e poi chiudiamo il bus: questo fa uscire sd_bus_wait con errore.
308 sd_bus_flush(bus_);
309 sd_bus_close(bus_);
310 }
311
312 if (bus_thread_.joinable())
313 bus_thread_.join();
314
315 // bus_ è già chiuso, non serve release_bus() che chiamerebbe
316 // sd_bus_unref — lo facciamo comunque per azzerare il puntatore.
317 unexport_objects();
318 release_bus();
319}
320
322 if (ui_ctx_) return;
323 ui_ctx_ = g_main_context_ref(g_main_context_default());
324}
325
326void TrayIcon::set_icon_file(const std::string& png_path, int target_px) {
327 GError* err = nullptr;
328 GdkPixbuf* pb = gdk_pixbuf_new_from_file(png_path.c_str(), &err);
329 if (!pb) {
330 std::fprintf(stderr, "[TrayIcon] cannot load icon '%s': %s\n",
331 png_path.c_str(), err ? err->message : "unknown error");
332 if (err) g_error_free(err);
333 have_pixmap_ = false;
334 emit_sni_signal("NewIcon");
335 return;
336 }
337
338 if (target_px > 0) {
339 int w = gdk_pixbuf_get_width(pb);
340 int h = gdk_pixbuf_get_height(pb);
341 if (w != target_px || h != target_px) {
342 GdkPixbuf* scaled = gdk_pixbuf_scale_simple(pb, target_px, target_px, GDK_INTERP_BILINEAR);
343 g_object_unref(pb);
344 pb = scaled;
345 }
346 }
347
348 GdkPixbuf* rgba = gdk_pixbuf_add_alpha(pb, FALSE, 0, 0, 0);
349 g_object_unref(pb);
350
351 pix_w_ = gdk_pixbuf_get_width(rgba);
352 pix_h_ = gdk_pixbuf_get_height(rgba);
353
354 const int rowstride = gdk_pixbuf_get_rowstride(rgba);
355 const int nchan = gdk_pixbuf_get_n_channels(rgba);
356 const guchar* pixels = gdk_pixbuf_get_pixels(rgba);
357
358 pix_rgba_.resize((size_t)pix_w_ * (size_t)pix_h_ * 4u);
359
360 for (int y = 0; y < pix_h_; ++y) {
361 const guchar* src = pixels + y * rowstride;
362 uint8_t* dst = pix_rgba_.data() + (size_t)y * (size_t)pix_w_ * 4u;
363
364 for (int x = 0; x < pix_w_; ++x) {
365 uint8_t r = src[x * nchan + 0];
366 uint8_t g = src[x * nchan + 1];
367 uint8_t b = src[x * nchan + 2];
368 uint8_t a = (nchan >= 4) ? src[x * nchan + 3] : 255u;
369
370 uint32_t argb = ((uint32_t)a << 24) |
371 ((uint32_t)r << 16) |
372 ((uint32_t)g << 8) |
373 (uint32_t)b;
374 argb = htobe32(argb);
375 std::memcpy(dst + x * 4, &argb, 4);
376 }
377 }
378
379 g_object_unref(rgba);
380
381 have_pixmap_ = true;
382 icon_name_.clear();
383
384 emit_sni_signal("NewIcon");
385}
386
387void TrayIcon::set_icon(const std::string& icon_name) {
388 icon_name_ = icon_name.empty() ? "bastionguard-sc" : icon_name;
389 have_pixmap_ = false; // forza IconName invece di IconPixmap
390 emit_sni_signal("NewIcon");
391}
392
393void TrayIcon::set_tooltip(const std::string& text) {
394 tooltip_ = text;
395 emit_sni_signal("NewToolTip");
396}
397
398void TrayIcon::set_title(const std::string& title) {
399 title_ = title;
400 emit_sni_signal("NewTitle");
401}
402
403bool TrayIcon::acquire_bus() {
404 if (sd_bus_open_user(&bus_) < 0) {
405 bus_ = nullptr;
406 return false;
407 }
408 return true;
409}
410
411bool TrayIcon::request_name() {
412 int r = sd_bus_request_name(bus_, service_name_.c_str(), 0);
413 return r >= 0;
414}
415
416void TrayIcon::release_bus() {
417 if (!bus_) return;
418 sd_bus_unref(bus_);
419 bus_ = nullptr;
420}
421
422bool TrayIcon::export_objects() {
423 int r1 = sd_bus_add_object_vtable(
424 bus_, &sni_slot_,
425 "/StatusNotifierItem",
426 "org.kde.StatusNotifierItem",
428 this
429 );
430 if (r1 < 0) return false;
431
432 int r2 = sd_bus_add_object_vtable(
433 bus_, &menu_slot_,
434 "/Menu",
435 "com.canonical.dbusmenu",
437 this
438 );
439 if (r2 < 0) return false;
440
441 return true;
442}
443
444void TrayIcon::unexport_objects() {
445 if (sni_slot_) { sd_bus_slot_unref(sni_slot_); sni_slot_ = nullptr; }
446 if (menu_slot_) { sd_bus_slot_unref(menu_slot_); menu_slot_ = nullptr; }
447}
448
449bool TrayIcon::register_to_watcher() {
450 sd_bus_message* m = nullptr;
451 sd_bus_error er = SD_BUS_ERROR_NULL;
452
453 int r = sd_bus_message_new_method_call(
454 bus_, &m,
455 "org.kde.StatusNotifierWatcher",
456 "/StatusNotifierWatcher",
457 "org.kde.StatusNotifierWatcher",
458 "RegisterStatusNotifierItem"
459 );
460 if (r < 0) return false;
461
462 r = sd_bus_message_append(m, "s", service_name_.c_str());
463 if (r < 0) { sd_bus_message_unref(m); return false; }
464
465 r = sd_bus_call(bus_, m, 0, &er, nullptr);
466 if (r < 0) {
467 std::fprintf(stderr, "[TrayIcon] RegisterStatusNotifierItem error: %s\n",
468 er.message ? er.message : "(unknown)");
469 }
470
471 sd_bus_error_free(&er);
472 sd_bus_message_unref(m);
473 return r >= 0;
474}
475
476void TrayIcon::bus_loop() {
477 while (running_) {
478 for (;;) {
479 int r = sd_bus_process(bus_, nullptr);
480 if (r < 0) {
481 // Bus chiuso (da stop()) oppure errore reale: esci
482 running_ = false;
483 break;
484 }
485 if (r == 0) break;
486 }
487
488 if (!running_) break;
489
490 // Timeout breve: 50 ms invece di 100 ms.
491 // Se running_ diventa false durante l'attesa, usciamo al prossimo giro.
492 int w = sd_bus_wait(bus_, 50000);
493 if (w < 0) {
494 // Bus chiuso da stop() → sd_bus_wait ritorna EBADF o simile
495 running_ = false;
496 break;
497 }
498 }
499}
500
501void TrayIcon::emit_sni_signal(const char* signal_name) {
502 if (!bus_) return;
503 (void)sd_bus_emit_signal(
504 bus_,
505 "/StatusNotifierItem",
506 "org.kde.StatusNotifierItem",
507 signal_name,
508 ""
509 );
510}
511
512void TrayIcon::emit_dbusmenu_layout_updated() {
513 if (!bus_) return;
514 const uint32_t revision = 1u;
515 const int32_t parent = MENU_ROOT;
516 (void)sd_bus_emit_signal(
517 bus_,
518 "/Menu",
519 "com.canonical.dbusmenu",
520 "LayoutUpdated",
521 "ui",
522 revision,
523 parent
524 );
525}
526
527int TrayIcon::sni_method_activate(sd_bus_message* m, void* userdata, sd_bus_error*) {
528 auto* self = static_cast<TrayIcon*>(userdata);
529
530 std::fprintf(stderr, "[TrayIcon] SNI Activate received\n");
531
532 if (!self->ui_ctx_) {
533 std::fprintf(stderr, "[TrayIcon] WARNING: ui_ctx_ null in Activate -> fallback default\n");
534 self->ui_ctx_ = g_main_context_ref(g_main_context_default());
535 }
536
537 if (self->on_activate_cb_) {
538 g_main_context_invoke(self->ui_ctx_, invoke_std_function,
539 new std::function<void()>([self]() {
540 std::fprintf(stderr, "[TrayIcon] SNI Activate dispatch on UI thread\n");
541 self->on_activate_cb_();
542 })
543 );
544 }
545
546 return sd_bus_reply_method_return(m, "");
547}
548
549int TrayIcon::sni_method_context_menu(sd_bus_message* m, void*, sd_bus_error*) {
550 return sd_bus_reply_method_return(m, "");
551}
552
553int TrayIcon::sni_method_secondary_activate(sd_bus_message* m, void*, sd_bus_error*) {
554 return sd_bus_reply_method_return(m, "");
555}
556
557int TrayIcon::sni_prop_get(sd_bus*, const char*, const char*,
558 const char* property, sd_bus_message* reply,
559 void* userdata, sd_bus_error* ret_error) {
560 auto* self = static_cast<TrayIcon*>(userdata);
561
562 if (std::strcmp(property, "Category") == 0)
563 return sd_bus_message_append(reply, "s", "ApplicationStatus");
564
565 if (std::strcmp(property, "Id") == 0)
566 return sd_bus_message_append(reply, "s", self->id_.c_str());
567
568 if (std::strcmp(property, "Title") == 0)
569 return sd_bus_message_append(reply, "s", self->title_.c_str());
570
571 if (std::strcmp(property, "Status") == 0)
572 return sd_bus_message_append(reply, "s", self->status_.c_str());
573
574 if (std::strcmp(property, "IconName") == 0) {
575 if (self->have_pixmap_)
576 return sd_bus_message_append(reply, "s", "");
577 return sd_bus_message_append(reply, "s", self->icon_name_.c_str());
578 }
579
580 if (std::strcmp(property, "IconPixmap") == 0) {
581 int r = sd_bus_message_open_container(reply, 'a', "(iiay)");
582 if (r < 0) return r;
583
584 if (self->have_pixmap_ && self->pix_w_ > 0 && self->pix_h_ > 0
585 && !self->pix_rgba_.empty())
586 {
587 r = sd_bus_message_open_container(reply, 'r', "iiay");
588 if (r < 0) return r;
589
590 r = sd_bus_message_append(reply, "ii", self->pix_w_, self->pix_h_);
591 if (r < 0) return r;
592
593 r = sd_bus_message_append_array(reply, 'y',
594 self->pix_rgba_.data(),
595 self->pix_rgba_.size());
596 if (r < 0) return r;
597
598 r = sd_bus_message_close_container(reply);
599 if (r < 0) return r;
600 }
601
602 return sd_bus_message_close_container(reply);
603 }
604
605 if (std::strcmp(property, "Menu") == 0)
606 return sd_bus_message_append(reply, "o", "/Menu");
607
608 if (std::strcmp(property, "ToolTip") == 0) {
609 int r = sd_bus_message_open_container(reply, 'r', "sa(iiay)ss");
610 if (r < 0) return r;
611
612 r = sd_bus_message_append(reply, "s", self->icon_name_.c_str());
613 if (r < 0) return r;
614
615 r = sd_bus_message_open_container(reply, 'a', "(iiay)");
616 if (r < 0) return r;
617 r = sd_bus_message_close_container(reply);
618 if (r < 0) return r;
619
620 r = sd_bus_message_append(reply, "s", self->title_.c_str());
621 if (r < 0) return r;
622 r = sd_bus_message_append(reply, "s", self->tooltip_.c_str());
623 if (r < 0) return r;
624
625 r = sd_bus_message_close_container(reply);
626 if (r < 0) return r;
627 return 0;
628 }
629
630 return sd_bus_error_setf(ret_error, SD_BUS_ERROR_UNKNOWN_PROPERTY,
631 "Unknown property '%s'", property);
632 }
633
634 int TrayIcon::dbusmenu_prop_get(sd_bus*, const char*, const char*,
635 const char* /*property*/, sd_bus_message* reply,
636 void*, sd_bus_error*) {
637 return sd_bus_message_append(reply, "u", 3u);
638 }
639
640 static int append_menu_item(sd_bus_message* m, int id,
641 const char* label, bool enabled = true) {
642 int r = 0;
643 r = sd_bus_message_open_container(m, 'r', "ia{sv}av"); if (r < 0) return r;
644 r = sd_bus_message_append(m, "i", id); if (r < 0) return r;
645
646 r = sd_bus_message_open_container(m, 'a', "{sv}"); if (r < 0) return r;
647
648 r = sd_bus_message_open_container(m, 'e', "sv"); if (r < 0) return r;
649 r = sd_bus_message_append(m, "s", "label"); if (r < 0) return r;
650 r = sd_bus_message_open_container(m, 'v', "s"); if (r < 0) return r;
651 r = sd_bus_message_append(m, "s", label); if (r < 0) return r;
652 r = sd_bus_message_close_container(m); if (r < 0) return r;
653 r = sd_bus_message_close_container(m); if (r < 0) return r;
654
655 r = sd_bus_message_open_container(m, 'e', "sv"); if (r < 0) return r;
656 r = sd_bus_message_append(m, "s", "enabled"); if (r < 0) return r;
657 r = sd_bus_message_open_container(m, 'v', "b"); if (r < 0) return r;
658 r = sd_bus_message_append(m, "b", enabled ? 1 : 0); if (r < 0) return r;
659 r = sd_bus_message_close_container(m); if (r < 0) return r;
660 r = sd_bus_message_close_container(m); if (r < 0) return r;
661
662 r = sd_bus_message_close_container(m); if (r < 0) return r;
663
664 r = sd_bus_message_open_container(m, 'a', "v"); if (r < 0) return r;
665 r = sd_bus_message_close_container(m); if (r < 0) return r;
666
667 return sd_bus_message_close_container(m);
668 }
669
670 static int append_item_properties(sd_bus_message* m, int id,
671 const char* label, bool enabled) {
672 int r = 0;
673 r = sd_bus_message_open_container(m, 'r', "ia{sv}"); if (r < 0) return r;
674 r = sd_bus_message_append(m, "i", id); if (r < 0) return r;
675
676 r = sd_bus_message_open_container(m, 'a', "{sv}"); if (r < 0) return r;
677
678 r = sd_bus_message_open_container(m, 'e', "sv"); if (r < 0) return r;
679 r = sd_bus_message_append(m, "s", "label"); if (r < 0) return r;
680 r = sd_bus_message_open_container(m, 'v', "s"); if (r < 0) return r;
681 r = sd_bus_message_append(m, "s", label); if (r < 0) return r;
682 r = sd_bus_message_close_container(m); if (r < 0) return r;
683 r = sd_bus_message_close_container(m); if (r < 0) return r;
684
685 r = sd_bus_message_open_container(m, 'e', "sv"); if (r < 0) return r;
686 r = sd_bus_message_append(m, "s", "enabled"); if (r < 0) return r;
687 r = sd_bus_message_open_container(m, 'v', "b"); if (r < 0) return r;
688 r = sd_bus_message_append(m, "b", enabled ? 1 : 0); if (r < 0) return r;
689 r = sd_bus_message_close_container(m); if (r < 0) return r;
690 r = sd_bus_message_close_container(m); if (r < 0) return r;
691
692 r = sd_bus_message_close_container(m); if (r < 0) return r;
693 return sd_bus_message_close_container(m);
694 }
695
696 int TrayIcon::dbusmenu_method_get_status(sd_bus_message* m, void*, sd_bus_error*) {
697 return sd_bus_reply_method_return(m, "s", "normal");
698 }
699
700 int TrayIcon::dbusmenu_method_get_children(sd_bus_message* m, void* userdata, sd_bus_error*) {
701 [[maybe_unused]] auto* self = static_cast<TrayIcon*>(userdata);
702
703 int32_t parent = 0;
704 int r = sd_bus_message_read(m, "i", &parent);
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_open_container(reply, 'a', "i");
712 if (r < 0) { sd_bus_message_unref(reply); return r; }
713
714 if (parent == MENU_ROOT) {
715 int32_t ids[] = { MENU_SHOW, MENU_PERSIST_STARTUP, MENU_QUIT };
716 for (int32_t id : ids) {
717 r = sd_bus_message_append(reply, "i", id);
718 if (r < 0) { sd_bus_message_unref(reply); return r; }
719 }
720 }
721
722 r = sd_bus_message_close_container(reply);
723 if (r < 0) { sd_bus_message_unref(reply); return r; }
724
725 sd_bus* bus = sd_bus_message_get_bus(m);
726 r = sd_bus_send(bus, reply, nullptr);
727 sd_bus_message_unref(reply);
728 return (r < 0) ? r : 1;
729 }
730
731 int TrayIcon::dbusmenu_method_get_layout(sd_bus_message* m, void* userdata, sd_bus_error*) {
732 [[maybe_unused]] auto* self = static_cast<TrayIcon*>(userdata);
733
734 int32_t parent_id = 0, depth = 0;
735 int r = sd_bus_message_read(m, "ii", &parent_id, &depth);
736 if (r < 0) return r;
737
738 r = sd_bus_message_skip(m, "as");
739 if (r < 0) return r;
740
741 sd_bus_message* reply = nullptr;
742 r = sd_bus_message_new_method_return(m, &reply);
743 if (r < 0) return r;
744
745 r = sd_bus_message_append(reply, "u", (uint32_t)1);
746 if (r < 0) { sd_bus_message_unref(reply); return r; }
747
748 r = sd_bus_message_open_container(reply, 'r', "ia{sv}av");
749 if (r < 0) { sd_bus_message_unref(reply); return r; }
750
751 r = sd_bus_message_append(reply, "i", MENU_ROOT);
752 if (r < 0) { sd_bus_message_unref(reply); return r; }
753
754 r = sd_bus_message_open_container(reply, 'a', "{sv}");
755 if (r < 0) { sd_bus_message_unref(reply); return r; }
756 r = sd_bus_message_close_container(reply);
757 if (r < 0) { sd_bus_message_unref(reply); return r; }
758
759 r = sd_bus_message_open_container(reply, 'a', "v");
760 if (r < 0) { sd_bus_message_unref(reply); return r; }
761
762 bool include_children = (depth != 0);
763
764 if (include_children) {
765 auto push_child = [&](int id) -> int {
766 int rr = sd_bus_message_open_container(reply, 'v', "(ia{sv}av)");
767 if (rr < 0) return rr;
768 rr = append_menu_item(reply, id,
771 if (rr < 0) return rr;
772 return sd_bus_message_close_container(reply);
773 };
774 r = push_child(MENU_SHOW); if (r < 0) { sd_bus_message_unref(reply); return r; }
775 r = push_child(MENU_PERSIST_STARTUP); if (r < 0) { sd_bus_message_unref(reply); return r; }
776 r = push_child(MENU_QUIT); if (r < 0) { sd_bus_message_unref(reply); return r; }
777 }
778
779 r = sd_bus_message_close_container(reply);
780 if (r < 0) { sd_bus_message_unref(reply); return r; }
781
782 r = sd_bus_message_close_container(reply);
783 if (r < 0) { sd_bus_message_unref(reply); return r; }
784
785 sd_bus* bus = sd_bus_message_get_bus(m);
786 r = sd_bus_send(bus, reply, nullptr);
787 sd_bus_message_unref(reply);
788 return (r < 0) ? r : 1;
789 }
790
791 int TrayIcon::dbusmenu_method_event(sd_bus_message* m, void* userdata, sd_bus_error*) {
792 auto* self = static_cast<TrayIcon*>(userdata);
793
794 int32_t id = 0;
795 const char* event_id_c = nullptr;
796
797 int r = sd_bus_message_read(m, "is", &id, &event_id_c);
798 if (r < 0)
799 return sd_bus_reply_method_return(m, "");
800
801 std::string ev = event_id_c ? event_id_c : "";
802
803 (void)sd_bus_message_skip(m, "vu");
804
805 std::fprintf(stderr, "[TrayIcon] Event: id=%d ev='%s'\n", (int)id, ev.c_str());
806
807 if (!self->ui_ctx_) {
808 std::fprintf(stderr, "[TrayIcon] WARNING: ui_ctx_ null (bind_ui_context?)\n");
809 return sd_bus_reply_method_return(m, "");
810 }
811
812 if (id == MENU_QUIT) {
813 std::fprintf(stderr, "[TrayIcon] -> MENU_QUIT: SIGTERM\n");
814 ::kill(::getpid(), SIGTERM);
815 } else {
816 g_main_context_invoke(self->ui_ctx_, invoke_std_function,
817 new std::function<void()>([self, id]() {
818 std::fprintf(stderr, "[TrayIcon] Dispatch on UI: id=%d\n", (int)id);
819 if (id == MENU_SHOW) {
820 if (self->on_activate_cb_) self->on_activate_cb_();
821 } else if (id == MENU_PERSIST_STARTUP) {
822 if (self->on_make_startup_persistent_cb_) self->on_make_startup_persistent_cb_();
823 } else if (id != MENU_ROOT) {
824 std::fprintf(stderr, "[TrayIcon] Unknown menu id=%d\n", (int)id);
825 }
826 })
827 );
828 }
829
830 return sd_bus_reply_method_return(m, "");
831 }
832
833 int TrayIcon::dbusmenu_method_about_to_show(sd_bus_message* m, void*, sd_bus_error*) {
834 int32_t id = 0;
835 int r = sd_bus_message_read(m, "i", &id);
836 if (r < 0) return r;
837 return sd_bus_reply_method_return(m, "b", 0);
838 }
839
840 int TrayIcon::dbusmenu_method_get_group_properties(sd_bus_message* m, void* userdata, sd_bus_error*) {
841 [[maybe_unused]] auto* self = static_cast<TrayIcon*>(userdata);
842
843 int r = sd_bus_message_enter_container(m, 'a', "i");
844 if (r < 0) return r;
845
846 std::vector<int32_t> ids;
847 for (;;) {
848 int32_t id = 0;
849 r = sd_bus_message_read(m, "i", &id);
850 if (r < 0) return r;
851 if (r == 0) break;
852 ids.push_back(id);
853 }
854
855 r = sd_bus_message_exit_container(m);
856 if (r < 0) return r;
857
858 r = sd_bus_message_skip(m, "as");
859 if (r < 0) return r;
860
861 sd_bus_message* reply = nullptr;
862 r = sd_bus_message_new_method_return(m, &reply);
863 if (r < 0) return r;
864
865 r = sd_bus_message_open_container(reply, 'a', "(ia{sv})");
866 if (r < 0) { sd_bus_message_unref(reply); return r; }
867
868 for (auto id : ids) {
869 r = append_item_properties(reply, id,
872 if (r < 0) { sd_bus_message_unref(reply); return r; }
873 }
874
875 r = sd_bus_message_close_container(reply);
876 if (r < 0) { sd_bus_message_unref(reply); return r; }
877
878 sd_bus* bus = sd_bus_message_get_bus(m);
879 r = sd_bus_send(bus, reply, nullptr);
880 sd_bus_message_unref(reply);
881 return (r < 0) ? r : 1;
882 }
883
884 int TrayIcon::dbusmenu_method_get_property(sd_bus_message* m, void* userdata, sd_bus_error*) {
885 [[maybe_unused]] auto* self = static_cast<TrayIcon*>(userdata);
886
887 int32_t id = 0;
888 const char* prop = nullptr;
889 int r = sd_bus_message_read(m, "is", &id, &prop);
890 if (r < 0) return r;
891
892 std::string prop_str = prop ? prop : "";
893
894 sd_bus_message* reply = nullptr;
895 r = sd_bus_message_new_method_return(m, &reply);
896 if (r < 0) return r;
897
898 if (prop_str == "label") {
899 r = sd_bus_message_open_container(reply, 'v', "s");
900 if (r < 0) { sd_bus_message_unref(reply); return r; }
901 r = sd_bus_message_append(reply, "s", label_for_menu_id(id));
902 if (r < 0) { sd_bus_message_unref(reply); return r; }
903 r = sd_bus_message_close_container(reply);
904 if (r < 0) { sd_bus_message_unref(reply); return r; }
905 } else if (prop_str == "enabled") {
906 r = sd_bus_message_open_container(reply, 'v', "b");
907 if (r < 0) { sd_bus_message_unref(reply); return r; }
908 r = sd_bus_message_append(reply, "b", enabled_for_menu_id(id) ? 1 : 0);
909 if (r < 0) { sd_bus_message_unref(reply); return r; }
910 r = sd_bus_message_close_container(reply);
911 if (r < 0) { sd_bus_message_unref(reply); return r; }
912 } else {
913 r = sd_bus_message_open_container(reply, 'v', "s");
914 if (r < 0) { sd_bus_message_unref(reply); return r; }
915 r = sd_bus_message_append(reply, "s", "");
916 if (r < 0) { sd_bus_message_unref(reply); return r; }
917 r = sd_bus_message_close_container(reply);
918 if (r < 0) { sd_bus_message_unref(reply); return r; }
919 }
920
921 sd_bus* bus = sd_bus_message_get_bus(m);
922 r = sd_bus_send(bus, reply, nullptr);
923 sd_bus_message_unref(reply);
924 return (r < 0) ? r : 1;
925 }
#define _(s)
static constexpr int MENU_ROOT
Definition TrayIcon.hpp:120
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
static constexpr int MENU_PERSIST_STARTUP
Definition TrayIcon.hpp:122
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