A GNOME Shell extension that shows StatusNotifierItem (AppIndicator) icons
tightly grouped in the top panel. Written from scratch for GNOME 45 and later,
after auditing the AppIndicator extension this repo forked originally (see
../legacy). About a tenth of the code, because it drops a decade of
compatibility shims and supports exactly one protocol.
- Owns
org.kde.StatusNotifierWatcherand shows every registered item as a panel icon. Both registration conventions work: Ayatana apps that pass an object path and KDE apps that pass a bus name. - Left click activates the app (or opens the menu when the item says it is a menu), right click opens the menu, middle click sends SecondaryActivate, scrolling forwards Scroll events.
- Renders icons from theme names, absolute paths, app-shipped theme directories, and raw ARGB pixmaps.
- Mirrors each item's dbusmenu into a native shell popup menu, including submenus, separators, checkmarks, radios, and item icons.
- Preferences: icon size, spacing, opacity, desaturation, brightness, contrast. All bound directly to GSettings.
- Optional RAM usage readout left of the icons: one async read of /proc/meminfo per interval (MemTotal minus MemAvailable, the free(1) definition). Toggle, refresh interval, format (percent, used, used/total), and the gap to the icons are settings; when off, no timer runs at all.
- Hover and pressed styling comes from the shell theme (
panel-button), so light and dark themes both look right.
- No legacy XEmbed (X11 systray) icons. That protocol is nearly extinct; use the legacy extension if you still need it.
- No overlay icons, tooltips, or the Ayatana label extension. Rarely visible at panel sizes, and each one costs protocol code.
- No custom icon replacement table.
- No brute-force rediscovery of items that registered before the extension enabled. Well-behaved apps watch the watcher name and re-register when it changes owner; the old approach spawned a helper process to walk the bus and was broken in the fork anyway.
./install.sh # install or update
./install.sh --uninstallThen restart the shell (log out and in on Wayland) and run
gnome-extensions enable traylet@roverflow. Disable the legacy extension
first if it is running; both queue for the same bus name and only one acts.
The seam runs between protocol and presentation. Files under sni/ never
import St or Clutter; files under ui/ never speak raw D-Bus except through
their own proxy in menuClient.js.
extension.js entry point; everything created in enable(), torn down in disable()
util.js Debouncer (owns GSource cleanup) and cancelled-error check
system/
memory.js polls /proc/meminfo, emits used/total samples; costs nothing while stopped
sni/
interfaces.js D-Bus interface XML as constants; no file reads at runtime
watcher.js owns the watcher name, emits item-added / item-removed with ready StatusItems
item.js one StatusNotifierItem: property cache, change batching, actions
icons.js icon facts (name, theme path, pixmaps) to a single Gio.Icon
ui/
tray.js the panel container; owns TrayIcon widgets and menu registration
trayIcon.js per-item widget: icon, effects, click and scroll routing
ramIndicator.js RAM label; wires the memory monitor to settings and the panel
menuClient.js dbusmenu tree to shell PopupMenu; full-layout rebuild, in-place property patches
Design rules the code follows:
- Nothing happens in the
Extensionconstructor.enable()builds,disable()destroys, and nothing global gets patched. - Every async call runs against a
Gio.Cancellableowned by the object that started it, cancelled in that object'sdestroy(). - Signal connections on widgets use
connectObject, so they disconnect when the widget goes away. - The SNI spec's refusal to use
PropertiesChangedis handled in one place:item.jsmaps change signals to property names and batches the re-fetches behind a 50ms debounce.
GPL-2.0-or-later, same as the extension this replaces.