fix(linux): the Pomodoro timer shows in the top bar from the snap too
All checks were successful
CI / check (push) Successful in 9m41s
CI / deploy (push) Successful in 14s

From the snap, nothing showed in the top bar: AppArmor refused the
item's bus name (dbus_bind org.freedesktop.StatusNotifierItem-<pid>-1),
which snapd's desktop plug doesn't grant, contrary to what the research
found. The item is now laid out as libayatana-appindicator lays itself
out, within what snapd v2.77's desktop-legacy plug allows: at
/org/ayatana/NotificationItem/halkyon_learn, the only path from which
the label's signal (XAyatanaNewLabel) may come, and registered with the
watcher by that path under the connection's own name, binding no name
at all. Checked against GNOME's watcher here: it registers as the
system's own indicators do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-07 13:21:07 +02:00
commit 2030e397cc
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 20 additions and 16 deletions

View file

@ -63,7 +63,7 @@ class LinuxPomodoroPlatform(
);
/// The timer in the top bar, where the desktop shows such icons.
late final _indicator = StatusNotifierItem(_bus, id: 'halkyon-learn')
late final _indicator = StatusNotifierItem(_bus, id: 'halkyon_learn')
..chosen.listen((key) {
if (PomodoroAction.values.asNameMap()[key] case final action?) {
_actions.add(action);

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'dart:typed_data';
@ -14,19 +13,21 @@ typedef IndicatorEntry = ({String key, String label, bool enabled});
/// Ubuntu's built in, and KDE show), menu through `com.canonical.dbusmenu`.
///
/// Written over package:dbus rather than libayatana-appindicator, which a
/// build would need besides (and the snap the `unity7` plug): the bus name
/// `org.freedesktop.StatusNotifierItem-…`, the item at `/StatusNotifierItem`
/// and the menu at `/MenuBar` are what the snap's `desktop` plug allows. A
/// desktop without a watcher (stock GNOME without the extension) shows
/// nothing, and [show] says so.
/// build would need besides. Laid out as libayatana lays itself out, within
/// what the snap's rules allow (snapd v2.77's `desktop-legacy` plug, checked
/// 2026-10-07): the item at `/org/ayatana/NotificationItem/<id>`, the only
/// path the label's signal (`XAyatanaNewLabel`) may come from, registered
/// with the watcher by that path under the connection's own name, since the
/// item may bind no name of its own; the menu at `/MenuBar`. [id] names the
/// path, in letters, digits and underscores. A desktop without a watcher
/// (stock GNOME without the extension) shows nothing, and [show] says so.
class StatusNotifierItem(final DBusClient _bus, {required final String id}) {
static const _watcher = 'org.kde.StatusNotifierWatcher';
late final _item = _Item(id);
late final _menu = _Menu();
final _name = 'org.freedesktop.StatusNotifierItem-$pid-1';
/// Whether the item and its menu are on the bus, under their name.
/// Whether the item and its menu are on the bus.
var _exported = false;
/// Whether the top bar's watcher knows the item.
@ -50,7 +51,6 @@ class StatusNotifierItem(final DBusClient _bus, {required final String id}) {
if (!_exported) {
await _bus.registerObject(_item);
await _bus.registerObject(_menu);
await _bus.requestName(_name);
_exported = true;
_watcherChanges = _bus.nameOwnerChanged
.where((change) => change.name == _watcher)
@ -76,7 +76,7 @@ class StatusNotifierItem(final DBusClient _bus, {required final String id}) {
).callMethod(
_watcher,
'RegisterStatusNotifierItem',
[DBusString(_name)],
[DBusString(_item.path.value)],
replySignature: DBusSignature(''),
);
_registered = true;
@ -101,7 +101,7 @@ const _menuInterface = 'com.canonical.dbusmenu';
final _menuPath = DBusObjectPath('/MenuBar');
class _Item(final String _id) extends DBusObject {
this : super(DBusObjectPath('/StatusNotifierItem'));
this : super(DBusObjectPath('/org/ayatana/NotificationItem/$_id'));
var _title = '';
var _label = '';

View file

@ -187,13 +187,17 @@ void main() {
await platform.show(notice(run));
final [name] = watcher.registered;
expect(name, startsWith('org.freedesktop.StatusNotifierItem-'));
// Registered by its path, as libayatana-appindicator does: the snap's
// rules let the item bind no name, and send the label's signal only
// from there.
final [registered] = watcher.registered;
expect(registered, '/org/ayatana/NotificationItem/halkyon_learn');
final name = app.uniqueName;
expect(notifications.notified, isEmpty);
final item = DBusRemoteObject(
shell,
name: name,
path: DBusObjectPath('/StatusNotifierItem'),
path: DBusObjectPath(registered),
);
expect(
await item.getProperty('org.kde.StatusNotifierItem', 'XAyatanaLabel'),