taffybar: refine tray behavior and add SNI menu debug tooling

This commit is contained in:
2026-02-10 22:42:47 -08:00
committed by Kat Huang
parent 5bfb1a5884
commit 1c461048d9
9 changed files with 307 additions and 54 deletions

View File

@@ -0,0 +1,85 @@
# Taffybar SNI Menu CSS Debug Notes
This documents the root cause and debugging workflow for the "SNI tray submenu text is unreadable" and "menus become transparent" issues.
## Background
Taffybar renders StatusNotifierItem (SNI) menus using GTK menus created from the application's DBusMenu (`StatusNotifierItem.Menu`).
Key GTK behavior that matters here:
- SNI menus are shown as separate popup windows (GTK creates `window.popup` / menu surfaces).
- Those popup windows can *inherit style context* from the widget chain they are attached to (`Gtk.menuAttachToWidget`).
- This means bar CSS can "leak" into menu windows even though the menus are not children of the bar in the widget hierarchy.
## What We Observed
1. **Blanket descendant rules leak into menus**
- Rules like `.taffy-window .taffy-box * { color: ... }` can override the theme foreground color for menu text.
- Rules like `... * { background-color: transparent; }` can make menu surfaces transparent (especially submenus).
2. **The main culprit was the "pill solid" background reset**
- The bar used a broad "reset" to make widget pills look solid by clearing backgrounds on many descendants.
- Even with attempted exclusions like `:not(menu):not(menuitem):not(popover):not(window)`, the selector was still too broad in practice and resulted in transparent menus.
3. **Wayland popup restrictions complicate programmatic reproduction**
- Under Wayland, GTK may refuse to show real `GtkMenu` popups without a triggering input event (serial), producing warnings like:
- `no trigger event for menu popup`
- This makes "programmatically popup a real menu" unreliable without simulating input.
## How We Isolated It
We created a minimal stylesheet and reintroduced rules incrementally:
- `scratch.css` started essentially empty, then we added bar/pill styling only (no resets).
- Menus were fine.
- Adding the broad background reset reproduced the problem immediately.
- Replacing the broad reset with a narrow, targeted reset fixed the problem.
## Working Fix
In `scratch.css` the "pill solid" reset was replaced with a *safe reset* that does not use broad `*` descendant selectors:
- Only clear backgrounds on common bar widgets: `label`, `image`, `button` under `.outer-pad/.inner-pad/.contents`.
- Avoid `... * { ... }` rules in the bar where possible.
This preserved pill visuals while keeping SNI menus theme-driven and opaque.
## Practical Guidance (CSS)
Avoid in bar CSS:
- Broad descendants: `.something * { ... }`
- Broad transparency resets: `background-color: transparent` applied to large subtrees
Prefer:
- Targeted selectors (specific widget types/classes)
- Rules scoped to the bar window/container classes (`.taffy-box`, `.outer-pad`, `.inner-pad`) without `*`
- If you must use descendants, be extremely conservative and re-test SNI menus after changes
## Debug/Automation Helpers
This repo includes a DBus debug server (running inside taffybar) and scripts to help automate style debugging:
- DBus name: `taffybar.debug`
- Object path: `/taffybar/debug`
- Interface: `taffybar.debug`
Useful scripts / just targets:
- `just restart-scratch` runs taffybar with `TAFFYBAR_CSS_PATHS=scratch.css`
- `just restart-menu-debug` runs with `TAFFYBAR_CSS_PATHS=menu-debug.css`
- `just popup-sni-menu` attempts to popup menus (may be blocked on Wayland)
- `scripts/taffybar-screenshot-focused-monitor` takes a monitor screenshot with `grim`
Notes:
- Programmatic menu popups are not always possible on Wayland without a trigger event.
- If needed, prefer "preview window" or input simulation for fully automated capture.
## Next Steps
- Migrate the "safe reset" approach back into `taffybar.css` (or replace `taffybar.css` with the final `scratch.css` contents once complete).
- Keep `scratch.css` as a known-good bisect harness for future menu/theme regressions.

View File

@@ -0,0 +1,35 @@
/* Minimal, high-contrast menu styling for debugging CSS application. */
@define-color dbg_menu_bg #ffffcc;
@define-color dbg_menu_fg #000000;
@define-color dbg_menu_border #ff0000;
menu,
menuitem,
window.popup,
window.menu {
background-color: @dbg_menu_bg;
color: @dbg_menu_fg;
border: 2px solid @dbg_menu_border;
}
menu label,
menuitem label,
window.popup label,
window.menu label {
color: @dbg_menu_fg;
}
.dbusmenu-menu,
.dbusmenu-submenu,
.dbusmenu-item {
background-color: @dbg_menu_bg;
color: @dbg_menu_fg;
}
.dbusmenu-menu label,
.dbusmenu-submenu label,
.dbusmenu-item label {
color: @dbg_menu_fg;
}

