pax_global_header00006660000000000000000000000064142675450370014527gustar00rootroot0000000000000052 comment=6025cc7257d3f8cc245a538b8e2c4d117d9e4bed snixembed-0.3.3/000077500000000000000000000000001426754503700135105ustar00rootroot00000000000000snixembed-0.3.3/.gitignore000066400000000000000000000000161426754503700154750ustar00rootroot00000000000000snixembed doc snixembed-0.3.3/LICENSE000066400000000000000000000013271426754503700145200ustar00rootroot00000000000000Copyright 2020 Steef Hegeman Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. snixembed-0.3.3/README.md000066400000000000000000000123011426754503700147640ustar00rootroot00000000000000snixembed - proxy StatusNotifierItems as XEmbedded systemtray-spec icons INFO: recent electron versions [supports XEmbed tray icons again for now](https://github.com/electron/electron/pull/23674). This means snixembed is of limited use. It will however be maintained in case electron or other software eventually drops their XEmbed support again. Previously, electron dropped support for the old XEmbed-based system tray protocol, while many status bars for simple X window managers still do no not support newer protocols like [StatusNotifierItem](https://freedesktop.org/wiki/Specifications/StatusNotifierItem/). This project is basically a dirty hack using deprecated GTK+ StatusIcons to proxy this new specification to the old one. As newer electron versions support the old XEmbed-based tray icons again, snixembed is currently not very useful, unless you rely on those old versions. (or rely on chromium tray icons, which still only support StatusNotifierItem) Most other software supports XEmbed icons as a fallback. While snixembed works fine with most setups, some bars and DEs provide their own optional SNI support, which should be preferred when available. ## Status Currently supported: - icons¹ (by pixmap and by freedesktop name) - activation on left mouse button - context menu on right mouse button (Menu dbusmenu or ContextMenu) - tooltips (on hover, all markup except hyperlinks) - limited AppIndicator support as a fallback (see Known issues and workarounds) This is enough for electron apps like Riot and Signal, and also for most Qt apps as the list above includes all functionality QSystemTrayIcon permits. Please see the known issues and issue tracker if an app doesn't work properly. ¹: so-called AttentionIcons and OverlayIcons are not supported, but I have yet to encounter software that uses this part of the spec. Qt does not support it. ## Usage Simply launch `snixembed` to start watching for icons. Some applications only present a tray icon if snixembed is available on start. For autostarting, use `snixembed --fork` before starting SNI applications to avoid race conditions. This will fork once the service is running. (e.g. in an `.xinitrc`, use `snixembed --fork` instead of `snixembed &` before tray apps) ## Issues and support Issues are tracked on sourcehut: [~steef/snixembed](https://todo.sr.ht/~steef/snixembed). There's a Matrix room at [#snixembed:matrix.org](https://matrix.to/#/#snixembed:matrix.org) for discussion and support. Come say hi! ## Building and installing ### Dependencies - make (make) - vala (make) - gtk+-3.0, gio-2.0, glib-2.0 - libdbusmenu-gtk3-0.4, libdbusmenu-glib-0.4 (the most recent version) These should be packaged in most major distributions. ### Building Simply run `make` to build and if desired `make install` to install, by default to `/usr/bin/snixembed`. ### Packages There are AUR packages available: [stable](https://aur.archlinux.org/packages/snixembed) and [development](https://aur.archlinux.org/packages/snixembed-git). For Gentoo/Portage, [powerman made an ebuild](https://github.com/powerman/powerman-overlay/tree/master/x11-misc/snixembed). For Void Linux, it is [packaged by projectmoon](https://github.com/void-linux/void-packages/tree/master/srcpkgs/snixembed). ## Known issues and workarounds - ayatana/libappindicator seem to think their standard is supported when they see the service, and then start talking appindicator to it. Therefore they do not fall back to XEmbed specification, and snixembed gets a load of AppIndicator jargon thrown at it. Currently, snixembed can handle basic AppIndicator functionality (icons and menus), and may support more in the future. But not everything works. Feel free to report issues to the tracker. As a workaround, you may start such applications with their own D-Bus session bus. They will be unable to communicate through D-Bus with the outside world, and therefore they won't find snixembed and fall back to the XEmbed tray icon. To do this, execute application like this: `dbus-launch --exit-with-session `. As already mentioned, this will only work for apps that don't heavily rely on other services on the session bus for functionality. Examples that have been tested to work are discord and skypeforlinux. Alternatively, one could also choose to run snixembed on a separate session bus, and let all SNI-only apps run on that session bus too by running `dbus-daemon --session --print-address` and then setting the environment variable `DBUS_SESSION_BUS_ADDRESS` to the address printed by dbus-daemon for all applications that have to communicate with snixembed. This will isolate snixembed and those applications from the rest of the D-Bus world. As the author does not really use any non-SNI applcations, this is not well tested or supported, but I'm happy to assist anyone going this route or look into contributions that make this easier. - some apps probe for StatusNotifierItem support not by checking for the D-Bus service but by checking environment variables. Exporting `XDG_CURRENT_DESKTOP=KDE` and `KDE_SESSION_CURRENT=5` seems to do the trick. - `gtk_widget_get_scale_factor: assertion 'GTK_IS_WIDGET (widget)' failed`: This error could mean that no XEmbed tray is present. snixembed-0.3.3/makefile000066400000000000000000000017771426754503700152240ustar00rootroot00000000000000PKG = gtk+-3.0 gio-2.0 Dbusmenu-0.4 DbusmenuGtk3-0.4 CPKG = dbusmenu-gtk3-0.4 SRC = src/*.vala version.vala PREFIX ?= /usr BINDIR ?= /bin MANDIR ?= /share/man PKG := $(foreach pkg, $(PKG), --pkg $(pkg)) CFLAGS := $(CFLAGS) $(shell pkg-config --cflags --libs $(CPKG)) VFLAGS := $(foreach flag, $(CFLAGS), -X $(flag)) $(VFLAGS) snixembed: $(SRC) valac -o $@ $(VFLAGS) $(PKG) $(SRC) version.vala: # update version from git if possible ifneq (,$(wildcard ./.git/HEAD)) echo "const string VERSION = \"$(shell git describe --always --tags --dirty)\";" > $@ endif doc: $(SRC) valadoc --force -o $@ --internal --private --package-name snixembed $(SRC) $(PKG) clean: rm -rf src/*.c version.vala snixembed doc install: snixembed install -Dm755 snixembed $(DESTDIR)$(PREFIX)$(BINDIR)/snixembed install -Dm644 snixembed.1 $(DESTDIR)$(PREFIX)$(MANDIR)/man1/snixembed.1 uninstall: rm -f $(DESTDIR)$(PREFIX)$(BINDIR)/snixembed \ $(DESTDIR)$(PREFIX)$(MANDIR)/man1/snixembed.1 .PHONY: clean install uninstall version.vala snixembed-0.3.3/snixembed.1000066400000000000000000000017021426754503700155500ustar00rootroot00000000000000.\" Contributors: see groff_man(7) and man-pages(7) .TH snixembed 1 2022-07-25 .SH NAME snixembed \- proxy the StatusNotifierItem protocol to the XEmbed System Tray .SH SYNOPSIS .SY snixembed .OP \-\-fork .YS .SH DESCRIPTION The snixembed daemon proxies StatusNotifierItems as XEmbedded System Tray icons. .SH OPTIONS .TP .BR \-\-fork .RS Set up in the foreground, then fork to background process. .PP This can be used to avoid race conditions with applications that expect a StatusNotifierWatcher to be ready on startup. Start these applications only after snixembed --fork has completed. .RE .SH BUGS Not all of the StatusNotifierItem specification is supported. Current support includes: mouse events, context menus, tooltips. There is also limited AppIndicator support. .SH AUTHORS Maintained by Steef Hegeman, who is assisted by other open source contributors. .PP More information and up-to-date sources can be found at https://git.sr.ht/~steef/snixembed. snixembed-0.3.3/src/000077500000000000000000000000001426754503700142775ustar00rootroot00000000000000snixembed-0.3.3/src/main.vala000066400000000000000000000017271426754503700160770ustar00rootroot00000000000000int fork_self(string[] argv) { Bus.watch_name(BusType.SESSION, StatusNotifierWatcher.NAME, BusNameWatcherFlags.NONE, () => Process.exit(0), null); for (int i = 0; i < argv.length; i++) { argv[i] = argv[i] == "--fork" ? "" : argv[i]; } Pid pid; Process.spawn_async(null, argv, null, SpawnFlags.CHILD_INHERITS_STDIN | SpawnFlags.SEARCH_PATH, null, out pid); ChildWatch.add(pid, (pid, status) => { Process.close_pid(pid); Process.exit(status); }); new MainLoop().run(); return 0; } int main(string[] args) { if ("-v" in args || "--version" in args) { stdout.printf("version: %s\n", VERSION); return 0; } if ("--fork" in args) { string[] argv = args; // Copy since we're not allowed to mutate args return fork_self(argv); } Gtk.init(ref args); var server = new ProxyServer(); server.start(); Gtk.main(); return 0; } snixembed-0.3.3/src/proxyicon.vala000066400000000000000000000111451426754503700172000ustar00rootroot00000000000000class ProxyIcon : Object { StatusNotifierItem item; Gtk.StatusIcon icon; DbusmenuGtk.Menu menu; public ProxyIcon(string name, string object) throws IOError { init.begin(name, object); } async void init(string name, string object) { item = yield Bus.get_proxy(BusType.SESSION, name, object, DBusProxyFlags.NONE, null); icon = new Gtk.StatusIcon(); item.changed.connect(property => { switch(property) { case "tool-tip": set_tooltip(); break; } }); item.icon_changed.connect(set_icon); set_icon(); icon.set_name(item.id); // MUST be called after an icon is set due to Gdk internals set_tooltip(); if (item.menu != null) { stdout.printf("%s set menu: %s\n", name, item.menu); menu = new DbusmenuGtk.Menu(name, item.menu); } icon.activate.connect(() => item.activate(0, 0)); // TODO: consider smarter coordinate hints icon.button_press_event.connect(on_button_press); icon.scroll_event.connect(on_scroll); } bool on_button_press(Gdk.EventButton event) { if (event.button == 3) { if (menu != null) { menu.popup_at_pointer(event); return true; } item.context_menu((int)event.x_root, (int)event.y_root); return true; } return false; } bool on_scroll(Gdk.EventScroll event) { switch(event.direction) { case Gdk.ScrollDirection.SMOOTH: // It seems that we can safely ignore this break; case Gdk.ScrollDirection.DOWN: case Gdk.ScrollDirection.UP: item.scroll((int)event.delta_y, "vertical"); break; case Gdk.ScrollDirection.LEFT: case Gdk.ScrollDirection.RIGHT: item.scroll((int)event.delta_x, "horizontal"); break; default: stderr.printf("unsupported scrolling event\n"); break; } return true; } void set_icon() { var name = item.icon_name; if (name != null && name != "") { /* Some apps wrongly set a filename for name, relying on this * undefined behavior. Since StatusIcon.set_from_file gives no * feedback, this check and race is about the best we can do. */ if (name.contains("/") && FileUtils.test(name, FileTest.EXISTS)) { icon.set_from_file(name); return; } var theme = Gtk.IconTheme.get_for_screen(icon.screen); var path = item.icon_theme_path; if (path != null) { string[] paths; theme.get_search_path(out paths); if (!(path in paths)) { theme.append_search_path(path); } } if (!theme.rescan_if_needed() && !theme.has_icon(name)) { /* Forcibly trigger a rescan in case the icon file was added * after the theme has been scanned (new files don't change * mtime if they're in a subdirectory). */ string[] paths; theme.get_search_path(out paths); theme.set_search_path(paths); } set_icon_name(name); if (theme.has_icon(name)) { return; } } var pixmaps = item.icon_pixmap; if (pixmaps != null) { set_icon_pixmap(pixmaps); } } void set_icon_name(string name) { icon.set_from_icon_name(name); } void set_icon_pixmap(IconPixmap[] pixmaps) { if (pixmaps.length == 0) return; var pixmap = pixmaps[0]; // TODO: choose the best one // Convert from ARGB to RGBA for (int i = 0; i < pixmap.width * pixmap.height * 4; i += 3) { var alpha = pixmap.data[i]; pixmap.data[i] = pixmap.data[i + 1]; pixmap.data[i + 1] = pixmap.data[i + 2]; pixmap.data[i + 2] = pixmap.data[i + 3]; pixmap.data[i + 3] = alpha; } var pixbuf = new Gdk.Pixbuf.from_data(pixmap.data, Gdk.Colorspace.RGB, true, 8, pixmap.width, pixmap.height, pixmap.width * 4); icon.set_from_pixbuf(pixbuf); } void set_tooltip() { var tooltip = item.tool_tip; string title = tooltip.title; string text = tooltip.text; if (text != null && text != "") { title += @"\n$(tooltip.text)"; } icon.set_tooltip_markup(title); } } snixembed-0.3.3/src/proxyserver.vala000066400000000000000000000035401426754503700175560ustar00rootroot00000000000000class ProxyServer : Object { StatusNotifierWatcher watcher; HashTable icons; public ProxyServer() { icons = new HashTable(str_hash, str_equal); } public void start() { // Register watcher Bus.own_name(BusType.SESSION, StatusNotifierWatcher.NAME, BusNameOwnerFlags.NONE, on_bus_acquired_watcher, () => { }, () => { stderr.printf("error: could not acquire watcher name\n"); Process.exit(1); }); } bool on_item_registered(string service, BusName sender) { stdout.printf("%s appeared\n", service); var name = service; var object = StatusNotifierItem.OBJECT; if (!DBus.is_name(service)) { stdout.printf("%s is not a service name, falling back to appindicator on %s\n", service, sender); name = sender; object = service; } try { var icon = new ProxyIcon(name, object); icons.insert(name, icon); } catch (IOError e) { stderr.printf("error: %s\n", e.message); } return true; } bool on_item_unregistered(string service) { stdout.printf("%s vanished\n", service); icons.remove(service); return true; } void on_bus_acquired_watcher(DBusConnection conn) { try { watcher = new StatusNotifierWatcher(); watcher.status_notifier_item_registered.connect(on_item_registered); watcher.status_notifier_item_unregistered.connect(on_item_unregistered); conn.register_object(StatusNotifierWatcher.OBJECT, watcher); watcher.status_notifier_host_registered(); } catch (IOError e) { stderr.printf("could not register watcher service\n"); } } } snixembed-0.3.3/src/statusnotifieritem.vala000066400000000000000000000062721426754503700211150ustar00rootroot00000000000000struct IconPixmap { int32 width; int32 height; uint8[] data; } struct ToolTip { string icon_title; IconPixmap[] icon_pixmap; string title; string text; } // StatusNotifierItem proxy // // Do not use the D-Bus signals directly, instead rely on icon_changed for icon changes // and the property changed signals for the rest (title, tooltip, ...). // // TODO: handle race conditions [DBus (name = "org.kde.StatusNotifierItem")] interface StatusNotifierItem : Object { public const string OBJECT = "/StatusNotifierItem"; // Properties public abstract string id { owned get; } public abstract string title { owned get; } public abstract string icon_name { owned get; } public abstract IconPixmap[] icon_pixmap { owned get; } public abstract ToolTip tool_tip { owned get; } public abstract ObjectPath menu { owned get; } // AppIndicator TODO: split in separate interface public abstract string icon_theme_path { owned get; } // Methods public abstract void activate(int x, int y); public abstract void context_menu(int x, int y); public abstract void scroll(int delta, string orientation); // Signals public virtual signal void new_title() { update_cache("Title", "title"); } public virtual signal void new_icon() { handle_new_icon(); } public signal void icon_changed(); // Asynchronously update icon data and then emit icon_changed if necessary async void handle_new_icon() { var changes = 0; var done = 0; AsyncReadyCallback handler = (obj, res) => { try { if (update_cache.end(res)) changes++; } finally { if (++done == 3) handle_new_icon.callback(); } }; update_cache.begin("IconName", "icon-name", handler); update_cache.begin("IconPixmap", "icon-pixmap", handler); update_cache.begin("IconThemePath", "icon-theme-path", handler); // AppIndicator only yield; if (changes != 0) icon_changed(); } public virtual signal void new_tool_tip() { update_cache("ToolTip", "tool-tip"); } // Internal cache updates and signalling // // This looks like the most ugly implementation ever, however: // - Vala DBus abstraction is abstract // - GObject notify signals doesn't work // - g_properties_changed does not get called automatically when the cache is updated // - this is simple and just works internal abstract signal void changed(string property); async bool update_cache(string property, string name) throws Error { var me = this as DBusProxy; var par = new Variant("(ss)", me.get_interface_name(), property); try { var res = yield me.call("org.freedesktop.DBus.Properties.Get", par, 0, -1, null); Variant v; res.get("(v)", out v); if (v != me.get_cached_property(property)) { me.set_cached_property(property, v); (me as StatusNotifierItem).changed(name); return true; } } catch (DBusError.UNKNOWN_PROPERTY e) {} catch (DBusError.INVALID_ARGS e) {} return false; } } snixembed-0.3.3/src/statusnotifierwatcher.vala000066400000000000000000000034011426754503700216030ustar00rootroot00000000000000[DBus (name = "org.kde.StatusNotifierWatcher")] public class StatusNotifierWatcher : Object { public const string NAME = "org.kde.StatusNotifierWatcher"; public const string OBJECT = "/StatusNotifierWatcher"; // Methods public void register_status_notifier_item(string service, BusName sender) { var name = service; if (!DBus.is_name(service)) { // appindicator fallback name = sender; } if (watchers.contains(name)) { stdout.printf("%s (%s) is already registered; ignoring registration\n", name, service); return; } watchers[name] = Bus.watch_name(BusType.SESSION, name, BusNameWatcherFlags.NONE, (conn, item, owner) => status_notifier_item_registered(service, sender), (conn, item) => { Bus.unwatch_name(watchers[name]); watchers.remove(name); status_notifier_item_unregistered(item); }); } public void register_status_notifier_host(string service) { // We probably don't have to anything here, as we just proxy } // Properties public string[] registered_status_notifier_items { get; } public bool is_status_notifier_host_registered { get { return true; } } // This property is undocumented yet KDE implements it public int protocol_version { get { return 0; } } // Signals internal signal bool status_notifier_item_registered(string service, BusName sender); public signal bool status_notifier_item_unregistered(string service); public signal bool status_notifier_host_registered(); // Internal HashTable watchers; construct { watchers = new HashTable(str_hash, str_equal); } } snixembed-0.3.3/test/000077500000000000000000000000001426754503700144675ustar00rootroot00000000000000snixembed-0.3.3/test/appindicator.py000077500000000000000000000017341426754503700175260ustar00rootroot00000000000000#!/usr/bin/env python3 import pathlib import gi gi.require_version('Gtk', '3.0') gi.require_version('AppIndicator3', '0.1') from gi.repository import Gtk from gi.repository import AppIndicator3 as ai ICONNAME = "edit-undo" # Usually available ICONPATH = str((pathlib.Path(__file__).parent / "test.png").resolve()) indicator = ai.Indicator.new("appindicator-test", ICONNAME, ai.IndicatorCategory.APPLICATION_STATUS) menu = Gtk.Menu() menu_items = map(lambda l: [Gtk.MenuItem(label=l[0]), l[1]], [ ["Set IconName", lambda _: indicator.set_icon(ICONNAME)], ["Set bogus IconName", lambda _: indicator.set_icon("!@#$")], ["Set path as IconName", lambda _: indicator.set_icon(ICONPATH)], ["Quit", Gtk.main_quit], ]) for i in menu_items: i[0].connect("activate", i[1]) menu.append(i[0]) menu.show_all() indicator.set_title("AppIndicator test") indicator.set_menu(menu) Gtk.main() snixembed-0.3.3/test/statusnotifieritem.py000077500000000000000000000015131426754503700210060ustar00rootroot00000000000000#!/usr/bin/env python3 import pathlib from PySide6.QtGui import QAction, QIcon from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon ICONNAME = "firefox" # Usually available ICONPATH = str((pathlib.Path(__file__).parent / "test.png").resolve()) QIcon.setThemeSearchPaths([".local/share/icons", "/usr/share/icons"]) app = QApplication([]) name_icon = QIcon.fromTheme(ICONNAME) file_icon = QIcon(ICONPATH) sni = QSystemTrayIcon() menu = QMenu() menu_items = list(map(lambda l: [QAction(l[0]), l[1]], [ ["Set IconName", lambda : sni.setIcon(name_icon)], ["Set Pixmap", lambda : sni.setIcon(file_icon)], ["Quit", app.quit], ])) for i in menu_items: i[0].triggered.connect(i[1]) menu.addAction(i[0]) sni.setIcon(file_icon) sni.setContextMenu(menu) sni.show(); app.exec() snixembed-0.3.3/test/test.png000066400000000000000000000011441426754503700161540ustar00rootroot00000000000000PNG  IHDR szziCCPICC profile(}=HPOS"A;(dNDEt*BZu0yФ!Iqq\ ,V\uup77'E)"q;ZiV(鶙JLvE "i 2f%) ub<ޟKY 30muMOaEY%>'1AG+q.,̈NGB +-̊Fhۿwr K.bKGD pHYs.#.#x?vtIME  /F\tEXtCommentCreated with GIMPW;IDATXձ 07l,L Ԇ @=q k!4=3TIENDB`snixembed-0.3.3/version.vala000066400000000000000000000000401426754503700160340ustar00rootroot00000000000000const string VERSION = "0.3.3";