nix flake update

This commit is contained in:
2026-02-09 19:17:24 -08:00
committed by Kat Huang
parent 3a23ad2960
commit 334eeefa76
7 changed files with 248 additions and 42 deletions

View File

@@ -8,6 +8,31 @@ run:
restart: restart:
@scripts/taffybar-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. # Capture the reserved top area (taffybar) on the focused monitor.
screenshot: screenshot:
@scripts/taffybar-screenshot @scripts/taffybar-screenshot
@@ -16,6 +41,20 @@ screenshot:
screenshot-all: screenshot-all:
@scripts/taffybar-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 the top bar out of an existing screenshot (defaults to 56px high).
crop in out="": crop in out="":
@if [[ -n "{{out}}" ]]; then scripts/taffybar-crop-bar "{{in}}" "{{out}}"; else scripts/taffybar-crop-bar "{{in}}"; fi @if [[ -n "{{out}}" ]]; then scripts/taffybar-crop-bar "{{in}}" "{{out}}"; else scripts/taffybar-crop-bar "{{in}}"; fi

View File

@@ -1 +1,3 @@
packages: . taffybar/ packages:
.
taffybar/

View File

@@ -16,12 +16,16 @@ executable taffybar
ghc-options: -threaded -rtsopts -with-rtsopts=-N ghc-options: -threaded -rtsopts -with-rtsopts=-N
ghc-prof-options: -fprof-auto ghc-prof-options: -fprof-auto
build-depends: base build-depends: base
, dbus
, dbus-menu
, X11 , X11
, bytestring , bytestring
, containers , containers
, filepath , filepath
, gi-gdk3
, gi-gtk3 , gi-gtk3
, gi-gdkpixbuf , gi-gdkpixbuf
, gi-glib
, gtk-sni-tray , gtk-sni-tray
, gtk-strut , gtk-strut
, haskell-gi-base , haskell-gi-base

View File

@@ -9,13 +9,11 @@
/* Base typography + foreground color for the bar itself. /* Base typography + foreground color for the bar itself.
* *
* IMPORTANT: menus/popovers created by SNI items are separate GtkWindows but * IMPORTANT: SNI menus are popup windows but inherit style context from the
* inherit style context from the "attach widget" chain. If we apply a blanket * attach-widget chain. Avoid broad descendant selectors (especially `*`) here
* `color:` rule here, it will bleed into those menus and override the GTK * so we don't accidentally override menu theming.
* theme, making submenu text unreadable.
*/ */
.taffy-window .taffy-box :not(menu):not(menuitem):not(popover):not(window), .taffy-box {
.taffy-window .taffy-box :not(menu):not(menuitem):not(popover):not(window) * {
/* Most text should come from Iosevka Aile; icon glyphs (Font Awesome / Nerd /* 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 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; font-family: "Iosevka Aile", "Iosevka Nerd Font", "Iosevka NF", "Noto Sans", sans-serif;
@@ -71,17 +69,22 @@
opacity: 1; opacity: 1;
} }
/* Make each widget's squircle background feel "solid": avoid GTK nodes and /* Make each widget's squircle background feel "solid": avoid child widgets
labels painting their own backgrounds on top of `.outer-pad`. painting their own backgrounds on top of `.outer-pad`.
Exclude menu/menuitem/popover so popup menus attached via menuAttachToWidget *
aren't forced transparent. */ * We intentionally avoid broad descendant selectors (especially `*`) here
.outer-pad :not(menu):not(menuitem):not(popover):not(window), * because SNI menus inherit style context via their attach-widget chain and
.inner-pad, * those selectors can leak into menu windows, making them transparent.
.inner-pad :not(menu):not(menuitem):not(popover):not(window), */
.inner-pad :not(menu):not(menuitem):not(popover):not(window) *, .outer-pad label,
.contents, .outer-pad image,
.contents :not(menu):not(menuitem):not(popover):not(window), .outer-pad button,
.contents :not(menu):not(menuitem):not(popover):not(window) * { .inner-pad label,
.inner-pad image,
.inner-pad button,
.contents label,
.contents image,
.contents button {
background-color: transparent; background-color: transparent;
} }
@@ -105,8 +108,7 @@
inset 0 0 0 1px @pill-border, inset 0 0 0 1px @pill-border,
0 10px 24px @pill-shadow; 0 10px 24px @pill-shadow;
} }
.workspaces .outer-pad :not(menu):not(menuitem):not(popover):not(window), .workspaces .outer-pad {
.workspaces .outer-pad :not(menu):not(menuitem):not(popover):not(window) * {
color: @font-color; color: @font-color;
} }

