From 334eeefa769646e11f47748c92249cfd00b595cb Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Mon, 9 Feb 2026 19:17:24 -0800 Subject: [PATCH] nix flake update --- dotfiles/config/taffybar/Justfile | 39 ++++ dotfiles/config/taffybar/cabal.project | 4 +- .../config/taffybar/imalison-taffybar.cabal | 4 + dotfiles/config/taffybar/taffybar | 2 +- dotfiles/config/taffybar/taffybar.css | 40 ++-- dotfiles/config/taffybar/taffybar.hs | 173 +++++++++++++++++- nixos/flake.lock | 28 +-- 7 files changed, 248 insertions(+), 42 deletions(-) diff --git a/dotfiles/config/taffybar/Justfile b/dotfiles/config/taffybar/Justfile index 90fa6620..cb34cd6a 100644 --- a/dotfiles/config/taffybar/Justfile +++ b/dotfiles/config/taffybar/Justfile @@ -8,6 +8,31 @@ run: restart: @scripts/taffybar-restart +# Restart with no vendor CSS (taffybar's built-in stylesheet) loaded. +restart-no-vendor-css: + @TAFFYBAR_DISABLE_VENDOR_CSS=1 scripts/taffybar-restart + +# Restart with no user CSS loaded (ignores ~/.config/taffybar/taffybar.css and cssPaths). +restart-no-user-css: + @TAFFYBAR_DISABLE_USER_CSS=1 scripts/taffybar-restart + +# Restart with no taffybar CSS loaded at all (GTK theme only). +restart-empty-css: + @TAFFYBAR_DISABLE_VENDOR_CSS=1 TAFFYBAR_DISABLE_USER_CSS=1 scripts/taffybar-restart + +# Restart with an explicit colon-separated list of CSS files. +# Example: just restart-css-paths 'menu-debug.css:taffybar.css' +restart-css-paths paths: + @TAFFYBAR_CSS_PATHS='{{paths}}' scripts/taffybar-restart + +# Restart using only the minimal debug stylesheet in menu-debug.css. +restart-menu-debug: + @TAFFYBAR_CSS_PATHS='menu-debug.css' scripts/taffybar-restart + +# Restart using `scratch.css` (initially empty) for bisecting CSS leakage into menus. +restart-scratch: + @TAFFYBAR_CSS_PATHS='scratch.css' scripts/taffybar-restart + # Capture the reserved top area (taffybar) on the focused monitor. screenshot: @scripts/taffybar-screenshot @@ -16,6 +41,20 @@ screenshot: screenshot-all: @scripts/taffybar-screenshot-all +# Screenshot the entire focused monitor (useful when debugging popup menus). +screenshot-monitor: + @scripts/taffybar-screenshot-focused-monitor + +# Restart taffybar, pop up an SNI menu automatically (and a submenu if present), +# then screenshot the focused monitor. Optional argument selects the SNI item +# by substring match. +capture-sni-menu match="": + @if [[ -n "{{match}}" ]]; then scripts/taffybar-capture-sni-menu "{{match}}"; else scripts/taffybar-capture-sni-menu; fi + +# Trigger an SNI menu popup via DBus (taffybar.debug). +popup-sni-menu match="" submenu="1": + @scripts/taffybar-popup-sni-menu "{{match}}" "{{submenu}}" + # Crop the top bar out of an existing screenshot (defaults to 56px high). crop in out="": @if [[ -n "{{out}}" ]]; then scripts/taffybar-crop-bar "{{in}}" "{{out}}"; else scripts/taffybar-crop-bar "{{in}}"; fi diff --git a/dotfiles/config/taffybar/cabal.project b/dotfiles/config/taffybar/cabal.project index a066d60c..7490b0b9 100644 --- a/dotfiles/config/taffybar/cabal.project +++ b/dotfiles/config/taffybar/cabal.project @@ -1 +1,3 @@ -packages: . taffybar/ +packages: + . + taffybar/ diff --git a/dotfiles/config/taffybar/imalison-taffybar.cabal b/dotfiles/config/taffybar/imalison-taffybar.cabal index 3f08c58a..eed223e9 100644 --- a/dotfiles/config/taffybar/imalison-taffybar.cabal +++ b/dotfiles/config/taffybar/imalison-taffybar.cabal @@ -16,12 +16,16 @@ executable taffybar ghc-options: -threaded -rtsopts -with-rtsopts=-N ghc-prof-options: -fprof-auto build-depends: base + , dbus + , dbus-menu , X11 , bytestring , containers , filepath + , gi-gdk3 , gi-gtk3 , gi-gdkpixbuf + , gi-glib , gtk-sni-tray , gtk-strut , haskell-gi-base diff --git a/dotfiles/config/taffybar/taffybar b/dotfiles/config/taffybar/taffybar index 2c0ddcab..6ed126fe 160000 --- a/dotfiles/config/taffybar/taffybar +++ b/dotfiles/config/taffybar/taffybar @@ -1 +1 @@ -Subproject commit 2c0ddcab23a5898d1442282a27daf6695c1b6c24 +Subproject commit 6ed126fe6e26365393df42de2c659bb40ef34475 diff --git a/dotfiles/config/taffybar/taffybar.css b/dotfiles/config/taffybar/taffybar.css index 4891efbf..7c98db9b 100644 --- a/dotfiles/config/taffybar/taffybar.css +++ b/dotfiles/config/taffybar/taffybar.css @@ -9,13 +9,11 @@ /* Base typography + foreground color for the bar itself. * - * IMPORTANT: menus/popovers created by SNI items are separate GtkWindows but - * inherit style context from the "attach widget" chain. If we apply a blanket - * `color:` rule here, it will bleed into those menus and override the GTK - * theme, making submenu text unreadable. + * IMPORTANT: SNI menus are popup windows but inherit style context from the + * attach-widget chain. Avoid broad descendant selectors (especially `*`) here + * so we don't accidentally override menu theming. */ -.taffy-window .taffy-box :not(menu):not(menuitem):not(popover):not(window), -.taffy-window .taffy-box :not(menu):not(menuitem):not(popover):not(window) * { +.taffy-box { /* Most text should come from Iosevka Aile; icon glyphs (Font Awesome / Nerd Font PUA) should come from a Nerd Font family to avoid tiny fallback glyphs. */ font-family: "Iosevka Aile", "Iosevka Nerd Font", "Iosevka NF", "Noto Sans", sans-serif; @@ -71,17 +69,22 @@ opacity: 1; } -/* Make each widget's squircle background feel "solid": avoid GTK nodes and - labels painting their own backgrounds on top of `.outer-pad`. - Exclude menu/menuitem/popover so popup menus attached via menuAttachToWidget - aren't forced transparent. */ -.outer-pad :not(menu):not(menuitem):not(popover):not(window), -.inner-pad, -.inner-pad :not(menu):not(menuitem):not(popover):not(window), -.inner-pad :not(menu):not(menuitem):not(popover):not(window) *, -.contents, -.contents :not(menu):not(menuitem):not(popover):not(window), -.contents :not(menu):not(menuitem):not(popover):not(window) * { +/* Make each widget's squircle background feel "solid": avoid child widgets + painting their own backgrounds on top of `.outer-pad`. + * + * We intentionally avoid broad descendant selectors (especially `*`) here + * because SNI menus inherit style context via their attach-widget chain and + * those selectors can leak into menu windows, making them transparent. + */ +.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; } @@ -105,8 +108,7 @@ inset 0 0 0 1px @pill-border, 0 10px 24px @pill-shadow; } -.workspaces .outer-pad :not(menu):not(menuitem):not(popover):not(window), -.workspaces .outer-pad :not(menu):not(menuitem):not(popover):not(window) * { +.workspaces .outer-pad { color: @font-color; } diff --git a/dotfiles/config/taffybar/taffybar.hs b/dotfiles/config/taffybar/taffybar.hs index ac10bebd..01711e56 100644 --- a/dotfiles/config/taffybar/taffybar.hs +++ b/dotfiles/config/taffybar/taffybar.hs @@ -5,19 +5,32 @@ module Main (main) where import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Monad.Trans.Reader (ask) +import Control.Applicative ((<|>)) +import Control.Concurrent.MVar (readMVar) +import Control.Monad (void) import Data.Int (Int32) -import Data.List (nub) +import Data.IORef (newIORef, readIORef, writeIORef) +import Data.List (find, isInfixOf, nub) import qualified Data.Map as M -import Data.Maybe (catMaybes, fromMaybe, mapMaybe) +import Data.Maybe (catMaybes, fromMaybe, listToMaybe, mapMaybe) import Data.Text (Text) import qualified Data.Text as T -import qualified GI.GdkPixbuf.Objects.Pixbuf as Gdk +import DBus +import qualified DBus.Client as DBusClient +import Data.GI.Base (castTo) +import qualified DBusMenu +import qualified GI.Gdk as Gdk +import qualified GI.Gdk.Enums as GdkE +import qualified GI.GdkPixbuf.Objects.Pixbuf as GdkPixbuf import qualified GI.Gtk as Gtk +import qualified GI.GLib as GLib import Network.HostName (getHostName) +import System.Environment (lookupEnv) import System.Environment.XDG.BaseDir (getUserConfigFile) -import System.Log.Logger (Priority(WARNING), rootLoggerName, setLevel, updateGlobalLogger) +import System.Log.Logger (Priority(WARNING), logM, rootLoggerName, setLevel, updateGlobalLogger) import System.Taffybar (startTaffybar) -import System.Taffybar.Context (Backend (BackendWayland, BackendX11), TaffyIO, detectBackend) +import System.Taffybar.Context (Backend (BackendWayland, BackendX11), Context(..), TaffyIO, detectBackend) import System.Taffybar.DBus import System.Taffybar.DBus.Toggle import System.Taffybar.Hooks (withLogLevels) @@ -39,7 +52,151 @@ import qualified StatusNotifier.Tray as SNITray (MenuBackend (HaskellDBusMenu), import System.Taffybar.Widget.Util (buildContentsBox, buildIconLabelBox, loadPixbufByName, widgetSetClassGI) import qualified System.Taffybar.Widget.Workspaces as X11Workspaces import System.Taffybar.WindowIcon (pixBufFromColor) +import Data.Ratio ((%)) +import qualified StatusNotifier.Tray as SNITray (MenuBackend (HaskellDBusMenu), defaultTrayParams, trayMenuBackend, trayOverlayScale) +-- ** Debug: Programmatic SNI Menu Popup + +-- | DBus Properties.Get helper. +propsGet :: DBusClient.Client -> BusName -> ObjectPath -> String -> String -> IO (Maybe Variant) +propsGet client dest obj iface prop = do + let mc = + (methodCall obj "org.freedesktop.DBus.Properties" "Get") + { methodCallDestination = Just dest + , methodCallBody = [toVariant iface, toVariant prop] + } + result <- DBusClient.call client mc + case result of + Left _ -> pure Nothing + Right reply -> + case methodReturnBody reply of + [v] -> pure (fromVariant v) + _ -> pure Nothing + +-- | Return (bus name, object path, display string) for currently registered SNI +-- entries from the watcher. Prefer this over RegisteredStatusNotifierItems, +-- which is only bus names and doesn't include object paths. +getRegisteredSNIEntries :: DBusClient.Client -> IO [(BusName, ObjectPath, String)] +getRegisteredSNIEntries client = do + mv <- propsGet client watcherName watcherPath "org.kde.StatusNotifierWatcher" "RegisteredSNIEntries" + let raw :: [(String, String)] + raw = fromMaybe [] $ mv >>= fromVariant + pure + [ (busName_ bus, objectPath_ path, bus <> path) + | (bus, path) <- raw + ] + where + watcherName = busName_ "org.kde.StatusNotifierWatcher" + watcherPath = objectPath_ "/StatusNotifierWatcher" + +getSNIItemMenuPath :: DBusClient.Client -> BusName -> ObjectPath -> IO (Maybe ObjectPath) +getSNIItemMenuPath client itemBus itemPath = do + mv <- propsGet client itemBus itemPath "org.kde.StatusNotifierItem" "Menu" + pure $ mv >>= fromVariant + +-- | Pop up the first submenu we can find under a menu. +popupFirstSubmenu :: Gtk.Menu -> IO () +popupFirstSubmenu rootMenu = do + children <- Gtk.containerGetChildren rootMenu + let go [] = pure () + go (w:ws) = do + mi <- castTo Gtk.MenuItem w + case mi of + Nothing -> go ws + Just menuItem -> do + smw <- Gtk.menuItemGetSubmenu menuItem + case smw of + Nothing -> go ws + Just sw -> do + sm <- castTo Gtk.Menu sw + case sm of + Nothing -> go ws + Just submenu -> do + Gtk.widgetShowAll submenu + Gtk.menuPopupAtWidget + submenu + menuItem + GdkE.GravityNorthEast + GdkE.GravityNorthWest + Nothing + go children + +-- | When enabled by env vars, pop up an SNI menu (and a submenu if present) so +-- we can screenshot it in automation loops. +-- +-- Env vars: +-- - TAFFYBAR_DEBUG_POPUP_SNI_MENU=1 to enable +-- - TAFFYBAR_DEBUG_SNI_MATCH= to choose an item (matches the raw item id) +debugPopupSNIMenuHook :: TaffyIO () +debugPopupSNIMenuHook = do + enabled <- liftIO $ lookupEnv "TAFFYBAR_DEBUG_POPUP_SNI_MENU" + case enabled of + Nothing -> pure () + Just _ -> do + match <- liftIO $ fromMaybe "" <$> lookupEnv "TAFFYBAR_DEBUG_SNI_MATCH" + ctx <- ask + -- Poll until the tray watcher has registered items; on startup this can + -- take a few seconds. + liftIO $ do + triesRef <- newIORef (40 :: Int) -- ~10s at 250ms + void $ GLib.timeoutAdd GLib.PRIORITY_LOW 250 $ do + let client = sessionDBusClient ctx + entries <- getRegisteredSNIEntries client + remaining <- readIORef triesRef + if not (null entries) + then do + logM "TaffybarDebug" WARNING $ + "SNI debug popup: registered entries=" <> show (length entries) + let chosen = + case match of + "" -> listToMaybe entries + _ -> find (\(_, _, disp) -> isInfixOf match disp) entries <|> listToMaybe entries + case chosen of + Nothing -> do + logM "TaffybarDebug" WARNING "SNI debug popup: no suitable item found." + pure False + Just (itemBus, itemPath, disp) -> do + mMenuPath <- getSNIItemMenuPath client itemBus itemPath + case mMenuPath of + Nothing -> + do + logM "TaffybarDebug" WARNING $ + "SNI debug popup: entry has no Menu property: " <> disp + pure False + Just menuPath -> do + logM "TaffybarDebug" WARNING $ + "SNI debug popup: popping menu for " <> disp <> " menu=" <> show menuPath + gtkMenu <- DBusMenu.buildMenu client itemBus menuPath + -- Attach to the bar window if possible to keep CSS parent chain realistic. + wins <- readMVar (existingWindows ctx) + case wins of + ((_, win):_) -> Gtk.menuAttachToWidget gtkMenu win Nothing + _ -> pure () + _ <- Gtk.onWidgetHide gtkMenu $ + void $ GLib.idleAdd GLib.PRIORITY_LOW $ do + Gtk.widgetDestroy gtkMenu + pure False + + Gtk.widgetShowAll gtkMenu + case wins of + ((_, win):_) -> + Gtk.menuPopupAtWidget + gtkMenu + win + GdkE.GravitySouthWest + GdkE.GravityNorthWest + Nothing + _ -> Gtk.menuPopupAtPointer gtkMenu Nothing + + popupFirstSubmenu gtkMenu + pure False + else if remaining <= 0 + then do + logM "TaffybarDebug" WARNING "SNI debug popup: timed out waiting for tray items." + pure False + else do + writeIORef triesRef (remaining - 1) + pure True -- | Wrap the widget in a "TaffyBox" (via 'buildContentsBox') and add a CSS class. decorateWithClassAndBox :: MonadIO m => Text -> Gtk.Widget -> m Gtk.Widget decorateWithClassAndBox klass widget = do @@ -132,7 +289,7 @@ isPathCandidate name = T.isInfixOf "/" name || any (`T.isSuffixOf` name) [".png", ".svg", ".xpm"] -hyprlandIconFromCandidate :: Int32 -> Text -> TaffyIO (Maybe Gdk.Pixbuf) +hyprlandIconFromCandidate :: Int32 -> Text -> TaffyIO (Maybe GdkPixbuf.Pixbuf) hyprlandIconFromCandidate size name | isPathCandidate name = liftIO $ getPixbufFromFilePath (T.unpack name) @@ -147,7 +304,7 @@ hyprlandManualIconGetter = foldl maybeTCombine (return Nothing) $ map (hyprlandIconFromCandidate size) (hyprlandIconCandidates windowData) -fallbackIconPixbuf :: Int32 -> TaffyIO (Maybe Gdk.Pixbuf) +fallbackIconPixbuf :: Int32 -> TaffyIO (Maybe GdkPixbuf.Pixbuf) fallbackIconPixbuf size = do let fallbackNames = [ "application-x-executable" @@ -365,6 +522,7 @@ mkSimpleTaffyConfig hostName backend cssFiles = , barHeight = ScreenRatio $ 1 / 33 , cssPaths = cssFiles , centerWidgets = [sniTrayWidget] + , startupHook = debugPopupSNIMenuHook } -- ** Entry Point @@ -382,4 +540,5 @@ main = do withLogServer $ withLogLevels $ withToggleServer $ + withDebugServer $ toTaffybarConfig simpleTaffyConfig diff --git a/nixos/flake.lock b/nixos/flake.lock index b4a3ca88..aa07bc62 100644 --- a/nixos/flake.lock +++ b/nixos/flake.lock @@ -116,11 +116,11 @@ ] }, "locked": { - "lastModified": 1770491193, - "narHash": "sha256-zdnWeXmPZT8BpBo52s4oansT1Rq0SNzksXKpEcMc5lE=", + "lastModified": 1770687088, + "narHash": "sha256-WM353TQnhVCbgMGUqoPIsLEdF8HHtMo/dFryzBSEswI=", "owner": "sadjow", "repo": "claude-code-nix", - "rev": "f68a2683e812d1e4f9a022ff3e0206d46347d019", + "rev": "5fb242d2c746009f9fa3b63e9f346e8ea64328ea", "type": "github" }, "original": { @@ -579,11 +579,11 @@ ] }, "locked": { - "lastModified": 1770613567, - "narHash": "sha256-t0wC7BrU7YwqvZcZpCB50dk37/bVcpIoE/qME/kw8PA=", + "lastModified": 1770693358, + "narHash": "sha256-rqUwaRsxMd9OOkDUxAlQlGfohlV8ZK4s2j5qZ9dJQVI=", "owner": "taffybar", "repo": "gtk-sni-tray", - "rev": "fe5a75bc20228544217f96a1e6895be12e6198c7", + "rev": "a7a72c72d719bb3a6e4c36974671f2f6581f55ff", "type": "github" }, "original": { @@ -1134,11 +1134,11 @@ "nixpkgs-regression": "nixpkgs-regression" }, "locked": { - "lastModified": 1770591402, - "narHash": "sha256-7qxOxkj11ExOhpxcsFK3O8Ktegkgw0SCq9nfoAOvjxM=", + "lastModified": 1770673695, + "narHash": "sha256-7lLVzlUSkAPo9LDe0/M9CdAh6HHDfzq8pfn98PXoKu0=", "owner": "NixOS", "repo": "nix", - "rev": "e4ce788f9d8de1bc5e58002d01088cd71c6703d0", + "rev": "845d951682008a009a9727437c9d913403053d06", "type": "github" }, "original": { @@ -1585,11 +1585,11 @@ ] }, "locked": { - "lastModified": 1770410595, + "lastModified": 1770668379, "narHash": "sha256-sqDK58NI/+tfIBd5gzYKXMhMv3CNtFBtnR958KqQhlk=", "ref": "refs/heads/master", - "rev": "01dc1d61477037e19ebe7a58c71581020bf7eea0", - "revCount": 145, + "rev": "f65d0110607dba433ea746d445b1b66d5ead6f6c", + "revCount": 149, "type": "git", "url": "ssh://gitea@dev.railbird.ai:1123/railbird/secrets-flake.git" }, @@ -1735,8 +1735,8 @@ ] }, "locked": { - "lastModified": 1770661157, - "narHash": "sha256-Le3mnGVNLrD66/CV5jqMeSSQsMMMEaFrFbYzl3KCD3A=", + "lastModified": 1770685149, + "narHash": "sha256-3PY90Kt/8z4E0TNHBQuEzqaTfrRfUTK5HTfoIM3Z7W4=", "path": "/home/imalison/dotfiles/dotfiles/config/taffybar/taffybar", "type": "path" },