Fix missing KDE tray icon by preferring Ayatana AppIndicator (#621)
* Fix missing KDE tray icon by preferring Ayatana AppIndicator Fedora and Arch installs pull libappindicator-gtk3, Canonical's original AppIndicator library that hasn't had a real release since ~2013. It loads fine and Indicator.new_with_path()/set_status() run without raising, but it never registers a StatusNotifierItem with KDE's StatusNotifierWatcher, so the tray icon silently never shows up on Plasma. Confirmed on Fedora 44 / Plasma 6.7 by checking RegisteredStatusNotifierItems over D-Bus directly before and after the fix. The actively maintained Ayatana fork registers correctly and is already what the Debian, Ubuntu, openSUSE and Gentoo branches of install.sh prefer. Reorder the gi import in tray_indicator.py to try AyatanaAppIndicator3 first and fall back to the legacy AppIndicator3 typelib only if neither Ayatana variant is available. Update install.sh so Fedora and Arch installs pull the Ayatana package too, via a shared install_preferred_appindicator() helper that falls back to the legacy package if Ayatana isn't in the repos (e.g. RHEL/CentOS without EPEL). * Add tests for the AppIndicator/Ayatana import fallback chain The reordered try/except in tray_indicator.py wasn't exercised by the existing tests: they replace gi.repository with a MagicMock, which auto-creates any attribute on access and so can never raise the ImportError the fallback chain is built to catch. Add a small test class that swaps in a plain object exposing only a chosen subset of AppIndicator symbols, so importing anything else raises a real ImportError, and verify each of the three branches (Ayatana, lowercase Ayatana variant, legacy AppIndicator3) resolves correctly. * Retry Ayatana install even when legacy AppIndicator is already present install_preferred_appindicator() previously skipped the Ayatana install whenever either package was already present, so re-running install.sh on an existing Fedora/Arch setup with libappindicator-gtk3 from a prior install would never pick up Ayatana, leaving the KDE tray broken for exactly the users this fix targets. Now it only short-circuits when Ayatana itself is installed, tries Ayatana first regardless of legacy presence, and only falls back to a warning about the already-installed legacy package if the Ayatana install fails. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
co-authored by
Jatin K Malik
parent
5eff933d73
commit
afc4b37c80
+43
-2
@@ -1451,6 +1451,43 @@ pacman_package_installed() {
|
||||
pacman -Q "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Install an AppIndicator/StatusNotifierItem provider, preferring the
|
||||
# actively maintained Ayatana fork over the legacy Canonical package
|
||||
# (unmaintained since ~2013). The legacy package can install and import
|
||||
# without error yet silently fail to register a tray icon with KDE's
|
||||
# StatusNotifierWatcher, leaving no icon and no logged error. $1 is the
|
||||
# package-manager install command (e.g. "sudo dnf install -y"), $2/$3 are
|
||||
# the Ayatana/legacy package names for that package manager, $4 is the
|
||||
# "is this package installed" checker function name.
|
||||
install_preferred_appindicator() {
|
||||
local install_cmd="$1"
|
||||
local ayatana_pkg="$2"
|
||||
local legacy_pkg="$3"
|
||||
local checker="$4"
|
||||
|
||||
if "$checker" "$ayatana_pkg"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if $install_cmd "$ayatana_pkg" 2>/dev/null; then
|
||||
print_info "Installed $ayatana_pkg (Ayatana AppIndicator; required for a working KDE tray icon)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if "$checker" "$legacy_pkg"; then
|
||||
print_info "$ayatana_pkg not available; $legacy_pkg is already installed but may not show a tray icon on KDE Plasma"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_info "$ayatana_pkg not available, falling back to $legacy_pkg (may not show a tray icon on KDE Plasma)..."
|
||||
if $install_cmd "$legacy_pkg"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_error "Failed to install an AppIndicator package (tried $ayatana_pkg and $legacy_pkg)"
|
||||
return 1
|
||||
}
|
||||
|
||||
suse_python_package_prefix() {
|
||||
python3 -c 'import sys; print(f"python{sys.version_info.major}{sys.version_info.minor}")' 2>/dev/null || echo "python3"
|
||||
}
|
||||
@@ -1588,8 +1625,8 @@ install_system_dependencies() {
|
||||
local APT_PACKAGES_DEBIAN_BASE="python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-ibus-1.0 libcairo2-dev cmake python3-dev build-essential portaudio19-dev python3-venv pkg-config wget curl unzip vulkan-tools libvulkan-dev $VULKAN_SHADER_PKG xclip xsel wl-clipboard $PYWHISPERCPP_BUILD_DEPS"
|
||||
local APT_PACKAGES_DEBIAN_11_12="$APT_PACKAGES_DEBIAN_BASE libgirepository1.0-dev gir1.2-ayatanaappindicator3-0.1"
|
||||
local APT_PACKAGES_DEBIAN_13_PLUS="$APT_PACKAGES_DEBIAN_BASE libgirepository-2.0-dev gir1.2-ayatanaappindicator3-0.1 util-linux-extra"
|
||||
local DNF_PACKAGES="python3-pip python3-gobject gtk3 libappindicator-gtk3 ibus-devel gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv pkg-config cmake wget curl unzip vulkan-tools vulkan-loader-devel glslang xclip xsel wl-clipboard"
|
||||
local PACMAN_PACKAGES="python-pip python-gobject gtk3 libappindicator-gtk3 ibus gobject-introspection python-cairo portaudio python-virtualenv pkg-config cmake wget curl unzip base-devel vulkan-tools vulkan-headers glslang xclip xsel wl-clipboard"
|
||||
local DNF_PACKAGES="python3-pip python3-gobject gtk3 ibus-devel gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv pkg-config cmake wget curl unzip vulkan-tools vulkan-loader-devel glslang xclip xsel wl-clipboard"
|
||||
local PACMAN_PACKAGES="python-pip python-gobject gtk3 ibus gobject-introspection python-cairo portaudio python-virtualenv pkg-config cmake wget curl unzip base-devel vulkan-tools vulkan-headers glslang xclip xsel wl-clipboard"
|
||||
local ZYPPER_PACKAGES="gtk3 ibus-devel gobject-introspection-devel portaudio-devel pkg-config cmake wget curl unzip xclip xsel wl-clipboard typelib-1_0-Notify-0_7 libnotify4"
|
||||
# Gentoo uses Portage and different package naming convention
|
||||
local EMERGE_PACKAGES="dev-python/pygobject:3 x11-libs/gtk+:3 dev-libs/libayatana-appindicator media-libs/portaudio dev-lang/python:3.9 pkgconf cmake dev-util/glslang x11-misc/xclip x11-misc/xsel gui-apps/wl-clipboard"
|
||||
@@ -1678,6 +1715,8 @@ install_system_dependencies() {
|
||||
else
|
||||
print_info "All required packages are already installed."
|
||||
fi
|
||||
|
||||
install_preferred_appindicator "$INSTALL_CMD" "libayatana-appindicator-gtk3" "libappindicator-gtk3" dnf_package_installed || exit 1
|
||||
;;
|
||||
|
||||
arch)
|
||||
@@ -1701,6 +1740,8 @@ install_system_dependencies() {
|
||||
else
|
||||
print_info "All required packages are already installed."
|
||||
fi
|
||||
|
||||
install_preferred_appindicator "sudo pacman -S --noconfirm" "libayatana-appindicator" "libappindicator-gtk3" pacman_package_installed || exit 1
|
||||
;;
|
||||
|
||||
suse)
|
||||
|
||||
@@ -14,16 +14,21 @@ import gi
|
||||
|
||||
# Import GTK
|
||||
gi.require_version("Gtk", "3.0")
|
||||
# Prefer Ayatana AppIndicator: it is the actively maintained fork and the one
|
||||
# most distros ship for KDE/SNI compatibility. The original Canonical
|
||||
# AppIndicator3 (unmaintained since ~2013) can be present alongside it (e.g.
|
||||
# Fedora's libappindicator-gtk3) but fails to register a StatusNotifierItem
|
||||
# on modern KDE Plasma, leaving the tray icon invisible with no error logged.
|
||||
try:
|
||||
gi.require_version("AppIndicator3", "0.1")
|
||||
from gi.repository import AppIndicator3
|
||||
gi.require_version("AyatanaAppIndicator3", "0.1")
|
||||
from gi.repository import AyatanaAppIndicator3 as AppIndicator3
|
||||
except (ImportError, ValueError):
|
||||
try:
|
||||
gi.require_version("AyatanaAppIndicator3", "0.1")
|
||||
from gi.repository import AyatanaAppIndicator3 as AppIndicator3
|
||||
except (ImportError, ValueError):
|
||||
gi.require_version("AyatanaAppindicator3", "0.1")
|
||||
from gi.repository import AyatanaAppindicator3 as AppIndicator3
|
||||
except (ImportError, ValueError):
|
||||
gi.require_version("AppIndicator3", "0.1")
|
||||
from gi.repository import AppIndicator3
|
||||
|
||||
from gi.repository import GdkPixbuf, Gio, GLib, GObject, Gtk
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ The tests focus on the business logic of the TrayIndicator class.
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -634,3 +635,74 @@ class TestTrayIndicatorFlatpakIcons(unittest.TestCase):
|
||||
self.assertEqual(names["default"], "com.vocalinux.Vocalinux-microphone-off")
|
||||
self.assertEqual(names["active"], "com.vocalinux.Vocalinux-microphone")
|
||||
self.assertEqual(names["processing"], "com.vocalinux.Vocalinux-microphone-process")
|
||||
|
||||
|
||||
class TestAppIndicatorImportFallback(unittest.TestCase):
|
||||
"""The AppIndicator/Ayatana import chain must prefer the actively
|
||||
maintained Ayatana fork and only fall back to the legacy Canonical
|
||||
AppIndicator3 typelib when no Ayatana variant is installed.
|
||||
|
||||
Unlike the rest of this file, these tests use a bare object instead of
|
||||
a MagicMock for ``gi.repository``: MagicMock auto-creates any attribute
|
||||
on access, so it can never reproduce the real ``ImportError`` a missing
|
||||
typelib raises, which is exactly the behavior this fallback chain
|
||||
depends on.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.addCleanup(self._restore_gi_repository)
|
||||
self._clear_tray_indicator_module()
|
||||
|
||||
def tearDown(self):
|
||||
self._clear_tray_indicator_module()
|
||||
|
||||
@staticmethod
|
||||
def _clear_tray_indicator_module():
|
||||
for mod in [k for k in list(sys.modules.keys()) if "tray_indicator" in k]:
|
||||
del sys.modules[mod]
|
||||
|
||||
@staticmethod
|
||||
def _restore_gi_repository():
|
||||
sys.modules["gi"] = mock_gi
|
||||
sys.modules["gi.repository"] = mock_gi_repository
|
||||
|
||||
@staticmethod
|
||||
def _fake_repository(**appindicator_attrs):
|
||||
"""A gi.repository stand-in exposing only the given AppIndicator
|
||||
symbols, so importing anything else raises a real ImportError."""
|
||||
repo = SimpleNamespace(
|
||||
Gtk=mock_gtk,
|
||||
GLib=mock_glib,
|
||||
GObject=mock_gobject,
|
||||
GdkPixbuf=mock_gdkpixbuf,
|
||||
Gio=MagicMock(name="Gio"),
|
||||
**appindicator_attrs,
|
||||
)
|
||||
return repo
|
||||
|
||||
def test_prefers_ayatana_appindicator3_when_available(self):
|
||||
sentinel = MagicMock(name="AyatanaAppIndicator3")
|
||||
sys.modules["gi"] = mock_gi
|
||||
sys.modules["gi.repository"] = self._fake_repository(AyatanaAppIndicator3=sentinel)
|
||||
|
||||
import vocalinux.ui.tray_indicator as tray
|
||||
|
||||
self.assertIs(tray.AppIndicator3, sentinel)
|
||||
|
||||
def test_falls_back_to_lowercase_ayatana_variant(self):
|
||||
sentinel = MagicMock(name="AyatanaAppindicator3")
|
||||
sys.modules["gi"] = mock_gi
|
||||
sys.modules["gi.repository"] = self._fake_repository(AyatanaAppindicator3=sentinel)
|
||||
|
||||
import vocalinux.ui.tray_indicator as tray
|
||||
|
||||
self.assertIs(tray.AppIndicator3, sentinel)
|
||||
|
||||
def test_falls_back_to_legacy_appindicator3_when_no_ayatana_available(self):
|
||||
sentinel = MagicMock(name="AppIndicator3")
|
||||
sys.modules["gi"] = mock_gi
|
||||
sys.modules["gi.repository"] = self._fake_repository(AppIndicator3=sentinel)
|
||||
|
||||
import vocalinux.ui.tray_indicator as tray
|
||||
|
||||
self.assertIs(tray.AppIndicator3, sentinel)
|
||||
|
||||
Reference in New Issue
Block a user