Skip to content

Replace libayatana-appindicator with native D-Bus StatusNotifierItem implementation#43

Merged
lijy91 merged 2 commits intomainfrom
copilot/fix-libayatana-appindicator-issues
Mar 22, 2026
Merged

Replace libayatana-appindicator with native D-Bus StatusNotifierItem implementation#43
lijy91 merged 2 commits intomainfrom
copilot/fix-libayatana-appindicator-issues

Conversation

Copy link
Contributor

Copilot AI commented Mar 8, 2026

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:

    • Registers a D-Bus object at /StatusNotifierItem implementing org.kde.StatusNotifierItem
    • Exposes all required SNI properties including IconPixmap (GdkPixbuf → ARGB32 network-byte-order conversion), Title, ToolTip, Status
    • Maps D-Bus method calls to existing events: ActivateTrayIconClickedEvent, SecondaryActivateTrayIconDoubleClickedEvent, ContextMenuTrayIconRightClickedEvent
    • Registers with both org.kde.StatusNotifierWatcher (KDE/Plasma) and com.canonical.StatusNotifierWatcher (Ubuntu/GNOME) with graceful fallback
    • OpenContextMenu() uses PositioningStrategy::CursorPosition() to pop the GTK menu at pointer position
    • Thread-safe SNI instance indexing via std::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 — Drops ayatana-appindicator3-0.1 pkg-config module and its link target

  • .github/workflows/build.yml — Removes libayatana-appindicator3-dev from the Linux CI install step

Original prompt

This section details on the original issue you should resolve

<issue_title>[Linux] libayatana-appindicator troublesome</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-appindicator is 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:

Required targets must not attempt network access during build.

However Flutter itself often expects:

  • installation in writable directories
  • downloading artifacts during build
  • execution outside restricted environments

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-appindicator libraries installed by distributions can vary in:

  • file names
  • ABI versions
  • installation paths

Examples from typical installations:

/usr/lib64/libayatana-appindicator3.so
/usr/lib64/libayatana-appindicator3.so.1
/usr/lib64/libayatana-appindicator3.so.1.0.0
/usr/lib64/libayatana-ido3-0.4.so
/usr/lib64/libayatana-ido3-0.4.so.0
/usr/lib64/libayatana-ido3-0.4.so.0.0.0
/usr/lib64/libayatana-indicator3.so
/usr/lib64/libayatana-indicator3.so.7
/usr/lib64/libayatana-indicator3.so.7.0.0

Prebuilt binaries may:

  • expect a different version of these libraries
  • expect them in different paths
  • link against incompatible builds

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-appindicator is 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-appindicator does 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.


...


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…ntation

Co-authored-by: lijy91 <3889523+lijy91@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix issues with libayatana-appindicator for Linux support Replace libayatana-appindicator with native D-Bus StatusNotifierItem implementation Mar 8, 2026
@lijy91 lijy91 marked this pull request as ready for review March 22, 2026 09:45
Copilot AI review requested due to automatic review settings March 22, 2026 09:45
@lijy91 lijy91 merged commit 93730d8 into main Mar 22, 2026
9 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.StatusNotifierItem over 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.

Comment on lines 462 to 466
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;
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +264 to +270
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) {
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +34
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;
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}
});

Copilot uses AI. Check for mistakes.
Comment on lines 23 to +35
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;
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +179 to +203
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);

Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +394 to +397
TrayIcon::TrayIcon() : pimpl_(std::make_unique<Impl>(this)) {
if (pimpl_->Init()) {
pimpl_->visible_ = true;
} else {
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +403 to +404
TrayIcon::TrayIcon(void* /*tray*/) : pimpl_(std::make_unique<Impl>(this)) {
// For API compatibility; create a fresh SNI tray icon ignoring the raw pointer.
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Copilot uses AI. Check for mistakes.
Comment on lines +345 to +346
if (g_strcmp0(property_name, "Id") == 0)
return g_variant_new_string("nativeapi-tray");
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_).

Suggested change
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());
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Linux] libayatana-appindicator troublesome

3 participants