packages feed

gtk-sni-tray 0.1.14.2 → 0.1.15.0

raw patch · 6 files changed

+409/−89 lines, 6 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- StatusNotifier.Tray: getThemeWithDefaultFallbacks :: String -> IO IconTheme
+ StatusNotifier.Icon.Pixbuf: Rgb8 :: !Word8 -> !Word8 -> !Word8 -> Rgb8
+ StatusNotifier.Icon.Pixbuf: [rgb8Blue] :: Rgb8 -> !Word8
+ StatusNotifier.Icon.Pixbuf: [rgb8Green] :: Rgb8 -> !Word8
+ StatusNotifier.Icon.Pixbuf: [rgb8Red] :: Rgb8 -> !Word8
+ StatusNotifier.Icon.Pixbuf: data Rgb8
+ StatusNotifier.Icon.Pixbuf: instance GHC.Classes.Eq StatusNotifier.Icon.Pixbuf.Rgb8
+ StatusNotifier.Icon.Pixbuf: instance GHC.Internal.Show.Show StatusNotifier.Icon.Pixbuf.Rgb8
+ StatusNotifier.Icon.Pixbuf: recolorPixbufDuotone :: Rgb8 -> Rgb8 -> Pixbuf -> IO (Maybe Pixbuf)
+ StatusNotifier.Icon.Pixbuf: recolorPixbufDuotoneRGBA :: RGBA -> RGBA -> Pixbuf -> IO (Maybe Pixbuf)
+ StatusNotifier.Icon.Pixbuf: recolorPixbufMonochrome :: Rgb8 -> Pixbuf -> IO (Maybe Pixbuf)
+ StatusNotifier.Icon.Pixbuf: recolorPixbufMonochromeRGBA :: RGBA -> Pixbuf -> IO (Maybe Pixbuf)
+ StatusNotifier.Icon.Pixbuf: rgb8FromGdkRGBA :: RGBA -> IO Rgb8
+ StatusNotifier.Tray: PreferPixmaps :: TrayIconPreference
+ StatusNotifier.Tray: PreferThemedIcons :: TrayIconPreference
+ StatusNotifier.Tray: [trayIconPreference] :: TrayParams -> TrayIconPreference
+ StatusNotifier.Tray: buildTrayWithPixbufTransform :: Host -> Client -> TrayParams -> Maybe PixbufTransform -> IO Box
+ StatusNotifier.Tray: buildTrayWithPriorityAndPixbufTransform :: Host -> Client -> TrayParams -> TrayPriorityConfig -> Maybe PixbufTransform -> IO Box
+ StatusNotifier.Tray: data TrayIconPreference
+ StatusNotifier.Tray: getThemeWithOptionalSearchPath :: Maybe String -> IO IconTheme
+ StatusNotifier.Tray: instance GHC.Classes.Eq StatusNotifier.Tray.TrayIconPreference
+ StatusNotifier.Tray: instance GHC.Internal.Read.Read StatusNotifier.Tray.TrayIconPreference
+ StatusNotifier.Tray: instance GHC.Internal.Show.Show StatusNotifier.Tray.TrayIconPreference
+ StatusNotifier.Tray: pathsForIconThemePath :: FilePath -> IO [FilePath]
+ StatusNotifier.Tray: type PixbufTransform = Image -> Pixbuf -> IO Pixbuf
- StatusNotifier.Tray: TrayParams :: Orientation -> TrayImageSize -> Bool -> StrutAlignment -> Rational -> TrayClickAction -> TrayClickAction -> TrayClickAction -> MenuBackend -> Bool -> TrayParams
+ StatusNotifier.Tray: TrayParams :: Orientation -> TrayImageSize -> Bool -> TrayIconPreference -> StrutAlignment -> Rational -> TrayClickAction -> TrayClickAction -> TrayClickAction -> MenuBackend -> Bool -> TrayParams

Files