View File

@@ -0,0 +1,72 @@
/* Scratch stylesheet for isolating menu/CSS leakage issues.
*
* Start empty, then add rules from `taffybar.css` incrementally and test SNI
* menus after each change.
*/
/* Step 1: only style the bar container and widget pills.
*
* Intentionally do NOT add any broad `background-color: transparent` or
* `color:` rules yet, since those are the common sources of menu bleed-through.
*/
@import url("theme.css");
/* Typography: apply only to the bar container. We avoid `... * { ... }` rules
* because descendant selectors are what tend to leak into attached SNI menus.
* (Inheritance still happens for bar children, but we aren't targeting the
* entire widget subtree explicitly.)
*/
.taffy-box {
font-family: "Iosevka Aile", "Iosevka Nerd Font", "Iosevka NF", "Noto Sans", sans-serif;
font-size: 11pt;
font-weight: 600;
color: @font-color;
text-shadow: none;
}
.taffy-box {
border-width: 0px;
padding: 0px;
margin: 0px;
border-radius: 6px;
box-shadow: none;
background-color: @bar-background;
background-image: none;
}
.outer-pad {
background-color: @pill-background;
border: 0px;
border-radius: 6px;
margin: 4px 6px;
box-shadow:
inset 0 1px 0 @pill-highlight,
inset 0 0 0 1px @pill-border,
0 10px 24px @pill-shadow;
}
.inner-pad {
padding: 2px 10px;
border-radius: 4px;
}
.contents {
padding: 0px;
opacity: 1;
}
/* Safe "pill solid" reset:
* Only clear backgrounds on common bar widgets, without using broad descendant
* selectors that can bleed into SNI popup menus via the attach-widget chain.
*/
.outer-pad label,
.outer-pad image,
.outer-pad button,
.inner-pad label,
.inner-pad image,
.inner-pad button,
.contents label,
.contents image,
.contents button {
background-color: transparent;
}

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
match="${1:-}"
wait_secs="${TAFFYBAR_DEBUG_WAIT_SECS:-1}"
"$root/scripts/taffybar-restart" >/dev/null
if [[ -n "${match}" ]]; then
"$root/scripts/taffybar-popup-sni-menu" "$match" 1
else
"$root/scripts/taffybar-popup-sni-menu" "" 1
fi
sleep "$wait_secs"
"$root/scripts/taffybar-screenshot-focused-monitor"

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
match="${1:-}"
submenu="${2:-1}" # 1 = also pop up first submenu
# Put cursor somewhere predictable in case the compositor/theme uses pointer hints.
hyprctl dispatch movecursor 10 10 >/dev/null 2>&1 || true
# Wait for the debug bus name to appear (taffybar startup can take a moment).
for _ in $(seq 1 50); do
if busctl --user list 2>/dev/null | rg -Fq 'taffybar.debug'; then
break
fi
sleep 0.1
done
if ! busctl --user list 2>/dev/null | rg -Fq 'taffybar.debug'; then
echo "taffybar.debug DBus name not present (is taffybar running with withDebugServer?)." >&2
exit 1
fi
busctl --user call taffybar.debug /taffybar/debug taffybar.debug PopupSniMenu sb "$match" "$submenu" >/dev/null

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
hyprctl_cmd=(hyprctl)
# Hyprland can restart and change the instance signature, leaving old shells with
# a stale HYPRLAND_INSTANCE_SIGNATURE. Detect the live instance and use it.
if ! hyprctl monitors -j >/dev/null 2>&1; then
if [[ -n "${WAYLAND_DISPLAY:-}" ]]; then
inst="$(hyprctl instances -j | jq -r --arg sock "$WAYLAND_DISPLAY" '.[] | select(.wl_socket == $sock) | .instance' | head -n1)"
else
inst="$(hyprctl instances -j | jq -r '.[0].instance // empty')"
fi
if [[ -n "${inst:-}" ]]; then
hyprctl_cmd=(hyprctl --instance "$inst")
fi
fi
monitors="$("${hyprctl_cmd[@]}" monitors -j)"
name="$(jq -r '.[] | select(.focused) | .name' <<<"$monitors")"
if [[ -z "${name:-}" || "${name}" == "null" ]]; then
echo "No focused monitor found." >&2
exit 1
fi
# Include nanoseconds so consecutive screenshots don't overwrite each other.
out="/tmp/monitor-${name}-$(date +%Y%m%d-%H%M%S-%N).png"
grim -o "$name" "$out"
echo "$out"

View File