View File

@@ -5,19 +5,32 @@
module Main (main) where module Main (main) where
import Control.Monad.IO.Class (MonadIO, liftIO) 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.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 qualified Data.Map as M
import Data.Maybe (catMaybes, fromMaybe, mapMaybe) import Data.Maybe (catMaybes, fromMaybe, listToMaybe, mapMaybe)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T 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.Gtk as Gtk
import qualified GI.GLib as GLib
import Network.HostName (getHostName) import Network.HostName (getHostName)
import System.Environment (lookupEnv)
import System.Environment.XDG.BaseDir (getUserConfigFile) 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 (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
import System.Taffybar.DBus.Toggle import System.Taffybar.DBus.Toggle
import System.Taffybar.Hooks (withLogLevels) 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 System.Taffybar.Widget.Util (buildContentsBox, buildIconLabelBox, loadPixbufByName, widgetSetClassGI)
import qualified System.Taffybar.Widget.Workspaces as X11Workspaces import qualified System.Taffybar.Widget.Workspaces as X11Workspaces
import System.Taffybar.WindowIcon (pixBufFromColor) 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=<substring> 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. -- | Wrap the widget in a "TaffyBox" (via 'buildContentsBox') and add a CSS class.
decorateWithClassAndBox :: MonadIO m => Text -> Gtk.Widget -> m Gtk.Widget decorateWithClassAndBox :: MonadIO m => Text -> Gtk.Widget -> m Gtk.Widget
decorateWithClassAndBox klass widget = do decorateWithClassAndBox klass widget = do
@@ -132,7 +289,7 @@ isPathCandidate name =
T.isInfixOf "/" name || T.isInfixOf "/" name ||
any (`T.isSuffixOf` name) [".png", ".svg", ".xpm"] any (`T.isSuffixOf` name) [".png", ".svg", ".xpm"]
hyprlandIconFromCandidate :: Int32 -> Text -> TaffyIO (Maybe Gdk.Pixbuf) hyprlandIconFromCandidate :: Int32 -> Text -> TaffyIO (Maybe GdkPixbuf.Pixbuf)
hyprlandIconFromCandidate size name hyprlandIconFromCandidate size name
| isPathCandidate name = | isPathCandidate name =
liftIO $ getPixbufFromFilePath (T.unpack name) liftIO $ getPixbufFromFilePath (T.unpack name)
@@ -147,7 +304,7 @@ hyprlandManualIconGetter =
foldl maybeTCombine (return Nothing) $ foldl maybeTCombine (return Nothing) $
map (hyprlandIconFromCandidate size) (hyprlandIconCandidates windowData) map (hyprlandIconFromCandidate size) (hyprlandIconCandidates windowData)
fallbackIconPixbuf :: Int32 -> TaffyIO (Maybe Gdk.Pixbuf) fallbackIconPixbuf :: Int32 -> TaffyIO (Maybe GdkPixbuf.Pixbuf)
fallbackIconPixbuf size = do fallbackIconPixbuf size = do
let fallbackNames = let fallbackNames =
[ "application-x-executable" [ "application-x-executable"
@@ -365,6 +522,7 @@ mkSimpleTaffyConfig hostName backend cssFiles =
, barHeight = ScreenRatio $ 1 / 33 , barHeight = ScreenRatio $ 1 / 33
, cssPaths = cssFiles , cssPaths = cssFiles
, centerWidgets = [sniTrayWidget] , centerWidgets = [sniTrayWidget]
, startupHook = debugPopupSNIMenuHook
} }
-- ** Entry Point -- ** Entry Point
@@ -382,4 +540,5 @@ main = do
withLogServer $ withLogServer $
withLogLevels $ withLogLevels $
withToggleServer $ withToggleServer $
withDebugServer $
toTaffybarConfig simpleTaffyConfig toTaffybarConfig simpleTaffyConfig

28
nixos/flake.lock generated
View File

@@ -116,11 +116,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1770491193, "lastModified": 1770687088,
"narHash": "sha256-zdnWeXmPZT8BpBo52s4oansT1Rq0SNzksXKpEcMc5lE=", "narHash": "sha256-WM353TQnhVCbgMGUqoPIsLEdF8HHtMo/dFryzBSEswI=",
"owner": "sadjow", "owner": "sadjow",
"repo": "claude-code-nix", "repo": "claude-code-nix",
"rev": "f68a2683e812d1e4f9a022ff3e0206d46347d019", "rev": "5fb242d2c746009f9fa3b63e9f346e8ea64328ea",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -579,11 +579,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1770613567, "lastModified": 1770693358,
"narHash": "sha256-t0wC7BrU7YwqvZcZpCB50dk37/bVcpIoE/qME/kw8PA=", "narHash": "sha256-rqUwaRsxMd9OOkDUxAlQlGfohlV8ZK4s2j5qZ9dJQVI=",
"owner": "taffybar", "owner": "taffybar",
"repo": "gtk-sni-tray", "repo": "gtk-sni-tray",
"rev": "fe5a75bc20228544217f96a1e6895be12e6198c7", "rev": "a7a72c72d719bb3a6e4c36974671f2f6581f55ff",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -1134,11 +1134,11 @@
"nixpkgs-regression": "nixpkgs-regression" "nixpkgs-regression": "nixpkgs-regression"
}, },
"locked": { "locked": {
"lastModified": 1770591402, "lastModified": 1770673695,
"narHash": "sha256-7qxOxkj11ExOhpxcsFK3O8Ktegkgw0SCq9nfoAOvjxM=", "narHash": "sha256-7lLVzlUSkAPo9LDe0/M9CdAh6HHDfzq8pfn98PXoKu0=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nix", "repo": "nix",
"rev": "e4ce788f9d8de1bc5e58002d01088cd71c6703d0", "rev": "845d951682008a009a9727437c9d913403053d06",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -1585,11 +1585,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1770410595, "lastModified": 1770668379,
"narHash": "sha256-sqDK58NI/+tfIBd5gzYKXMhMv3CNtFBtnR958KqQhlk=", "narHash": "sha256-sqDK58NI/+tfIBd5gzYKXMhMv3CNtFBtnR958KqQhlk=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "01dc1d61477037e19ebe7a58c71581020bf7eea0", "rev": "f65d0110607dba433ea746d445b1b66d5ead6f6c",
"revCount": 145, "revCount": 149,
"type": "git", "type": "git",
"url": "ssh://gitea@dev.railbird.ai:1123/railbird/secrets-flake.git" "url": "ssh://gitea@dev.railbird.ai:1123/railbird/secrets-flake.git"
}, },
@@ -1735,8 +1735,8 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1770661157, "lastModified": 1770685149,
"narHash": "sha256-Le3mnGVNLrD66/CV5jqMeSSQsMMMEaFrFbYzl3KCD3A=", "narHash": "sha256-3PY90Kt/8z4E0TNHBQuEzqaTfrRfUTK5HTfoIM3Z7W4=",
"path": "/home/imalison/dotfiles/dotfiles/config/taffybar/taffybar", "path": "/home/imalison/dotfiles/dotfiles/config/taffybar/taffybar",
"type": "path" "type": "path"
}, },