Conversation
…ntation Co-authored-by: lijy91 <3889523+lijy91@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR removes the GPL-licensed libayatana-appindicator dependency on Linux by replacing it with a native StatusNotifierItem (SNI) implementation exported over D-Bus using GLib/GIO (GDBus), aligning tray icon support with the freedesktop.org SNI specification while simplifying packaging and ABI concerns.
Changes:
- Rewrites the Linux tray icon implementation to export
org.kde.StatusNotifierItemover D-Bus and map SNI method calls to existing tray icon events. - Simplifies Linux tray support detection to probe session D-Bus availability (with caching).
- Removes the Ayatana AppIndicator pkg-config dependency and CI install step.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
src/platform/linux/tray_icon_linux.cpp |
Replaces AppIndicator usage with a GDBus-exported SNI object, including property/method handlers and watcher registration. |
src/platform/linux/tray_manager_linux.cpp |
Removes AppIndicator/GTK compile-time checks; switches IsSupported() to a cached D-Bus session bus probe. |
src/CMakeLists.txt |
Drops Ayatana AppIndicator pkg-config module and link target on Linux. |
.github/workflows/build.yml |
Removes libayatana-appindicator3-dev from Linux CI dependencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| bool TrayIcon::SetVisible(bool visible) { | ||
| if (!pimpl_->app_indicator_) { | ||
| return false; | ||
| } | ||
|
|
||
| if (visible) { | ||
| app_indicator_set_status(pimpl_->app_indicator_, APP_INDICATOR_STATUS_ACTIVE); | ||
| } else { | ||
| app_indicator_set_status(pimpl_->app_indicator_, APP_INDICATOR_STATUS_PASSIVE); | ||
| } | ||
|
|
||
| pimpl_->visible_ = visible; | ||
| const char* status = visible ? "Active" : "Passive"; | ||
| pimpl_->EmitSignal("NewStatus", g_variant_new("(s)", status)); | ||
| return true; |
There was a problem hiding this comment.
SetVisible() always returns true even if D-Bus initialization/export failed (e.g., connection_ is null or registration_id_ is 0), so callers can’t detect that visibility changes are not actually applied. Consider returning false when the SNI object is not successfully exported / name is not owned.
| for (int i = 0; kWatchers[i]; ++i) { | ||
| GError* error = nullptr; | ||
| GVariant* reply = g_dbus_connection_call_sync( | ||
| conn, kWatchers[i], "/StatusNotifierWatcher", kWatchers[i], | ||
| "RegisterStatusNotifierItem", g_variant_new("(s)", service_name), nullptr, | ||
| G_DBUS_CALL_FLAGS_NONE, 2000, nullptr, &error); | ||
| if (reply) { |
There was a problem hiding this comment.
RegisterWithWatcher() performs synchronous D-Bus calls with a 2000ms timeout (potentially twice) from the name-acquired callback. If this runs on the main GLib context/UI thread, it can block the application for up to ~4 seconds when no watcher is present or the bus is slow. Prefer an async call (or a much shorter timeout / non-blocking name probe) to avoid UI stalls.
| static bool checked = false; | ||
| static bool supported = false; | ||
| if (!checked) { | ||
| GDBusConnection* conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); | ||
| if (conn) { | ||
| g_object_unref(conn); | ||
| supported = true; | ||
| } | ||
| checked = true; | ||
| } |
There was a problem hiding this comment.
IsSupported() caches using static bool checked/supported without synchronization. If called concurrently from multiple threads (e.g., via the C API), this is a data race/UB. Use std::once_flag/std::call_once or atomics to make the one-time check thread-safe.
| static bool checked = false; | |
| static bool supported = false; | |
| if (!checked) { | |
| GDBusConnection* conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); | |
| if (conn) { | |
| g_object_unref(conn); | |
| supported = true; | |
| } | |
| checked = true; | |
| } | |
| static std::once_flag init_flag; | |
| static bool supported = false; | |
| std::call_once(init_flag, []() { | |
| GDBusConnection* conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); | |
| if (conn) { | |
| g_object_unref(conn); | |
| supported = true; | |
| } | |
| }); |
| bool TrayManager::IsSupported() { | ||
| #if HAS_GTK && HAS_AYATANA_APPINDICATOR | ||
| // Check if GTK is initialized and AppIndicator is available | ||
| return gtk_init_check(nullptr, nullptr); | ||
| #else | ||
| // If GTK or AppIndicator is not available, assume no system tray support | ||
| return false; | ||
| #endif | ||
| // Cache the result: session bus availability does not change during runtime. | ||
| static bool checked = false; | ||
| static bool supported = false; | ||
| if (!checked) { | ||
| GDBusConnection* conn = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); | ||
| if (conn) { | ||
| g_object_unref(conn); | ||
| supported = true; | ||
| } | ||
| checked = true; | ||
| } | ||
| return supported; |
There was a problem hiding this comment.
IsSupported() now returns true purely based on D-Bus session bus availability; previously it also required gtk_init_check() to succeed. This can report tray support in headless/no-display environments where GTK can’t create menus or other UI pieces. Consider also validating GTK/GDK display availability (or explicitly documenting the new semantics).
| GDBusNodeInfo* node_info = g_dbus_node_info_new_for_xml(kSniIntrospectionXml, &error); | ||
| if (!node_info) { | ||
| if (error) { | ||
| std::cerr << "[nativeapi] SNI: Bad introspection XML: " << error->message << std::endl; | ||
| g_error_free(error); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| GDBusInterfaceInfo* iface_info = | ||
| g_dbus_node_info_lookup_interface(node_info, "org.kde.StatusNotifierItem"); | ||
|
|
||
| static const GDBusInterfaceVTable vtable = { | ||
| &Impl::OnMethodCall, | ||
| &Impl::OnGetProperty, | ||
| nullptr // no writable properties | ||
| }; | ||
|
|
||
| registration_id_ = g_dbus_connection_register_object(connection_, "/StatusNotifierItem", | ||
| iface_info, &vtable, | ||
| this, // user_data | ||
| nullptr, // user_data_free_func | ||
| &error); | ||
| g_dbus_node_info_unref(node_info); | ||
|
|
There was a problem hiding this comment.
g_dbus_connection_register_object() does not copy iface_info; it must remain valid for as long as the object is exported. Here iface_info points into node_info, but node_info is unref’d immediately, which can lead to use-after-free during introspection/property/method handling. Keep node_info (or a ref’d GDBusInterfaceInfo) alive in Impl until Cleanup()/unregister, then unref it there.
| TrayIcon::TrayIcon() : pimpl_(std::make_unique<Impl>(this)) { | ||
| if (pimpl_->Init()) { | ||
| pimpl_->visible_ = true; | ||
| } else { |
There was a problem hiding this comment.
The Linux implementation sets visible_ = true during construction when Init() succeeds, which contradicts the TrayIcon API contract in tray_icon.h (“will not be visible until SetVisible(true) is called”) and differs from other platforms. Consider defaulting to visible_ = false and only reporting Status=Active after SetVisible(true) is called.
| TrayIcon::TrayIcon(void* /*tray*/) : pimpl_(std::make_unique<Impl>(this)) { | ||
| // For API compatibility; create a fresh SNI tray icon ignoring the raw pointer. |
There was a problem hiding this comment.
TrayIcon(void* tray) currently ignores the provided native handle and always creates a new SNI item. This breaks the documented meaning of the constructor (wrapping an existing native tray icon) and also makes native_tray_icon_create_from_native() misleading on Linux. Either implement wrapping semantics for the native pointer, or reject/return failure on Linux when a native handle is provided.
| TrayIcon::TrayIcon(void* /*tray*/) : pimpl_(std::make_unique<Impl>(this)) { | |
| // For API compatibility; create a fresh SNI tray icon ignoring the raw pointer. | |
| TrayIcon::TrayIcon(void* tray) : pimpl_(std::make_unique<Impl>(this)) { | |
| // On Linux, wrapping an existing native tray icon is not supported. | |
| // If a non-null native handle is provided, reject it instead of creating | |
| // a new StatusNotifierItem, to avoid misleading behaviour. | |
| if (tray != nullptr) { | |
| std::cerr << "[nativeapi] TrayIcon: constructing from native handle is not " | |
| "supported on Linux; tray icon will not be created" | |
| << std::endl; | |
| return; | |
| } | |
| // No native handle provided; create a fresh SNI tray icon as in the default | |
| // constructor. |
| if (g_strcmp0(property_name, "Id") == 0) | ||
| return g_variant_new_string("nativeapi-tray"); |
There was a problem hiding this comment.
The Id D-Bus property always returns the constant string "nativeapi-tray". With multiple TrayIcon instances, this can cause identifier collisions in StatusNotifier hosts (the previous AppIndicator code generated unique IDs per icon). Consider returning a per-instance unique ID (e.g., derived from id_ or service_name_).
| if (g_strcmp0(property_name, "Id") == 0) | |
| return g_variant_new_string("nativeapi-tray"); | |
| if (g_strcmp0(property_name, "Id") == 0) { | |
| // Use a per-instance unique ID to avoid collisions between multiple tray icons. | |
| // The instance's address is stable for its lifetime, so this remains consistent. | |
| std::string id = "nativeapi-tray-" + | |
| std::to_string(reinterpret_cast<uintptr_t>(self)); | |
| return g_variant_new_string(id.c_str()); | |
| } |
libayatana-appindicator(GPL-3.0) caused three compounding problems: GPL license contamination risk for downstream users, binary ABI incompatibilities across distros, and build failures in network-restricted packaging environments. The underlying functionality is just the StatusNotifierItem D-Bus spec, which can be implemented directly.Approach
GDBus (GLib/GIO) is already a transitive dependency of
gtk+-3.0, so this adds zero new dependencies.Changes
src/platform/linux/tray_icon_linux.cpp— Full rewrite:/StatusNotifierItemimplementingorg.kde.StatusNotifierItemIconPixmap(GdkPixbuf → ARGB32 network-byte-order conversion),Title,ToolTip,StatusActivate→TrayIconClickedEvent,SecondaryActivate→TrayIconDoubleClickedEvent,ContextMenu→TrayIconRightClickedEventorg.kde.StatusNotifierWatcher(KDE/Plasma) andcom.canonical.StatusNotifierWatcher(Ubuntu/GNOME) with graceful fallbackOpenContextMenu()usesPositioningStrategy::CursorPosition()to pop the GTK menu at pointer positionstd::atomic<int>src/platform/linux/tray_manager_linux.cpp— Removes all AppIndicator conditionals;IsSupported()probes D-Bus session bus availability (result cached)src/CMakeLists.txt— Dropsayatana-appindicator3-0.1pkg-config module and its link target.github/workflows/build.yml— Removeslibayatana-appindicator3-devfrom the Linux CI install stepOriginal prompt
This section details on the original issue you should resolve
<issue_title>[Linux]
libayatana-appindicatortroublesome</issue_title><issue_description>Rewritten with explicit separation of problems, causes, and solutions.
Summary
The library
libayatana-appindicator, currently used for Linux support, creates multiple issues for Linux distributions and downstream users. These issues fall into three main categories: packaging constraints, binary compatibility problems, and licensing risks.In practice, these issues arise because the library is being used where a simple D-Bus implementation would suffice, and the current dependency chain introduces unnecessary complexity.
Issues
1. Packaging problems on Linux distributions
Using
libayatana-appindicatoris difficult for many Linux distributions due to their packaging policies and Flutter’s installation model.Cause
Most distributions prohibit network access during package builds. Examples:
Gentoo packaging rules:
https://devmanual.gentoo.org/ebuild-writing/functions/src_test/index.html#tests-that-require-network-or-service-access
Debian policy:
https://www.debian.org/doc/debian-policy/ch-source.html
However Flutter itself often expects:
Flutter explicitly documents issues when installed in restricted locations:
https://docs.flutter.dev/install/troubleshoot#flutter-in-special-folders
Result
Package maintainers are forced to rely on prebuilt binaries rather than building from source.
2. Binary compatibility issues
Using prebuilt binaries introduces compatibility problems with system libraries.
Cause
The
libayatana-appindicatorlibraries installed by distributions can vary in:Examples from typical installations:
Prebuilt binaries may:
Additional complication
The library ecosystem has been forked and renamed multiple times, meaning different distributions may ship different implementations.
Examples:
Distribution renames and compatibility issues:
3. GPL licensing risk
A major issue is the license of the library.
libayatana-appindicatoris licensed under GPL-3.0:https://github.com/AyatanaIndicators/libayatana-appindicator/blob/main/COPYING
Cause
If a project links against a GPL library (including via FFI), the GPL's copyleft terms may apply to the entire program.
This can require the entire application to be distributed under GPL-compatible terms.
Result
This introduces a licensing contamination risk for downstream users, particularly those who wish to distribute closed-source software.
Root cause
After investigation,
libayatana-appindicatordoes not provide complex functionality.It is primarily an implementation of the StatusNotifierItem specification:
https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/
This specification simply defines a D-Bus interface for system tray indicators.
...
libayatana-appindicatortroublesome #42✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.