From 1c461048d9c1b4bd34a1c3622ed4c4b4f6810b25 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Tue, 10 Feb 2026 22:42:47 -0800 Subject: [PATCH] taffybar: refine tray behavior and add SNI menu debug tooling --- dotfiles/config/taffybar/MENU_CSS_DEBUG.md | 85 +++++++++++++++++++ dotfiles/config/taffybar/menu-debug.css | 35 ++++++++ dotfiles/config/taffybar/scratch.css | 72 ++++++++++++++++ .../scripts/taffybar-capture-sni-menu | 19 +++++ .../taffybar/scripts/taffybar-popup-sni-menu | 23 +++++ .../taffybar-screenshot-focused-monitor | 32 +++++++ dotfiles/config/taffybar/taffybar.hs | 37 ++++---- nixos/notifications-tray-icon.nix | 3 +- nixos/sni.nix | 55 ++++-------- 9 files changed, 307 insertions(+), 54 deletions(-) create mode 100644 dotfiles/config/taffybar/MENU_CSS_DEBUG.md create mode 100644 dotfiles/config/taffybar/menu-debug.css create mode 100644 dotfiles/config/taffybar/scratch.css create mode 100755 dotfiles/config/taffybar/scripts/taffybar-capture-sni-menu create mode 100755 dotfiles/config/taffybar/scripts/taffybar-popup-sni-menu create mode 100755 dotfiles/config/taffybar/scripts/taffybar-screenshot-focused-monitor diff --git a/dotfiles/config/taffybar/MENU_CSS_DEBUG.md b/dotfiles/config/taffybar/MENU_CSS_DEBUG.md new file mode 100644 index 00000000..e2014f8d --- /dev/null +++ b/dotfiles/config/taffybar/MENU_CSS_DEBUG.md @@ -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. + diff --git a/dotfiles/config/taffybar/menu-debug.css b/dotfiles/config/taffybar/menu-debug.css new file mode 100644 index 00000000..32a9a179 --- /dev/null +++ b/dotfiles/config/taffybar/menu-debug.css @@ -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; +} + diff --git a/dotfiles/config/taffybar/scratch.css b/dotfiles/config/taffybar/scratch.css new file mode 100644 index 00000000..5faa9081 --- /dev/null +++ b/dotfiles/config/taffybar/scratch.css @@ -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; +} diff --git a/dotfiles/config/taffybar/scripts/taffybar-capture-sni-menu b/dotfiles/config/taffybar/scripts/taffybar-capture-sni-menu new file mode 100755 index 00000000..bfbd9472 --- /dev/null +++ b/dotfiles/config/taffybar/scripts/taffybar-capture-sni-menu @@ -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" diff --git a/dotfiles/config/taffybar/scripts/taffybar-popup-sni-menu b/dotfiles/config/taffybar/scripts/taffybar-popup-sni-menu new file mode 100755 index 00000000..db5399c6 --- /dev/null +++ b/dotfiles/config/taffybar/scripts/taffybar-popup-sni-menu @@ -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 diff --git a/dotfiles/config/taffybar/scripts/taffybar-screenshot-focused-monitor b/dotfiles/config/taffybar/scripts/taffybar-screenshot-focused-monitor new file mode 100755 index 00000000..7b7c1a1b --- /dev/null +++ b/dotfiles/config/taffybar/scripts/taffybar-screenshot-focused-monitor @@ -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" + diff --git a/dotfiles/config/taffybar/taffybar.hs b/dotfiles/config/taffybar/taffybar.hs index f111837e..026783ed 100644 --- a/dotfiles/config/taffybar/taffybar.hs +++ b/dotfiles/config/taffybar/taffybar.hs @@ -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 diff --git a/nixos/notifications-tray-icon.nix b/nixos/notifications-tray-icon.nix index d6c9a17a..90a12757 100644 --- a/nixos/notifications-tray-icon.nix +++ b/nixos/notifications-tray-icon.nix @@ -17,8 +17,9 @@ makeEnable config "myModules.notifications-tray-icon" true { mkService = description: execStart: { Unit = { Description = description; - After = [ "graphical-session-pre.target" "tray.target" ]; + After = [ "graphical-session.target" "tray.target" ]; PartOf = [ "graphical-session.target" ]; + Requires = [ "tray.target" ]; }; Service = { ExecStart = execStart; diff --git a/nixos/sni.nix b/nixos/sni.nix index 174db613..34d22f7d 100644 --- a/nixos/sni.nix +++ b/nixos/sni.nix @@ -1,43 +1,26 @@ -{ config, inputs, lib, pkgs, makeEnable, ... }: +{ config, inputs, pkgs, makeEnable, ... }: makeEnable config "myModules.sni" true { home-manager.sharedModules = [ { - systemd.user.services = - let - wantGraphicalPre = { - Install.WantedBy = lib.mkAfter [ "graphical-session-pre.target" ]; - }; - in - { - kanshi-sni = { - Unit = { - Description = "kanshi-sni tray app"; - After = [ "graphical-session.target" "tray.target" ]; - PartOf = [ "graphical-session.target" ]; - Requires = [ "tray.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; + systemd.user.services.kanshi-sni = { + Unit = { + Description = "kanshi-sni tray app"; + After = [ "graphical-session.target" "tray.target" ]; + PartOf = [ "graphical-session.target" ]; + Requires = [ "tray.target" ]; + }; + Service = { + ExecStart = "${inputs.kanshi-sni.packages.${pkgs.stdenv.hostPlatform.system}.default}/bin/kanshi-sni"; + Restart = "always"; + RestartSec = 3; + }; + Install = { + WantedBy = [ "graphical-session.target" ]; }; - - services.blueman-applet = { - enable = true; }; + services.blueman-applet.enable = true; + services.kdeconnect = { enable = true; indicator = true; @@ -59,9 +42,7 @@ makeEnable config "myModules.sni" true { services.pasystray.enable = true; - services.flameshot = { - enable = true; - }; + services.flameshot.enable = true; } ]; }