@@ -331,11 +331,17 @@ asusWidget =
screenLockWidget :: TaffyIO Gtk.Widget screenLockWidget :: TaffyIO Gtk.Widget
screenLockWidget = screenLockWidget =
decorateWithClassAndBoxM "screen-lock" ScreenLock.screenLockNew decorateWithClassAndBoxM "screen-lock" $
ScreenLock.screenLockNewWithConfig
ScreenLock.defaultScreenLockConfig
{ ScreenLock.screenLockIcon = T.pack "\xF023" <> " Lock" }
wlsunsetWidget :: TaffyIO Gtk.Widget wlsunsetWidget :: TaffyIO Gtk.Widget
wlsunsetWidget = wlsunsetWidget =
decorateWithClassAndBoxM "wlsunset" Wlsunset.wlsunsetNew decorateWithClassAndBoxM "wlsunset" $
Wlsunset.wlsunsetNewWithConfig
Wlsunset.defaultWlsunsetWidgetConfig
{ Wlsunset.wlsunsetWidgetIcon = T.pack "\xF0599" <> " Sun" }
sniTrayWidget :: TaffyIO Gtk.Widget sniTrayWidget :: TaffyIO Gtk.Widget
sniTrayWidget = sniTrayWidget =
@@ -354,12 +360,11 @@ startWidgetsForBackend backend =
endWidgetsForHost :: String -> Backend -> [TaffyIO Gtk.Widget] endWidgetsForHost :: String -> Backend -> [TaffyIO Gtk.Widget]
endWidgetsForHost hostName backend = endWidgetsForHost hostName backend =
let tray = sniTrayWidget let baseEndWidgets = [audioWidget, diskUsageWidget, networkWidget, screenLockWidget, wlsunsetWidget, mprisWidget, sniTrayWidget]
baseEndWidgets = [clockWidget, audioWidget, diskUsageWidget, networkWidget, screenLockWidget, wlsunsetWidget, mprisWidget]
laptopEndWidgets = laptopEndWidgets =
[ batteryWidget [ batteryWidget
, asusWidget , asusWidget
, clockWidget , sniTrayWidget
, audioWidget , audioWidget
, diskUsageWidget , diskUsageWidget
, backlightWidget , backlightWidget
@@ -382,7 +387,7 @@ mkSimpleTaffyConfig hostName backend cssFiles =
, barPadding = 4 , barPadding = 4
, barHeight = ScreenRatio $ 1 / 33 , barHeight = ScreenRatio $ 1 / 33
, cssPaths = cssFiles , cssPaths = cssFiles
, centerWidgets = [sniTrayWidget] , centerWidgets = [clockWidget]
} }
-- ** Entry Point -- ** Entry Point

View File

@@ -17,8 +17,9 @@ makeEnable config "myModules.notifications-tray-icon" true {
mkService = description: execStart: { mkService = description: execStart: {
Unit = { Unit = {
Description = description; Description = description;
After = [ "graphical-session-pre.target" "tray.target" ]; After = [ "graphical-session.target" "tray.target" ];
PartOf = [ "graphical-session.target" ]; PartOf = [ "graphical-session.target" ];
Requires = [ "tray.target" ];
}; };
Service = { Service = {
ExecStart = execStart; ExecStart = execStart;

View File

@@ -1,15 +1,8 @@
{ config, inputs, lib, pkgs, makeEnable, ... }: { config, inputs, pkgs, makeEnable, ... }:
makeEnable config "myModules.sni" true { makeEnable config "myModules.sni" true {
home-manager.sharedModules = [ home-manager.sharedModules = [
{ {
systemd.user.services = systemd.user.services.kanshi-sni = {
let
wantGraphicalPre = {
Install.WantedBy = lib.mkAfter [ "graphical-session-pre.target" ];
};
in
{
kanshi-sni = {
Unit = { Unit = {
Description = "kanshi-sni tray app"; Description = "kanshi-sni tray app";
After = [ "graphical-session.target" "tray.target" ]; After = [ "graphical-session.target" "tray.target" ];
@@ -25,18 +18,8 @@ makeEnable config "myModules.sni" true {
WantedBy = [ "graphical-session.target" ]; WantedBy = [ "graphical-session.target" ];
}; };
}; };
blueman-applet = wantGraphicalPre;
kdeconnect = wantGraphicalPre;
kdeconnect-indicator = wantGraphicalPre;
network-manager-applet = wantGraphicalPre;
pasystray = wantGraphicalPre;
udiskie = wantGraphicalPre;
flameshot = wantGraphicalPre;
};
services.blueman-applet = { services.blueman-applet.enable = true;
enable = true;
};
services.kdeconnect = { services.kdeconnect = {
enable = true; enable = true;
@@ -59,9 +42,7 @@ makeEnable config "myModules.sni" true {
services.pasystray.enable = true; services.pasystray.enable = true;
services.flameshot = { services.flameshot.enable = true;
enable = true;
};
} }
]; ];
} }