taffybar: refine tray behavior and add SNI menu debug tooling
This commit is contained in:
85
dotfiles/config/taffybar/MENU_CSS_DEBUG.md
Normal file
85
dotfiles/config/taffybar/MENU_CSS_DEBUG.md
Normal 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.
|
||||
|
||||
35
dotfiles/config/taffybar/menu-debug.css
Normal file
35
dotfiles/config/taffybar/menu-debug.css
Normal 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;
|
||||
}
|
||||
|
||||
72
dotfiles/config/taffybar/scratch.css
Normal file
72
dotfiles/config/taffybar/scratch.css
Normal 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;
|
||||
}
|
||||
19
dotfiles/config/taffybar/scripts/taffybar-capture-sni-menu
Executable file
19
dotfiles/config/taffybar/scripts/taffybar-capture-sni-menu
Executable 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"
|
||||
23
dotfiles/config/taffybar/scripts/taffybar-popup-sni-menu
Executable file
23
dotfiles/config/taffybar/scripts/taffybar-popup-sni-menu
Executable 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
|
||||
32
dotfiles/config/taffybar/scripts/taffybar-screenshot-focused-monitor
Executable file
32
dotfiles/config/taffybar/scripts/taffybar-screenshot-focused-monitor
Executable 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"
|
||||
|
||||
@@ -331,11 +331,17 @@ asusWidget =
|
||||
|
||||
screenLockWidget :: TaffyIO Gtk.Widget
|
||||
screenLockWidget =
|
||||
decorateWithClassAndBoxM "screen-lock" ScreenLock.screenLockNew
|
||||
decorateWithClassAndBoxM "screen-lock" $
|
||||
ScreenLock.screenLockNewWithConfig
|
||||
ScreenLock.defaultScreenLockConfig
|
||||
{ ScreenLock.screenLockIcon = T.pack "\xF023" <> " Lock" }
|
||||
|
||||
wlsunsetWidget :: TaffyIO Gtk.Widget
|
||||
wlsunsetWidget =
|
||||
decorateWithClassAndBoxM "wlsunset" Wlsunset.wlsunsetNew
|
||||
decorateWithClassAndBoxM "wlsunset" $
|
||||
Wlsunset.wlsunsetNewWithConfig
|
||||
Wlsunset.defaultWlsunsetWidgetConfig
|
||||
{ Wlsunset.wlsunsetWidgetIcon = T.pack "\xF0599" <> " Sun" }
|
||||
|
||||
sniTrayWidget :: TaffyIO Gtk.Widget
|
||||
sniTrayWidget =
|
||||
@@ -354,20 +360,19 @@ startWidgetsForBackend backend =
|
||||
|
||||
endWidgetsForHost :: String -> Backend -> [TaffyIO Gtk.Widget]
|
||||
endWidgetsForHost hostName backend =
|
||||
let tray = sniTrayWidget
|
||||
baseEndWidgets = [clockWidget, audioWidget, diskUsageWidget, networkWidget, screenLockWidget, wlsunsetWidget, mprisWidget]
|
||||
let baseEndWidgets = [audioWidget, diskUsageWidget, networkWidget, screenLockWidget, wlsunsetWidget, mprisWidget, sniTrayWidget]
|
||||
laptopEndWidgets =
|
||||
[ batteryWidget
|
||||
, asusWidget
|
||||
, clockWidget
|
||||
, audioWidget
|
||||
, diskUsageWidget
|
||||
, backlightWidget
|
||||
, networkWidget
|
||||
, screenLockWidget
|
||||
, wlsunsetWidget
|
||||
, mprisWidget
|
||||
]
|
||||
[ batteryWidget
|
||||
, asusWidget
|
||||
, sniTrayWidget
|
||||
, audioWidget
|
||||
, diskUsageWidget
|
||||
, backlightWidget
|
||||
, networkWidget
|
||||
, screenLockWidget
|
||||
, wlsunsetWidget
|
||||
, mprisWidget
|
||||
]
|
||||
in if hostName `elem` laptopHosts
|
||||
then laptopEndWidgets
|
||||
else baseEndWidgets
|
||||
@@ -382,7 +387,7 @@ mkSimpleTaffyConfig hostName backend cssFiles =
|
||||
, barPadding = 4
|
||||
, barHeight = ScreenRatio $ 1 / 33
|
||||
, cssPaths = cssFiles
|
||||
, centerWidgets = [sniTrayWidget]
|
||||
, centerWidgets = [clockWidget]
|
||||
}
|
||||
|
||||
-- ** Entry Point
|
||||
|
||||
Reference in New Issue
Block a user