ChangeLog.md view
@@ -1,5 +1,16 @@ # Changelog for gtk-sni-tray +## 0.1.15.0++- Fix: respect the user's GTK icon theme when an SNI item provides IconThemePath+  (and handle IconThemePath values that point inside a theme directory).+- Add icon preference support (choose themed icons vs pixmaps when both are+  provided) and expose it in the standalone executable with `--icon-preference`.+- Add optional icon recoloring in the standalone executable with `--icon-recolor`.+- Nix: improve flake dev shell environment so `cabal new-run` can find icon+  themes and gdk-pixbuf loaders.+- Development: add `scripts/fmt` and `scripts/fmt-check` for ormolu formatting.+ ## 0.1.14.2  - Relax executable `optparse-applicative` upper bound to `< 0.20` for GHC 9.12 snapshots.
README.md view
@@ -38,3 +38,13 @@  If you see a Cabal error about missing pkg-config packages, `scripts/cabal-run` does a quick preflight check and prints a more direct message.++Development+-----------++Formatting is done with `ormolu` (available in the flake dev shell):++```sh+scripts/fmt+scripts/fmt-check+```
app/Main.hs view
@@ -17,6 +17,7 @@ import           Graphics.UI.GIGtkStrut import           Options.Applicative import qualified StatusNotifier.Host.Service as Host+import qualified StatusNotifier.Icon.Pixbuf as IconPixbuf import           StatusNotifier.TransparentWindow import           StatusNotifier.Tray import           System.Log.Logger@@ -34,6 +35,12 @@ data BackendChoice = BackendAuto | BackendX11Choice | BackendWaylandChoice   deriving (Eq, Show, Read) +data IconRecolorMode+  = IconRecolorNone+  | IconRecolorMono+  | IconRecolorDuotone+  deriving (Eq, Show, Read)+ detectBackend :: IO Backend detectBackend = do   supported <- GtkLayerShell.isSupported@@ -474,6 +481,40 @@   <> value (5 % 7)   ) +iconPreferenceP :: Parser TrayIconPreference+iconPreferenceP =+  option (eitherReader parsePref)+    ( long "icon-preference"+        <> help "Icon preference when both are provided: pixmaps (default) | themed"+        <> value PreferPixmaps+        <> metavar "PREFERENCE"+    )+  where+    parsePref s =+      case map toLower s of+        "pixmaps" -> Right PreferPixmaps+        "pixmap" -> Right PreferPixmaps+        "themed" -> Right PreferThemedIcons+        "theme" -> Right PreferThemedIcons+        _ -> Left "expected one of: pixmaps, themed"++iconRecolorModeP :: Parser IconRecolorMode+iconRecolorModeP =+  option (eitherReader parseMode)+    ( long "icon-recolor"+        <> help "Recolor icons based on their alpha mask: none (default) | mono | duotone"+        <> value IconRecolorNone+        <> metavar "MODE"+    )+  where+    parseMode s =+      case map toLower s of+        "none" -> Right IconRecolorNone+        "mono" -> Right IconRecolorMono+        "duo" -> Right IconRecolorDuotone+        "duotone" -> Right IconRecolorDuotone+        _ -> Left "expected one of: none, mono, duotone"+ menuBackendP :: Parser MenuBackend menuBackendP =   option (eitherReader parseMenuBackend)@@ -513,9 +554,11 @@              -> Rational              -> Rational              -> MenuBackend+             -> TrayIconPreference+             -> IconRecolorMode              -> IO () buildWindows pos align size padding monitors priority backendChoice maybeColorString expand-             centerIcons startWatcher noStrut barLength overlayScale menuBackend = do+             centerIcons startWatcher noStrut barLength overlayScale menuBackend iconPreference iconRecolorMode = do   _ <- Gtk.init Nothing   logger <- getLogger "StatusNotifier"   saveGlobalLogger $ setLevel priority logger@@ -569,12 +612,41 @@                 TopPos -> Gtk.OrientationHorizontal                 BottomPos -> Gtk.OrientationHorizontal                 _ -> Gtk.OrientationVertical+            mixWord8 t a b =+              let ta = fromIntegral a :: Double+                  tb = fromIntegral b :: Double+                  out = ta + max 0 (min 1 t) * (tb - ta)+              in fromIntegral (max (0 :: Int) (min 255 (round out)))+            mixRgb8 t (IconPixbuf.Rgb8 r g b) (IconPixbuf.Rgb8 r2 g2 b2) =+              IconPixbuf.Rgb8 (mixWord8 t r r2) (mixWord8 t g g2) (mixWord8 t b b2)+            mkIconTransform =+              case iconRecolorMode of+                IconRecolorNone -> Nothing+                IconRecolorMono ->+                  Just $ \image pb -> do+                    ctx <- Gtk.widgetGetStyleContext image+                    fg <- Gtk.styleContextGetColor ctx [Gtk.StateFlagsNormal]+                    fg8 <- IconPixbuf.rgb8FromGdkRGBA fg+                    mpb <- IconPixbuf.recolorPixbufMonochrome fg8 pb+                    return $ fromMaybe pb mpb+                IconRecolorDuotone ->+                  Just $ \image pb -> do+                    ctx <- Gtk.widgetGetStyleContext image+                    fg <- Gtk.styleContextGetColor ctx [Gtk.StateFlagsNormal]+                    fg8 <- IconPixbuf.rgb8FromGdkRGBA fg+                    let black = IconPixbuf.Rgb8 0 0 0+                        white = IconPixbuf.Rgb8 255 255 255+                        dark = mixRgb8 0.35 fg8 black+                        light = mixRgb8 0.35 fg8 white+                    mpb <- IconPixbuf.recolorPixbufDuotone dark light pb+                    return $ fromMaybe pb mpb         tray <--          buildTray host client+          buildTrayWithPixbufTransform host client             TrayParams             { trayOrientation = orientation             , trayImageSize = Expand             , trayIconExpand = expand+            , trayIconPreference = iconPreference             , trayAlignment = align             , trayOverlayScale = overlayScale             , trayLeftClickAction = Activate@@ -583,6 +655,7 @@             , trayMenuBackend = menuBackend             , trayCenterIcons = centerIcons             }+            mkIconTransform         window <- Gtk.windowNew Gtk.WindowTypeToplevel         Gtk.windowSetResizable window False         Gtk.windowSetSkipTaskbarHint window True@@ -616,7 +689,7 @@   buildWindows <$> positionP <*> alignmentP <*> sizeP <*> paddingP <*>   monitorNumberP <*> logP <*> backendChoiceP <*> colorP <*> expandP <*>   centerIconsP <*> startWatcherP <*>-  noStrutP <*> barLengthP <*> overlayScaleP <*> menuBackendP+  noStrutP <*> barLengthP <*> overlayScaleP <*> menuBackendP <*> iconPreferenceP <*> iconRecolorModeP  versionOption :: Parser (a -> a) versionOption = infoOption
gtk-sni-tray.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           gtk-sni-tray-version:        0.1.14.2+version:        0.1.15.0 synopsis:       A standalone StatusNotifierItem/AppIndicator tray description:    Please see the README on Github at <https://github.com/IvanMalison/gtk-sni-tray#readme> category:       System@@ -27,6 +27,7 @@  library   exposed-modules:+      StatusNotifier.Icon.Pixbuf       StatusNotifier.TransparentWindow       StatusNotifier.Tray   other-modules:
+ src/StatusNotifier/Icon/Pixbuf.hs view
@@ -0,0 +1,149 @@+module StatusNotifier.Icon.Pixbuf+  ( Rgb8 (..),+    rgb8FromGdkRGBA,+    recolorPixbufMonochrome,+    recolorPixbufMonochromeRGBA,+    recolorPixbufDuotone,+    recolorPixbufDuotoneRGBA,+  )+where++import Control.Monad (forM_)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Unsafe as BSU+import Data.Word (Word8)+import Foreign.Ptr (Ptr, castPtr, plusPtr)+import Foreign.Storable (peekByteOff, pokeByteOff)+import qualified GI.Gdk as Gdk+import qualified GI.GdkPixbuf.Objects.Pixbuf as GdkPixbuf++data Rgb8 = Rgb8+  { rgb8Red :: !Word8,+    rgb8Green :: !Word8,+    rgb8Blue :: !Word8+  }+  deriving (Eq, Show)++clamp01 :: Double -> Double+clamp01 x+  | x < 0 = 0+  | x > 1 = 1+  | otherwise = x++toWord8 :: Double -> Word8+toWord8 x = round (clamp01 x * 255.0)++rgb8FromGdkRGBA :: Gdk.RGBA -> IO Rgb8+rgb8FromGdkRGBA rgba = do+  r <- Gdk.getRGBARed rgba+  g <- Gdk.getRGBAGreen rgba+  b <- Gdk.getRGBABlue rgba+  pure $ Rgb8 (toWord8 r) (toWord8 g) (toWord8 b)++lerpWord8 :: Double -> Word8 -> Word8 -> Word8+lerpWord8 t a b =+  let ta = fromIntegral a :: Double+      tb = fromIntegral b :: Double+      out = ta + clamp01 t * (tb - ta)+   in fromIntegral (max (0 :: Int) (min 255 (round out)))++pixelLuminance01 :: Word8 -> Word8 -> Word8 -> Double+pixelLuminance01 r g b =+  let rf = fromIntegral r / 255.0 :: Double+      gf = fromIntegral g / 255.0 :: Double+      bf = fromIntegral b / 255.0 :: Double+   in clamp01 (0.2126 * rf + 0.7152 * gf + 0.0722 * bf)++-- | Replace the RGB channels of the pixbuf with the given color while preserving+-- the alpha channel. This is the "extract shapes and recolor" primitive: the+-- icon's shape comes from its alpha mask.+recolorPixbufMonochrome :: Rgb8 -> GdkPixbuf.Pixbuf -> IO (Maybe GdkPixbuf.Pixbuf)+recolorPixbufMonochrome (Rgb8 r g b) pixbuf = do+  mpb <- GdkPixbuf.pixbufCopy pixbuf+  case mpb of+    Nothing -> pure Nothing+    Just pb -> do+      recolorInPlace pb $ \_origR _origG _origB _a -> (r, g, b)+      pure (Just pb)++recolorPixbufMonochromeRGBA :: Gdk.RGBA -> GdkPixbuf.Pixbuf -> IO (Maybe GdkPixbuf.Pixbuf)+recolorPixbufMonochromeRGBA rgba pixbuf = do+  rgb <- rgb8FromGdkRGBA rgba+  recolorPixbufMonochrome rgb pixbuf++-- | Map the original pixel luminance to a color between the two provided colors,+-- while preserving the alpha channel.+recolorPixbufDuotone :: Rgb8 -> Rgb8 -> GdkPixbuf.Pixbuf -> IO (Maybe GdkPixbuf.Pixbuf)+recolorPixbufDuotone (Rgb8 r0 g0 b0) (Rgb8 r1 g1 b1) pixbuf = do+  mpb <- GdkPixbuf.pixbufCopy pixbuf+  case mpb of+    Nothing -> pure Nothing+    Just pb -> do+      recolorInPlace pb $ \origR origG origB _a ->+        let t = pixelLuminance01 origR origG origB+         in ( lerpWord8 t r0 r1,+              lerpWord8 t g0 g1,+              lerpWord8 t b0 b1+            )+      pure (Just pb)++recolorPixbufDuotoneRGBA :: Gdk.RGBA -> Gdk.RGBA -> GdkPixbuf.Pixbuf -> IO (Maybe GdkPixbuf.Pixbuf)+recolorPixbufDuotoneRGBA rgba0 rgba1 pixbuf = do+  c0 <- rgb8FromGdkRGBA rgba0+  c1 <- rgb8FromGdkRGBA rgba1+  recolorPixbufDuotone c0 c1 pixbuf++recolorInPlace ::+  GdkPixbuf.Pixbuf ->+  (Word8 -> Word8 -> Word8 -> Word8 -> (Word8, Word8, Word8)) ->+  IO ()+recolorInPlace pb computeRgb = do+  width <- fromIntegral <$> GdkPixbuf.pixbufGetWidth pb+  height <- fromIntegral <$> GdkPixbuf.pixbufGetHeight pb+  rowStride <- fromIntegral <$> GdkPixbuf.pixbufGetRowstride pb+  nChannels <- fromIntegral <$> GdkPixbuf.pixbufGetNChannels pb+  hasAlpha <- GdkPixbuf.pixbufGetHasAlpha pb+  pixelsBs <- GdkPixbuf.pixbufGetPixels pb++  let bytesPerPixel = nChannels+      alphaOff = if hasAlpha then 3 else (-1)++      -- Hard stop: unexpected pixbuf format.+      validFormat = bytesPerPixel == 3 || bytesPerPixel == 4++      readChannel :: Ptr Word8 -> Int -> IO Word8+      readChannel p off = peekByteOff p off++      writeChannel :: Ptr Word8 -> Int -> Word8 -> IO ()+      writeChannel p off v = pokeByteOff p off v++  if width <= 0 || height <= 0 || not validFormat || BS.null pixelsBs+    then pure ()+    else+      -- 'pixbufGetPixels' gives us a view of the pixbuf's pixel buffer. We mutate+      -- in-place after copying the pixbuf so the caller gets an isolated buffer.+      BSU.unsafeUseAsCString pixelsBs $ \pixels0 -> do+        let pixels :: Ptr Word8+            pixels = castPtr pixels0++            rows :: Int -> Ptr Word8+            rows y = pixels `plusPtr` (y * rowStride)++            pixelPtr :: Ptr Word8 -> Int -> Ptr Word8+            pixelPtr row x = row `plusPtr` (x * bytesPerPixel)++        forM_ [0 .. height - 1] $ \y -> do+          let row = rows y+          forM_ [0 .. width - 1] $ \x -> do+            let p = pixelPtr row x+            a <- if hasAlpha then readChannel p alphaOff else pure 255+            if a == 0+              then pure ()+              else do+                origR <- readChannel p 0+                origG <- readChannel p 1+                origB <- readChannel p 2+                let (outR, outG, outB) = computeRgb origR origG origB a+                writeChannel p 0 outR+                writeChannel p 1 outG+                writeChannel p 2 outB
src/StatusNotifier/Tray.hs view
@@ -30,7 +30,6 @@ import           GI.GLib.Structs.Bytes import qualified GI.Gdk as Gdk import           GI.Gdk.Enums-import           GI.Gdk.Objects.Screen import           GI.Gdk.Structs.EventScroll import           GI.GdkPixbuf.Enums import           GI.GdkPixbuf.Objects.Pixbuf as Gdk@@ -50,6 +49,10 @@ trayLogger :: Priority -> String -> IO () trayLogger = logM "StatusNotifier.Tray" +-- | Optional post-processing hook for item icons. This is applied after scaling+-- and overlay composition.+type PixbufTransform = Gtk.Image -> Pixbuf -> IO Pixbuf+ logItemInfo :: ItemInfo -> String -> IO () logItemInfo info message =   trayLogger INFO $ printf "%s - %s pixmap count: %s" message@@ -96,20 +99,47 @@ themeLoadFlags :: [IconLookupFlags] themeLoadFlags = [IconLookupFlagsGenericFallback, IconLookupFlagsUseBuiltin] -getThemeWithDefaultFallbacks :: String -> IO IconTheme-getThemeWithDefaultFallbacks themePath = do-  themeForIcon <- iconThemeNew-  defaultTheme <- iconThemeGetDefault--  _ <- runMaybeT $ do-    screen <- MaybeT screenGetDefault-    lift $ iconThemeSetScreen themeForIcon screen--  filePaths <- iconThemeGetSearchPath defaultTheme-  iconThemeAppendSearchPath themeForIcon themePath-  mapM_ (iconThemeAppendSearchPath themeForIcon) filePaths+getThemeWithOptionalSearchPath :: Maybe String -> IO IconTheme+getThemeWithOptionalSearchPath themePath = do+  theme <- iconThemeGetDefault+  forM_ (themePath >>= nonEmpty) $ \p -> do+    -- Respect the user's configured icon theme by using GTK's default theme+    -- object, but include any item-provided IconThemePath as an additional+    -- search path.+    --+    -- Some items provide IconThemePath pointing inside a theme dir, e.g.:+    --   .../Papirus/64x64/mimetypes+    -- GTK expects search paths that contain theme directories, so also try to+    -- append the parent directory of any ancestor that contains index.theme.+    pathsToAppend <- pathsForIconThemePath p+    existing <- iconThemeGetSearchPath theme+    forM_ pathsToAppend $ \p' ->+      unless (p' `elem` existing) $ iconThemeAppendSearchPath theme p'+  return theme+  where+    nonEmpty "" = Nothing+    nonEmpty x = Just x -  return themeForIcon+pathsForIconThemePath :: FilePath -> IO [FilePath]+pathsForIconThemePath rawPath = do+  mThemeDir <- findAncestorWithIndexTheme 8 rawPath+  let base =+        case mThemeDir of+          Nothing -> []+          Just themeDir -> [takeDirectory themeDir]+  return $ nub $ base ++ [rawPath]+  where+    findAncestorWithIndexTheme :: Int -> FilePath -> IO (Maybe FilePath)+    findAncestorWithIndexTheme 0 _ = return Nothing+    findAncestorWithIndexTheme n p = do+      hasIndex <- doesFileExist (p </> "index.theme")+      if hasIndex+        then return (Just p)+        else do+          let parent = takeDirectory p+          if parent == p+            then return Nothing+            else findAncestorWithIndexTheme (n - 1) parent  catchGErrorsAsLeft :: IO a -> IO (Either GError a) catchGErrorsAsLeft action = catch (Right <$> action) (return . Left)@@ -134,36 +164,38 @@ getIconPixbufByName :: Int32 -> T.Text -> Maybe String -> IO (Maybe Pixbuf) getIconPixbufByName size name themePath = do   trayLogger DEBUG $ printf "Getting Pixbuf from name for %s" name-  let nonEmptyThemePath = themePath >>= (\x -> if x == "" then Nothing else Just x)-  themeForIcon <--    maybe iconThemeGetDefault getThemeWithDefaultFallbacks nonEmptyThemePath+  themeForIcon <- getThemeWithOptionalSearchPath themePath    let panelName = T.pack $ printf "%s-panel" name-  hasPanelIcon <- iconThemeHasIcon themeForIcon panelName-  hasIcon <- iconThemeHasIcon themeForIcon name--  if hasIcon || hasPanelIcon+  -- Avoid relying on iconThemeHasIcon: it can be overly strict when fallback+  -- loading is enabled. Just try to load and fall back if it fails.+  let tryLoad :: T.Text -> IO (Maybe Pixbuf)+      tryLoad iconName =+        catchAny (iconThemeLoadIcon themeForIcon iconName size themeLoadFlags)+                 (const $ pure Nothing) -  then do-    let targetName = if hasPanelIcon then panelName else name-    trayLogger DEBUG $ printf "Found icon %s in theme" name-    catchAny (iconThemeLoadIcon themeForIcon targetName size themeLoadFlags)-             (const $ pure Nothing)+  themedPixbuf <- do+    pbPanel <- tryLoad panelName+    case pbPanel of+      Just _ -> return pbPanel+      Nothing -> tryLoad name -  else do-    trayLogger DEBUG $ printf "Trying to load icon %s as filepath" name-    -- Try to load the icon as a filepath-    let nameString = T.unpack name-    fileExists <- doesFileExist nameString-    maybeFile <- if fileExists-    then return $ Just nameString-    else fmap join $ sequenceA $ getIconPathFromThemePath nameString <$> themePath+  case themedPixbuf of+    Just _ -> return themedPixbuf+    Nothing -> do+      trayLogger DEBUG $ printf "Trying to load icon %s as filepath" name+      -- Try to load the icon as a filepath+      let nameString = T.unpack name+      fileExists <- doesFileExist nameString+      maybeFile <- if fileExists+      then return $ Just nameString+      else fmap join $ sequenceA $ getIconPathFromThemePath nameString <$> themePath #if MIN_VERSION_gi_gdkpixbuf(2,0,26)-    let handleResult = fmap join . sequenceA+      let handleResult = fmap join . sequenceA #else-    let handleResult = sequenceA+      let handleResult = sequenceA #endif-    handleResult $ safePixbufNewFromFile <$> maybeFile+      handleResult $ safePixbufNewFromFile <$> maybeFile  getIconPathFromThemePath :: String -> String -> IO (Maybe String) getIconPathFromThemePath name themePath = if name == "" then return Nothing else do@@ -378,10 +410,18 @@ trayMatchIsMenu expected =   mkTrayItemMatcher "is-menu" $ \info -> itemIsMenu info == expected +-- | Controls whether to prefer application-provided pixmaps or themed icons+-- when both are present. Some items provide both.+data TrayIconPreference+  = PreferPixmaps+  | PreferThemedIcons+  deriving (Eq, Show, Read)+ data TrayParams = TrayParams   { trayOrientation :: Gtk.Orientation   , trayImageSize :: TrayImageSize   , trayIconExpand :: Bool+  , trayIconPreference :: TrayIconPreference   , trayAlignment :: StrutAlignment   , trayOverlayScale :: Rational   , trayLeftClickAction :: TrayClickAction@@ -396,6 +436,7 @@   { trayOrientation = Gtk.OrientationHorizontal   , trayImageSize = Expand   , trayIconExpand = False+  , trayIconPreference = PreferPixmaps   , trayAlignment = End   , trayOverlayScale = 2 % 5   , trayLeftClickAction = Activate@@ -407,10 +448,18 @@  buildTray :: Host -> Client -> TrayParams -> IO Gtk.Box buildTray host client params =-  buildTrayWithPriority host client params defaultTrayPriorityConfig+  buildTrayWithPixbufTransform host client params Nothing  buildTrayWithPriority :: Host -> Client -> TrayParams -> TrayPriorityConfig -> IO Gtk.Box-buildTrayWithPriority Host+buildTrayWithPriority host client params priorityConfig =+  buildTrayWithPriorityAndPixbufTransform host client params priorityConfig Nothing++buildTrayWithPixbufTransform :: Host -> Client -> TrayParams -> Maybe PixbufTransform -> IO Gtk.Box+buildTrayWithPixbufTransform host client params mTransform =+  buildTrayWithPriorityAndPixbufTransform host client params defaultTrayPriorityConfig mTransform++buildTrayWithPriorityAndPixbufTransform :: Host -> Client -> TrayParams -> TrayPriorityConfig -> Maybe PixbufTransform -> IO Gtk.Box+buildTrayWithPriorityAndPixbufTransform Host             { itemInfoMap = getInfoMap             , addUpdateHandler = addUHandler             , removeUpdateHandler = removeUHandler@@ -419,6 +468,7 @@           TrayParams { trayOrientation = orientation                      , trayImageSize = imageSize                      , trayIconExpand = shouldExpand+                     , trayIconPreference = iconPreference                      , trayAlignment = alignment                      , trayOverlayScale = overlayScale                      , trayLeftClickAction = leftClickAction@@ -427,7 +477,8 @@                      , trayMenuBackend = menuBackend                      , trayCenterIcons = centerIcons                      }-          TrayPriorityConfig { trayPriorityMatchers = priorityMatchers } = do+          TrayPriorityConfig { trayPriorityMatchers = priorityMatchers }+          mTransform = do   trayLogger INFO "Building tray"    trayBox <- Gtk.boxNew orientation 0@@ -482,6 +533,13 @@           \(newIndex, child) ->             Gtk.boxReorderChild trayBox child (fromIntegral newIndex) +      applyTransform :: Gtk.Image -> Maybe Pixbuf -> IO (Maybe Pixbuf)+      applyTransform _ Nothing = return Nothing+      applyTransform image (Just pb) =+        case mTransform of+          Nothing -> return (Just pb)+          Just f -> Just <$> f image pb+       updateIconFromInfo info@ItemInfo { itemServiceName = name } =         getContext name >>= updateIcon         where updateIcon Nothing = updateHandler ItemAdded info@@ -489,7 +547,9 @@                 size <- case imageSize of                           TrayImageSize size -> return size                           Expand -> Gtk.widgetGetAllocation image >>= getSize-                getScaledPixBufFromInfo size info >>=+                getScaledPixBufFromInfo size info+                  >>= applyTransform image+                  >>=                                   let handlePixbuf mpbuf =                                         if isJust mpbuf                                         then Gtk.imageSetFromPixbuf image mpbuf@@ -526,56 +586,58 @@           Gtk.widgetGetStyleContext eventBox >>=             flip Gtk.styleContextAddClass "tray-icon-button" -          image <--            case imageSize of-              Expand -> do-                image <- Gtk.imageNew-                lastAllocation <- MV.newMVar Nothing+          image <- Gtk.imageNew -                let setPixbuf allocation =-                      do-                        size <- getSize allocation+          case imageSize of+            Expand -> do+              lastAllocation <- MV.newMVar Nothing -                        actualWidth <- Gdk.getRectangleWidth allocation-                        actualHeight <- Gdk.getRectangleHeight allocation+              let setPixbuf allocation =+                    do+                      size <- getSize allocation -                        requestResize <- MV.modifyMVar lastAllocation $ \previous ->-                          let thisTime = Just (size, actualWidth, actualHeight)-                          in return (thisTime, thisTime /= previous)+                      actualWidth <- Gdk.getRectangleWidth allocation+                      actualHeight <- Gdk.getRectangleHeight allocation -                        trayLogger DEBUG $-                                   printf-                                   ("Allocating image size %s, width %s," <>-                                    " height %s, resize %s")-                                   (show size)-                                   (show actualWidth)-                                   (show actualHeight)-                                   (show requestResize)+                      requestResize <- MV.modifyMVar lastAllocation $ \previous ->+                        let thisTime = Just (size, actualWidth, actualHeight)+                        in return (thisTime, thisTime /= previous) -                        when requestResize $ do-                          trayLogger DEBUG "Requesting resize"-                          pixBuf <- getInfo info serviceName >>=-                                    getScaledPixBufFromInfo size-                          when (isNothing pixBuf) $-                               trayLogger WARNING $-                                          printf "Got null pixbuf for info %s" $-                                          showInfo info-                          Gtk.imageSetFromPixbuf image pixBuf-                          void $ traverse-                                 (\pb -> do-                                    width <- pixbufGetWidth pb-                                    height <- pixbufGetHeight pb-                                    Gtk.widgetSetSizeRequest image width height)-                                 pixBuf-                          void (Gdk.threadsAddIdle GLib.PRIORITY_DEFAULT $-                                   Gtk.widgetQueueResize image >> return False)+                      trayLogger DEBUG $+                                 printf+                                 ("Allocating image size %s, width %s," <>+                                  " height %s, resize %s")+                                 (show size)+                                 (show actualWidth)+                                 (show actualHeight)+                                 (show requestResize) -                _ <- Gtk.onWidgetSizeAllocate image setPixbuf-                return image-              TrayImageSize size -> do-                pixBuf <- getScaledPixBufFromInfo size info-                Gtk.imageNewFromPixbuf pixBuf+                      when requestResize $ do+                        trayLogger DEBUG "Requesting resize"+                        pixBuf0 <- getInfo info serviceName >>=+                                  getScaledPixBufFromInfo size+                        pixBuf <- applyTransform image pixBuf0+                        when (isNothing pixBuf) $+                             trayLogger WARNING $+                                        printf "Got null pixbuf for info %s" $+                                        showInfo info+                        Gtk.imageSetFromPixbuf image pixBuf+                        void $ traverse+                               (\pb -> do+                                  width <- pixbufGetWidth pb+                                  height <- pixbufGetHeight pb+                                  Gtk.widgetSetSizeRequest image width height)+                               pixBuf+                        void (Gdk.threadsAddIdle GLib.PRIORITY_DEFAULT $+                                 Gtk.widgetQueueResize image >> return False) +              _ <- Gtk.onWidgetSizeAllocate image setPixbuf+              return ()+            TrayImageSize size -> do+              pixBuf0 <- getScaledPixBufFromInfo size info+              pixBuf <- applyTransform image pixBuf0+              Gtk.imageSetFromPixbuf image pixBuf+           Gtk.widgetGetStyleContext image >>=              flip Gtk.styleContextAddClass "tray-icon-image" @@ -778,9 +840,23 @@               if BS.length p == 0               then return Nothing               else getIconPixbufFromByteString w h p+            getFromThemed =+              if name == ""+              then return Nothing+              else getIconPixbufByName size (T.pack name) mpath+            firstJustM a b = do+              ma <- a+              case ma of+                Just _ -> return ma+                Nothing -> b+         if null pixmaps         then getIconPixbufByName size (T.pack name) mpath-        else getFromPixmaps selectedPixmap+        else case iconPreference of+               PreferThemedIcons ->+                 firstJustM getFromThemed (getFromPixmaps selectedPixmap)+               PreferPixmaps ->+                 firstJustM (getFromPixmaps selectedPixmap) getFromThemed        uiUpdateHandler updateType info =         void $ Gdk.threadsAddIdle GLib.PRIORITY_DEFAULT $