A lightweight daemon that bridges Wine/Windows system tray icons to Wayland's StatusNotifierItem protocol. Drop-in replacement for xembedsniproxy that doesn't steal keyboard focus.
wine-sni-bridge-demo.mp4
On Wayland, Wine applications that use the system tray (minimize-to-tray games, background apps) rely on the X11 XEmbed protocol. The standard solution, xembedsniproxy from KDE Plasma, converts these to the modern StatusNotifierItem (SNI) protocol for status bars like Waybar.
But xembedsniproxy has a critical flaw on wlroots-based compositors: it creates override_redirect (unmanaged) X11 windows that steal keyboard focus. Once focus is grabbed, you can't type, use shortcuts, or interact with any window until you open and close something like rofi to reset the focus chain. This is a known issue across Sway, Hyprland, dwl, and other wlroots compositors.
There is no upstream fix. Wine's native SNI support has been pending since 2023 and hasn't been merged.
wine-sni-bridge solves this by:
- Using a managed X11 window (not
override_redirect) that compositors can control - Setting
_NET_WM_WINDOW_TYPE_UTILITYso it's excluded from alt-tab and overview - Never requesting keyboard focus
- Letting compositor window rules hide it completely
Wine App (minimize to tray)
|
v
X11 XEmbed Protocol (Shell_NotifyIcon)
|
v
wine-sni-bridge.py (claims _NET_SYSTEM_TRAY_S0)
| - Embeds icon windows in an offscreen container
| - Extracts icon pixels via CopyArea
| - Crops and scales to 48x48
| - Makes black background transparent
|
v
DBus StatusNotifierItem (per-icon bus connection)
|
v
Waybar / Any SNI-compatible status bar
- No focus stealing - managed utility window, compositor-controlled
- Multi-app support - each Wine app gets its own SNI item (separate DBus connections)
- Icon caching - remembers icons per WM_CLASS, no white flash on re-minimize
- Click forwarding - left/middle/right clicks in the status bar forwarded to Wine
- Waybar reload survival - watches for StatusNotifierWatcher restarts, re-registers automatically
- Stale icon cleanup - auto-undocks icons when X11 windows are destroyed
- Systemd integration - runs as a user service, auto-restarts on failure
- Single file - ~580 lines of Python, no build system needed
- Python 3.10+
- python-xlib - X11 protocol
- dbus-python - DBus interface
- PyGObject - GLib main loop
Arch Linux:
pacman -S python-xlib python-dbus python-gobjectFedora:
dnf install python3-xlib python3-dbus python3-gobjectDebian/Ubuntu:
apt install python3-xlib python3-dbus python3-giNixOS (imperative):
nix-shell -p python3Packages.xlib python3Packages.dbus-python python3Packages.pygobject3Or add to your system/home-manager Python environment:
(python3.withPackages (ps: [ ps.xlib ps.dbus-python ps.pygobject3 ]))For a fully declarative setup, see NixOS Module below.
# Clone
git clone https://github.com/waliori/wine-sni-bridge.git
cd wine-sni-bridge
# Install
cp wine-sni-bridge.py ~/.local/bin/
chmod +x ~/.local/bin/wine-sni-bridge.py
# Install systemd service
cp wine-sni-bridge.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now wine-sni-bridgeWARNING: Without compositor rules, the bridge's X11 container window will be visible as a small floating window on your desktop. You must add window rules for your compositor.
The bridge window uses:
- WM_CLASS:
wine-sni-bridge - Window type:
_NET_WM_WINDOW_TYPE_UTILITY
Add to hyprland.conf:
windowrulev2 = float, class:wine-sni-bridge
windowrulev2 = nofocus, class:wine-sni-bridge
windowrulev2 = opacity 0.0 override 0.0 override, class:wine-sni-bridge
windowrulev2 = noborder, class:wine-sni-bridge
windowrulev2 = noshadow, class:wine-sni-bridge
windowrulev2 = noblur, class:wine-sni-bridge
windowrulev2 = noanimations, class:wine-sni-bridge
Add to config:
for_window [app_id="wine-sni-bridge"] {
floating enable
move position -9999 -9999
no_focus
border none
opacity 0
}
Add to your config:
windowrule=nofocus:1,appid:wine-sni-bridge
windowrule=isfloating:1,appid:wine-sni-bridge
windowrule=isoverlay:1,appid:wine-sni-bridge
windowrule=focused_opacity:0.0,appid:wine-sni-bridge
windowrule=unfocused_opacity:0.0,appid:wine-sni-bridge
windowrule=isnoborder:1,appid:wine-sni-bridge
windowrule=isnoshadow:1,appid:wine-sni-bridge
windowrule=isnoanimation:1,appid:wine-sni-bridge
windowrule=noblur:1,appid:wine-sni-bridge
Note: Hyprland and Sway configs above are based on their documented window rule syntax but have not been tested by the author. The dwl/MangoWC config is battle-tested daily. If you find issues or have working configs for other compositors, please open a PR.
The intent of the rules is:
- Float the window (don't tile it)
- No focus (never give it keyboard input)
- Zero opacity (invisible)
- No decorations (no border, shadow, blur, animations)
Adapt to your compositor's rule syntax using app_id / class = wine-sni-bridge.
Add "tray" to your modules and configure it:
Disable Wine's WM_TAKE_FOCUS protocol to prevent residual focus issues:
wine reg add 'HKEY_CURRENT_USER\Software\Wine\X11 Driver' /t REG_SZ /v UseTakeFocus /d N /fApply to each Wine prefix you use (including Proton prefixes if needed).
For NixOS with home-manager:
# In your home-manager config
imports = [ ./path/to/wine-sni-bridge/nix/module.nix ];
services.wine-sni-bridge.enable = true;
# Optional: byteOrder = "native" (default) or "network". See below.The module automatically creates the systemd service with the correct Python environment.
The DBus StatusNotifierItem spec describes the IconPixmap array as "ARGB32
in network byte order" (big-endian A,R,G,B bytes). In practice, every
known SNI host — waybar's Cairo-based renderer, Quickshell and KDE Plasma
via Qt's QImage::Format_ARGB32, fcitx5 — reads the bytes in native
byte order, which on x86_64 LE means B,G,R,A. If the bridge packs the
pixmap per spec (big-endian), those hosts render color-swapped (pink/magenta
artefacts on any icon with red or blue channels).
Default: --byte-order native. This matches what every tested host
actually wants and makes icons render correctly out of the box.
Pass --byte-order network only if you run a host that truly follows the
spec literally and you see color-swapped icons with the default. If you
find such a host, please open an issue so it can be documented here.
Icons not appearing in Waybar:
- Check that StatusNotifierWatcher is running:
busctl --user list | grep StatusNotifier - Verify Waybar has the
traymodule configured - Check bridge logs:
journalctl --user -u wine-sni-bridge -f
Bridge window visible on desktop:
- Compositor rules not applied. Check your compositor config matches
wine-sni-bridge(the WM_CLASS) - Some compositors need a reload after adding rules
White/blank icon on second minimize:
- This is usually resolved by icon caching. If persistent, restart the bridge:
systemctl --user restart wine-sni-bridge
Icons render with wrong colors (pink/magenta where red should be):
- Byte-order mismatch with your SNI host. Try
--byte-order network(the spec-literal big-endian mode). If that fixes it, please open an issue noting your host + version so we can track which consumers need it.
Black square on first minimize:
- Rare. Icon caching prevents this in most cases. If it happens, it resolves after the first dock/undock cycle
Bridge visible in compositor overview/expose:
- The window sets
_NET_WM_WINDOW_TYPE_UTILITYwhich most compositors exclude from overview - If your compositor still shows it, add compositor-specific rules to exclude utility windows
Focus still stolen after installing:
- Make sure
xembedsniproxyis not running:pkill -f xembedsniproxy - Disable the systemd service if it exists:
systemctl --user disable --now xembed-sni-proxy - Apply the Wine registry tweak above
- Icon caching eliminates the black square issue in practice, but a brief flash is theoretically possible on first-ever minimize after bridge start
- Some compositors may show the utility window in overview/expose modes
- Only supports
_NET_SYSTEM_TRAY_S0(primary screen) - Icon extraction uses polling (50ms X11 event loop) - negligible CPU impact
- Wine apps in the default
~/.wineprefix may show Wine's built-in tray bar alongside bridge icons (Proton/Steam games don't have this issue)
| xembedsniproxy | wine-sni-bridge | |
|---|---|---|
| Focus stealing | Yes (unmanaged X11 windows) | No (managed utility window) |
| Black square artifacts | Yes | No (icon caching) |
| Multi-app support | Yes | Yes (separate DBus connections) |
| Icon caching | No | Yes (per WM_CLASS) |
| Click forwarding | Yes | Yes |
| Survives bar restart | No | Yes (auto re-registers) |
| Dependencies | KDE Frameworks | Python + 3 packages |
| Wayland native | Partial | Yes (pure DBus SNI) |
This project was created to solve the xembedsniproxy focus-stealing problem on MangoWC (a dwl-based Wayland compositor). After extensive research confirmed that no existing solution works, we built wine-sni-bridge from scratch as a purpose-built X11-to-SNI bridge that respects Wayland compositor focus management.
MIT License. See LICENSE.
{ "modules-right": ["tray", ...], "tray": { "spacing": 10, "show-passive-items": true, "icon-size": 16 }, // Hide bridge from taskbar "wlr/taskbar": { "ignore-list": ["wine-sni-bridge"] } }