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 :: 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,20 +360,19 @@ 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
|
||||||
, networkWidget
|
, networkWidget
|
||||||
, screenLockWidget
|
, screenLockWidget
|
||||||
, wlsunsetWidget
|
, wlsunsetWidget
|
||||||
, mprisWidget
|
, mprisWidget
|
||||||
]
|
]
|
||||||
in if hostName `elem` laptopHosts
|
in if hostName `elem` laptopHosts
|
||||||
then laptopEndWidgets
|
then laptopEndWidgets
|
||||||
else baseEndWidgets
|
else baseEndWidgets
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,43 +1,26 @@
|
|||||||
{ 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
|
Unit = {
|
||||||
wantGraphicalPre = {
|
Description = "kanshi-sni tray app";
|
||||||
Install.WantedBy = lib.mkAfter [ "graphical-session-pre.target" ];
|
After = [ "graphical-session.target" "tray.target" ];
|
||||||
};
|
PartOf = [ "graphical-session.target" ];
|
||||||
in
|
Requires = [ "tray.target" ];
|
||||||
{
|
};
|
||||||
kanshi-sni = {
|
Service = {
|
||||||
Unit = {
|
ExecStart = "${inputs.kanshi-sni.packages.${pkgs.stdenv.hostPlatform.system}.default}/bin/kanshi-sni";
|
||||||
Description = "kanshi-sni tray app";
|
Restart = "always";
|
||||||
After = [ "graphical-session.target" "tray.target" ];
|
RestartSec = 3;
|
||||||
PartOf = [ "graphical-session.target" ];
|
};
|
||||||
Requires = [ "tray.target" ];
|
Install = {
|
||||||
};
|
WantedBy = [ "graphical-session.target" ];
|
||||||
Service = {
|
|
||||||
ExecStart = "${inputs.kanshi-sni.packages.${pkgs.stdenv.hostPlatform.system}.default}/bin/kanshi-sni";
|
|
||||||
Restart = "always";
|
|
||||||
RestartSec = 3;
|
|
||||||
};
|
|
||||||
Install = {
|
|
||||||
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 = {
|
|
||||||
enable = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
services.blueman-applet.enable = true;
|
||||||
|
|
||||||
services.kdeconnect = {
|
services.kdeconnect = {
|
||||||
enable = true;
|
enable = true;
|
||||||
indicator = true;
|
indicator = 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;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user