packages feed

gtk-sni-tray 0.1.15.0 → 0.2.0.0

raw patch · 4 files changed

+1781/−1578 lines, 4 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- StatusNotifier.Tray: buildTrayWithPixbufTransform :: Host -> Client -> TrayParams -> Maybe PixbufTransform -> IO Box
- StatusNotifier.Tray: buildTrayWithPriority :: Host -> Client -> TrayParams -> TrayPriorityConfig -> IO Box
- StatusNotifier.Tray: buildTrayWithPriorityAndPixbufTransform :: Host -> Client -> TrayParams -> TrayPriorityConfig -> Maybe PixbufTransform -> IO Box
+ StatusNotifier.Tray: ConsumeClick :: TrayClickDecision
+ StatusNotifier.Tray: OverrideClickAction :: TrayClickAction -> TrayClickDecision
+ StatusNotifier.Tray: TrayClickContext :: ItemInfo -> Word32 -> Int32 -> Int32 -> [ModifierType] -> TrayClickAction -> TrayClickContext
+ StatusNotifier.Tray: TrayEventHooks :: Maybe TrayClickHook -> TrayEventHooks
+ StatusNotifier.Tray: UseDefaultClickAction :: TrayClickDecision
+ StatusNotifier.Tray: [trayClickButton] :: TrayClickContext -> Word32
+ StatusNotifier.Tray: [trayClickDefaultAction] :: TrayClickContext -> TrayClickAction
+ StatusNotifier.Tray: [trayClickHook] :: TrayEventHooks -> Maybe TrayClickHook
+ StatusNotifier.Tray: [trayClickItemInfo] :: TrayClickContext -> ItemInfo
+ StatusNotifier.Tray: [trayClickModifiers] :: TrayClickContext -> [ModifierType]
+ StatusNotifier.Tray: [trayClickXRoot] :: TrayClickContext -> Int32
+ StatusNotifier.Tray: [trayClickYRoot] :: TrayClickContext -> Int32
+ StatusNotifier.Tray: [trayEventHooks] :: TrayParams -> TrayEventHooks
+ StatusNotifier.Tray: [trayPixbufTransform] :: TrayParams -> Maybe PixbufTransform
+ StatusNotifier.Tray: [trayPriorityConfig] :: TrayParams -> TrayPriorityConfig
+ StatusNotifier.Tray: data TrayClickContext
+ StatusNotifier.Tray: data TrayClickDecision
+ StatusNotifier.Tray: data TrayEventHooks
+ StatusNotifier.Tray: defaultTrayEventHooks :: TrayEventHooks
+ StatusNotifier.Tray: instance GHC.Classes.Eq StatusNotifier.Tray.TrayClickAction
+ StatusNotifier.Tray: instance GHC.Classes.Eq StatusNotifier.Tray.TrayClickDecision
+ StatusNotifier.Tray: instance GHC.Internal.Show.Show StatusNotifier.Tray.TrayClickAction
+ StatusNotifier.Tray: instance GHC.Internal.Show.Show StatusNotifier.Tray.TrayClickDecision
+ StatusNotifier.Tray: type TrayClickHook = TrayClickContext -> IO TrayClickDecision
- StatusNotifier.Tray: TrayParams :: Orientation -> TrayImageSize -> Bool -> TrayIconPreference -> StrutAlignment -> Rational -> TrayClickAction -> TrayClickAction -> TrayClickAction -> MenuBackend -> Bool -> TrayParams
+ StatusNotifier.Tray: TrayParams :: Orientation -> TrayImageSize -> Bool -> TrayIconPreference -> StrutAlignment -> Rational -> TrayClickAction -> TrayClickAction -> TrayClickAction -> MenuBackend -> Bool -> TrayPriorityConfig -> Maybe PixbufTransform -> TrayEventHooks -> TrayParams

Files

ChangeLog.md view
@@ -1,5 +1,14 @@ # Changelog for gtk-sni-tray +## 0.2.0.0++- Breaking: remove `buildTrayWithPriority`, `buildTrayWithPixbufTransform`,+  and `buildTrayWithPriorityAndPixbufTransform`.+- Consolidate tray configuration into `TrayParams` with new fields:+  `trayPriorityConfig`, `trayPixbufTransform`, and `trayEventHooks`.+- Add click interception hooks via `TrayEventHooks`/`TrayClickHook` so callers+  can run custom per-item behavior and choose default/override/consume.+ ## 0.1.15.0  - Fix: respect the user's GTK icon theme when an SNI item provides IconThemePath
app/Main.hs view
@@ -1,706 +1,780 @@ {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-deprecations #-}-module Main where--import           Control.Monad-import           DBus.Client-import           Data.Int-import           Data.Char (toLower)-import           Data.Maybe-import           Data.Ratio-import qualified Data.Map.Strict as Map-import qualified Data.Text as T-import           Data.Version (showVersion)-import qualified DBus as DBus-import qualified GI.Gdk as Gdk-import qualified GI.Gtk as Gtk-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-import           System.Posix.Process-import           System.Environment (lookupEnv)-import           System.Exit (exitFailure)-import           Text.Printf--import           Paths_gtk_sni_tray (version)--import qualified GI.GtkLayerShell as GtkLayerShell--data Backend = BackendX11 | BackendWayland deriving (Eq, Show)--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-  pure $ if supported then BackendWayland else BackendX11--backendChoiceP :: Parser BackendChoice-backendChoiceP =-  option (eitherReader parseBackendChoice)-  (  long "backend"-  <> help "Backend selection: auto | x11 | wayland"-  <> value BackendAuto-  <> metavar "BACKEND"-  )-  where-    parseBackendChoice s =-      case map toLower s of-        "auto" -> Right BackendAuto-        "x11" -> Right BackendX11Choice-        "wayland" -> Right BackendWaylandChoice-        _ -> Left "expected one of: auto, x11, wayland"--logRuntimeInfo :: BackendChoice -> Backend -> IO ()-logRuntimeInfo backendChoice backend = do-  sessionType <- lookupEnv "XDG_SESSION_TYPE"-  waylandDisplay <- lookupEnv "WAYLAND_DISPLAY"-  gdkBackend <- lookupEnv "GDK_BACKEND"-  mDisplay <- Gdk.displayGetDefault-  displayName <- case mDisplay of-    Nothing -> pure Nothing-    Just d -> Just <$> Gdk.displayGetName d-  layerShellSupported <- GtkLayerShell.isSupported-  logM "StatusNotifier.StandaloneWindow" INFO $-    printf "backendChoice=%s backend=%s layerShellSupported=%s XDG_SESSION_TYPE=%s WAYLAND_DISPLAY=%s GDK_BACKEND=%s gdkDisplay=%s"-      (show backendChoice)-      (show backend)-      (show layerShellSupported)-      (show sessionType)-      (show waylandDisplay)-      (show gdkBackend)-      (show displayName)--hasStatusNotifierWatcher :: Client -> IO Bool-hasStatusNotifierWatcher client = do-  let mc =-        (DBus.methodCall dbusPath-          (DBus.interfaceName_ "org.freedesktop.DBus")-          (DBus.memberName_ "NameHasOwner"))-        { DBus.methodCallDestination = Just dbusName-        , DBus.methodCallBody = [DBus.toVariant ("org.kde.StatusNotifierWatcher" :: String)]-        }-  reply <- call_ client mc-  case DBus.methodReturnBody reply of-    [v] -> pure $ fromMaybe False (DBus.fromVariant v)-    _ -> pure False--setupLayerShellWindow :: StrutConfig -> Gtk.Window -> Bool -> IO ()-setupLayerShellWindow StrutConfig-                      { strutWidth = widthSize-                      , strutHeight = heightSize-                      , strutXPadding = xpadding-                      , strutYPadding = ypadding-                      , strutMonitor = monitorNumber-                      , strutPosition = position-                      , strutAlignment = alignment-                      , strutDisplayName = maybeDisplayName-                      } window reserveSpace = do-  supported <- GtkLayerShell.isSupported-  unless supported $-    logM "StatusNotifier.StandaloneWindow" WARNING $-      "Wayland detected, but gtk-layer-shell is not supported; falling back to a regular toplevel window"-  when supported $ do-    Gtk.windowSetDecorated window False--    maybeDisplay <- maybe Gdk.displayGetDefault Gdk.displayOpen maybeDisplayName-    case maybeDisplay of-      Nothing -> logM "StatusNotifier.StandaloneWindow" WARNING "Failed to get GDK display for layer-shell"-      Just display -> do-        nMonitors <- Gdk.displayGetNMonitors display-        logM "StatusNotifier.StandaloneWindow" INFO $ printf "GDK monitors reported: %d" nMonitors--        let tryIndex idx = if idx < 0 || idx >= nMonitors then pure Nothing else Gdk.displayGetMonitor display idx--        mPrimary <- Gdk.displayGetPrimaryMonitor display-        mChosen <- case monitorNumber of-          Nothing -> pure mPrimary-          Just idx -> tryIndex idx--        monitor <--          case mChosen <|> mPrimary of-            Just m -> pure (Just m)-            Nothing -> tryIndex 0--        GtkLayerShell.initForWindow window-        GtkLayerShell.setKeyboardMode window GtkLayerShell.KeyboardModeNone-        GtkLayerShell.setNamespace window (T.pack "gtk-sni-tray")-        GtkLayerShell.setLayer window GtkLayerShell.LayerTop--        GtkLayerShell.setMargin window GtkLayerShell.EdgeLeft xpadding-        GtkLayerShell.setMargin window GtkLayerShell.EdgeRight xpadding-        GtkLayerShell.setMargin window GtkLayerShell.EdgeTop ypadding-        GtkLayerShell.setMargin window GtkLayerShell.EdgeBottom ypadding--        let setAnchor = GtkLayerShell.setAnchor window-        case position of-          TopPos -> do-            setAnchor GtkLayerShell.EdgeTop True-            setAnchor GtkLayerShell.EdgeBottom False-            setAnchor GtkLayerShell.EdgeLeft True-            setAnchor GtkLayerShell.EdgeRight True-          BottomPos -> do-            setAnchor GtkLayerShell.EdgeTop False-            setAnchor GtkLayerShell.EdgeBottom True-            setAnchor GtkLayerShell.EdgeLeft True-            setAnchor GtkLayerShell.EdgeRight True-          LeftPos -> do-            setAnchor GtkLayerShell.EdgeLeft True-            setAnchor GtkLayerShell.EdgeRight False-            setAnchor GtkLayerShell.EdgeTop True-            setAnchor GtkLayerShell.EdgeBottom True-          RightPos -> do-            setAnchor GtkLayerShell.EdgeLeft False-            setAnchor GtkLayerShell.EdgeRight True-            setAnchor GtkLayerShell.EdgeTop True-            setAnchor GtkLayerShell.EdgeBottom True--        let fallbackExclusive =-              if reserveSpace-              then case position of-                     TopPos -> case heightSize of ExactSize h -> h + 2 * ypadding; _ -> 0-                     BottomPos -> case heightSize of ExactSize h -> h + 2 * ypadding; _ -> 0-                     LeftPos -> case widthSize of ExactSize w -> w + 2 * xpadding; _ -> 0-                     RightPos -> case widthSize of ExactSize w -> w + 2 * xpadding; _ -> 0-              else 0-        GtkLayerShell.setExclusiveZone window fallbackExclusive--        case monitor of-          Nothing -> logM "StatusNotifier.StandaloneWindow" WARNING "Failed to select a GDK monitor for layer-shell; using fallback sizing/anchors"-          Just m -> do-            GtkLayerShell.setMonitor window m-            isPrim <- Gdk.monitorIsPrimary m-            model <- Gdk.monitorGetModel m-            manuf <- Gdk.monitorGetManufacturer m-            logM "StatusNotifier.StandaloneWindow" INFO $-              printf "Using monitor primary=%s manufacturer=%s model=%s"-                (show isPrim) (show manuf) (show model)--            monitorGeometry <- Gdk.monitorGetGeometry m-            monitorWidth <- Gdk.getRectangleWidth monitorGeometry-            monitorHeight <- Gdk.getRectangleHeight monitorGeometry-            let availableWidth = monitorWidth - (2 * xpadding)-                availableHeight = monitorHeight - (2 * ypadding)-                width =-                  case widthSize of-                    ExactSize w -> w-                    ScreenRatio p ->-                      floor $ p * fromIntegral availableWidth-                height =-                  case heightSize of-                    ExactSize h -> h-                    ScreenRatio p ->-                      floor $ p * fromIntegral availableHeight-                clampNonNegative x = if x < 0 then 0 else x-                centerOffset availSize size =-                  clampNonNegative $ (availSize - size) `div` 2-                endOffset availSize size =-                  clampNonNegative $ availSize - size--                (leftMargin, rightMargin, topMargin, bottomMargin) =-                  case position of-                    TopPos ->-                      let offset =-                            if width >= availableWidth-                            then 0-                            else case alignment of-                                   Beginning -> 0-                                   Center -> centerOffset availableWidth width-                                   End -> endOffset availableWidth width-                          l = xpadding + offset-                          r = xpadding-                      in (l, r, ypadding, ypadding)-                    BottomPos ->-                      let offset =-                            if width >= availableWidth-                            then 0-                            else case alignment of-                                   Beginning -> 0-                                   Center -> centerOffset availableWidth width-                                   End -> endOffset availableWidth width-                          l = xpadding + offset-                          r = xpadding-                      in (l, r, ypadding, ypadding)-                    LeftPos ->-                      let offset =-                            if height >= availableHeight-                            then 0-                            else case alignment of-                                   Beginning -> 0-                                   Center -> centerOffset availableHeight height-                                   End -> endOffset availableHeight height-                          t = ypadding + offset-                          b = ypadding-                      in (xpadding, xpadding, t, b)-                    RightPos ->-                      let offset =-                            if height >= availableHeight-                            then 0-                            else case alignment of-                                   Beginning -> 0-                                   Center -> centerOffset availableHeight height-                                   End -> endOffset availableHeight height-                          t = ypadding + offset-                          b = ypadding-                      in (xpadding, xpadding, t, b)--                exclusive =-                  if reserveSpace-                  then case position of-                         TopPos -> height + topMargin-                         BottomPos -> height + bottomMargin-                         LeftPos -> width + leftMargin-                         RightPos -> width + rightMargin-                  else 0--            Gtk.windowSetDefaultSize window (fromIntegral width) (fromIntegral height)-            let (reqWidth, reqHeight) =-                  case position of-                    TopPos -> (min width availableWidth, height)-                    BottomPos -> (min width availableWidth, height)-                    LeftPos -> (width, min height availableHeight)-                    RightPos -> (width, min height availableHeight)-            Gtk.widgetSetSizeRequest window-              (fromIntegral reqWidth)-              (fromIntegral reqHeight)--            GtkLayerShell.setMargin window GtkLayerShell.EdgeLeft leftMargin-            GtkLayerShell.setMargin window GtkLayerShell.EdgeRight rightMargin-            GtkLayerShell.setMargin window GtkLayerShell.EdgeTop topMargin-            GtkLayerShell.setMargin window GtkLayerShell.EdgeBottom bottomMargin--            case position of-              TopPos -> do-                setAnchor GtkLayerShell.EdgeTop True-                setAnchor GtkLayerShell.EdgeBottom False-                if width >= availableWidth-                then do-                  setAnchor GtkLayerShell.EdgeLeft True-                  setAnchor GtkLayerShell.EdgeRight True-                else case alignment of-                       Beginning -> do-                         setAnchor GtkLayerShell.EdgeLeft True-                         setAnchor GtkLayerShell.EdgeRight False-                       Center -> do-                         setAnchor GtkLayerShell.EdgeLeft True-                         setAnchor GtkLayerShell.EdgeRight False-                       End -> do-                         setAnchor GtkLayerShell.EdgeLeft False-                         setAnchor GtkLayerShell.EdgeRight True-              BottomPos -> do-                setAnchor GtkLayerShell.EdgeTop False-                setAnchor GtkLayerShell.EdgeBottom True-                if width >= availableWidth-                then do-                  setAnchor GtkLayerShell.EdgeLeft True-                  setAnchor GtkLayerShell.EdgeRight True-                else case alignment of-                       Beginning -> do-                         setAnchor GtkLayerShell.EdgeLeft True-                         setAnchor GtkLayerShell.EdgeRight False-                       Center -> do-                         setAnchor GtkLayerShell.EdgeLeft True-                         setAnchor GtkLayerShell.EdgeRight False-                       End -> do-                         setAnchor GtkLayerShell.EdgeLeft False-                         setAnchor GtkLayerShell.EdgeRight True-              LeftPos -> do-                setAnchor GtkLayerShell.EdgeLeft True-                setAnchor GtkLayerShell.EdgeRight False-                if height >= availableHeight-                then do-                  setAnchor GtkLayerShell.EdgeTop True-                  setAnchor GtkLayerShell.EdgeBottom True-                else case alignment of-                       Beginning -> do-                         setAnchor GtkLayerShell.EdgeTop True-                         setAnchor GtkLayerShell.EdgeBottom False-                       Center -> do-                         setAnchor GtkLayerShell.EdgeTop True-                         setAnchor GtkLayerShell.EdgeBottom False-                       End -> do-                         setAnchor GtkLayerShell.EdgeTop False-                         setAnchor GtkLayerShell.EdgeBottom True-              RightPos -> do-                setAnchor GtkLayerShell.EdgeLeft False-                setAnchor GtkLayerShell.EdgeRight True-                if height >= availableHeight-                then do-                  setAnchor GtkLayerShell.EdgeTop True-                  setAnchor GtkLayerShell.EdgeBottom True-                else case alignment of-                       Beginning -> do-                         setAnchor GtkLayerShell.EdgeTop True-                         setAnchor GtkLayerShell.EdgeBottom False-                       Center -> do-                         setAnchor GtkLayerShell.EdgeTop True-                         setAnchor GtkLayerShell.EdgeBottom False-                       End -> do-                         setAnchor GtkLayerShell.EdgeTop False-                         setAnchor GtkLayerShell.EdgeBottom True--            GtkLayerShell.setExclusiveZone window exclusive--positionP :: Parser StrutPosition-positionP = fromMaybe TopPos <$> optional-  (   flag' TopPos-  (  long "top"-  <> help "Position the bar at the top of the screen"-  )-  <|> flag' BottomPos-  (  long "bottom"-  <> help "Position the bar at the bottom of the screen"-  )-  <|> flag' LeftPos-  (  long "left"-  <> help "Position the bar on the left side of the screen"-  )-  <|> flag' RightPos-  (  long "right"-  <> help "Position the bar on the right side of the screen"-  ))--alignmentP :: Parser StrutAlignment-alignmentP = fromMaybe Center <$> optional-  (   flag' Beginning-  (  long "beginning"-  <> help "Use beginning alignment"-  )-  <|> flag' Center-  (  long "center"-  <> help "Use center alignment"-  )-  <|> flag' End-  (  long "end"-  <> help "Use end alignment"-  ))--sizeP :: Parser Int32-sizeP =-  option auto-  (  long "size"-  <> short 's'-  <> help "Set the size of the bar"-  <> value 30-  <> metavar "SIZE"-  )--paddingP :: Parser Int32-paddingP =-  option auto-  (  long "padding"-  <> short 'p'-  <> help "Set the padding of the bar"-  <> value 0-  <> metavar "PADDING"-  )--monitorNumberP :: Parser [Int32]-monitorNumberP = many $-  option auto-  (  long "monitor"-  <> short 'm'-  <> help "Display a tray bar on the given monitor"-  <> metavar "MONITOR"-  )--logP :: Parser Priority-logP =-  option auto-  (  long "log-level"-  <> short 'l'-  <> help "Set the log level"-  <> metavar "LEVEL"-  <> value WARNING-  )--colorP :: Parser (Maybe String)-colorP = optional $-  strOption-  (  long "color"-  <> short 'c'-  <> help "Set the background color of the tray; See https://developer.gnome.org/gdk3/stable/gdk3-RGBA-Colors.html#gdk-rgba-parse for acceptable values"-  <> metavar "COLOR"-  )--expandP :: Parser Bool-expandP =-  switch-  (  long "expand"-  <> help "Let icons expand into the space allocated to the tray"-  <> short 'e'-  )--centerIconsP :: Parser Bool-centerIconsP =-  switch-  (  long "center-icons"-  <> help "Center the tray icons within the bar"-  )--startWatcherP :: Parser Bool-startWatcherP =-  switch-  (  long "watcher"-  <> short 'w'-  <> help "Start a Watcher to handle SNI registration if one does not exist"-  )--noStrutP :: Parser Bool-noStrutP =-  switch-  (  long "no-strut"-  <> help "Do not reserve space for the window (X11: no strut; Wayland: exclusive zone 0)"-  )--barLengthP :: Parser Rational-barLengthP =-  option auto-  (  long "length"-  <> help "Set the proportion of the screen that the tray bar should occupy -- values are parsed as haskell rationals (e.g. 1 % 2)"-  <> value 1-  )--overlayScaleP :: Parser Rational-overlayScaleP =-  option auto-  (  long "overlay-scale"-  <> short 'o'-  <> help "The proportion of the tray icon's size that should be set for overlay icons."-  <> 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)-  (  long "menu-backend"-  <> help "Menu backend: haskell (default) | libdbusmenu"-  <> value HaskellDBusMenu-  <> metavar "BACKEND"-  )-  where-    parseMenuBackend s =-      case map toLower s of-        "libdbusmenu" -> Right LibDBusMenu-        "haskell" -> Right HaskellDBusMenu-        _ -> Left "expected one of: libdbusmenu, haskell"--getColor :: String -> IO Gdk.RGBA-getColor colorString = do-  rgba <- Gdk.newZeroRGBA-  colorParsed <- Gdk.rGBAParse rgba (T.pack colorString)-  unless colorParsed $ do-    logM "StatusNotifier.Tray" WARNING "Failed to parse provided color"-    void $ Gdk.rGBAParse rgba "#000000"-  return rgba--buildWindows :: StrutPosition-             -> StrutAlignment-             -> Int32-             -> Int32-             -> [Int32]-             -> Priority-             -> BackendChoice-             -> Maybe String-             -> Bool-             -> Bool-             -> Bool-             -> Bool-             -> Rational-             -> Rational-             -> MenuBackend-             -> TrayIconPreference-             -> IconRecolorMode-             -> IO ()-buildWindows pos align size padding monitors priority backendChoice maybeColorString expand-             centerIcons startWatcher noStrut barLength overlayScale menuBackend iconPreference iconRecolorMode = do-  _ <- Gtk.init Nothing-  logger <- getLogger "StatusNotifier"-  saveGlobalLogger $ setLevel priority logger-  detectedBackend <- detectBackend-  let backend =-        case backendChoice of-          BackendAuto -> detectedBackend-          BackendX11Choice -> BackendX11-          BackendWaylandChoice -> BackendWayland-  client <- connectSession-  logRuntimeInfo backendChoice backend-  watcherPresent <- hasStatusNotifierWatcher client-  unless watcherPresent $ do-    logM "StatusNotifier" WARNING $-      "No StatusNotifierWatcher found on D-Bus (org.kde.StatusNotifierWatcher). Tray will likely be empty."-    unless startWatcher $-      logM "StatusNotifier" WARNING $-        "Start a watcher first (recommended) or run with --watcher to start one in-process."-  _ <- getRootLogger-  pid <- getProcessID-  host <--    Host.build-      Host.defaultParams-        { Host.dbusClient = Just client-        , Host.uniqueIdentifier = printf "standalone-%s" $ show pid-        , Host.startWatcher = startWatcher-        }-      >>= maybe (logM "StatusNotifier" ERROR "Failed to start StatusNotifier host" >> exitFailure) pure-  initialItems <- Host.itemInfoMap host-  logM "StatusNotifier" INFO $ printf "Initial tray items: %d" (Map.size initialItems)-  let c1 =-        defaultStrutConfig-        { strutPosition = pos-        , strutAlignment = align-        , strutXPadding = padding-        , strutYPadding = padding-        }-      defaultRatio = ScreenRatio barLength-      configBase =-        case pos of-          TopPos -> c1 {strutHeight = ExactSize size, strutWidth = defaultRatio}-          BottomPos ->-            c1 {strutHeight = ExactSize size, strutWidth = defaultRatio}-          RightPos ->-            c1 {strutHeight = defaultRatio, strutWidth = ExactSize size}-          LeftPos ->-            c1 {strutHeight = defaultRatio, strutWidth = ExactSize size}-      buildWithConfig config = do-        let orientation =-              case strutPosition config of-                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 <--          buildTrayWithPixbufTransform host client-            TrayParams-            { trayOrientation = orientation-            , trayImageSize = Expand-            , trayIconExpand = expand-            , trayIconPreference = iconPreference-            , trayAlignment = align-            , trayOverlayScale = overlayScale-            , trayLeftClickAction = Activate-            , trayMiddleClickAction = SecondaryActivate-            , trayRightClickAction = PopupMenu-            , trayMenuBackend = menuBackend-            , trayCenterIcons = centerIcons-            }-            mkIconTransform-        window <- Gtk.windowNew Gtk.WindowTypeToplevel-        Gtk.windowSetResizable window False-        Gtk.windowSetSkipTaskbarHint window True-        Gtk.windowSetSkipPagerHint window True-        Gtk.windowSetAcceptFocus window False-        Gtk.windowSetFocusOnMap window False-        Gtk.windowSetKeepAbove window True-        Gtk.windowSetTypeHint window Gdk.WindowTypeHintDock-        case backend of-          BackendX11 ->-            when (not noStrut) $ setupStrutWindow config window-          BackendWayland ->-            setupLayerShellWindow config window (not noStrut)-        maybe-          (makeWindowTransparent window)-          (getColor >=>-           Gtk.widgetOverrideBackgroundColor window [Gtk.StateFlagsNormal] .-           Just)-          maybeColorString-        Gtk.containerAdd window tray-        Gtk.widgetShowAll window-      runForMonitor monitor =-        buildWithConfig configBase {strutMonitor = Just monitor}-  if null monitors-    then buildWithConfig configBase-    else mapM_ runForMonitor monitors-  Gtk.main--parser :: Parser (IO ())-parser =-  buildWindows <$> positionP <*> alignmentP <*> sizeP <*> paddingP <*>-  monitorNumberP <*> logP <*> backendChoiceP <*> colorP <*> expandP <*>-  centerIconsP <*> startWatcherP <*>-  noStrutP <*> barLengthP <*> overlayScaleP <*> menuBackendP <*> iconPreferenceP <*> iconRecolorModeP--versionOption :: Parser (a -> a)-versionOption = infoOption-                (printf "gtk-sni-tray-standalone %s" $ showVersion version)-                (  long "version"-                <> help "Show the version number of gtk-sni-tray"-                )--main :: IO ()-main =-  join $ execParser $ info (helper <*> versionOption <*> parser)-         (  fullDesc-         <> progDesc "Run a standalone StatusNotifierItem/AppIndicator tray"-         )++module Main where++import Control.Monad+import qualified DBus as DBus+import DBus.Client+import Data.Char (toLower)+import Data.Int+import qualified Data.Map.Strict as Map+import Data.Maybe+import Data.Ratio+import qualified Data.Text as T+import Data.Version (showVersion)+import qualified GI.Gdk as Gdk+import qualified GI.Gtk as Gtk+import qualified GI.GtkLayerShell as GtkLayerShell+import Graphics.UI.GIGtkStrut+import Options.Applicative+import Paths_gtk_sni_tray (version)+import qualified StatusNotifier.Host.Service as Host+import qualified StatusNotifier.Icon.Pixbuf as IconPixbuf+import StatusNotifier.TransparentWindow+import StatusNotifier.Tray+import System.Environment (lookupEnv)+import System.Exit (exitFailure)+import System.Log.Logger+import System.Posix.Process+import Text.Printf++data Backend = BackendX11 | BackendWayland deriving (Eq, Show)++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+  pure $ if supported then BackendWayland else BackendX11++backendChoiceP :: Parser BackendChoice+backendChoiceP =+  option+    (eitherReader parseBackendChoice)+    ( long "backend"+        <> help "Backend selection: auto | x11 | wayland"+        <> value BackendAuto+        <> metavar "BACKEND"+    )+  where+    parseBackendChoice s =+      case map toLower s of+        "auto" -> Right BackendAuto+        "x11" -> Right BackendX11Choice+        "wayland" -> Right BackendWaylandChoice+        _ -> Left "expected one of: auto, x11, wayland"++logRuntimeInfo :: BackendChoice -> Backend -> IO ()+logRuntimeInfo backendChoice backend = do+  sessionType <- lookupEnv "XDG_SESSION_TYPE"+  waylandDisplay <- lookupEnv "WAYLAND_DISPLAY"+  gdkBackend <- lookupEnv "GDK_BACKEND"+  mDisplay <- Gdk.displayGetDefault+  displayName <- case mDisplay of+    Nothing -> pure Nothing+    Just d -> Just <$> Gdk.displayGetName d+  layerShellSupported <- GtkLayerShell.isSupported+  logM "StatusNotifier.StandaloneWindow" INFO $+    printf+      "backendChoice=%s backend=%s layerShellSupported=%s XDG_SESSION_TYPE=%s WAYLAND_DISPLAY=%s GDK_BACKEND=%s gdkDisplay=%s"+      (show backendChoice)+      (show backend)+      (show layerShellSupported)+      (show sessionType)+      (show waylandDisplay)+      (show gdkBackend)+      (show displayName)++hasStatusNotifierWatcher :: Client -> IO Bool+hasStatusNotifierWatcher client = do+  let mc =+        ( DBus.methodCall+            dbusPath+            (DBus.interfaceName_ "org.freedesktop.DBus")+            (DBus.memberName_ "NameHasOwner")+        )+          { DBus.methodCallDestination = Just dbusName,+            DBus.methodCallBody = [DBus.toVariant ("org.kde.StatusNotifierWatcher" :: String)]+          }+  reply <- call_ client mc+  case DBus.methodReturnBody reply of+    [v] -> pure $ fromMaybe False (DBus.fromVariant v)+    _ -> pure False++setupLayerShellWindow :: StrutConfig -> Gtk.Window -> Bool -> IO ()+setupLayerShellWindow+  StrutConfig+    { strutWidth = widthSize,+      strutHeight = heightSize,+      strutXPadding = xpadding,+      strutYPadding = ypadding,+      strutMonitor = monitorNumber,+      strutPosition = position,+      strutAlignment = alignment,+      strutDisplayName = maybeDisplayName+    }+  window+  reserveSpace = do+    supported <- GtkLayerShell.isSupported+    unless supported $+      logM "StatusNotifier.StandaloneWindow" WARNING $+        "Wayland detected, but gtk-layer-shell is not supported; falling back to a regular toplevel window"+    when supported $ do+      Gtk.windowSetDecorated window False++      maybeDisplay <- maybe Gdk.displayGetDefault Gdk.displayOpen maybeDisplayName+      case maybeDisplay of+        Nothing -> logM "StatusNotifier.StandaloneWindow" WARNING "Failed to get GDK display for layer-shell"+        Just display -> do+          nMonitors <- Gdk.displayGetNMonitors display+          logM "StatusNotifier.StandaloneWindow" INFO $ printf "GDK monitors reported: %d" nMonitors++          let tryIndex idx = if idx < 0 || idx >= nMonitors then pure Nothing else Gdk.displayGetMonitor display idx++          mPrimary <- Gdk.displayGetPrimaryMonitor display+          mChosen <- case monitorNumber of+            Nothing -> pure mPrimary+            Just idx -> tryIndex idx++          monitor <-+            case mChosen <|> mPrimary of+              Just m -> pure (Just m)+              Nothing -> tryIndex 0++          GtkLayerShell.initForWindow window+          GtkLayerShell.setKeyboardMode window GtkLayerShell.KeyboardModeNone+          GtkLayerShell.setNamespace window (T.pack "gtk-sni-tray")+          GtkLayerShell.setLayer window GtkLayerShell.LayerTop++          GtkLayerShell.setMargin window GtkLayerShell.EdgeLeft xpadding+          GtkLayerShell.setMargin window GtkLayerShell.EdgeRight xpadding+          GtkLayerShell.setMargin window GtkLayerShell.EdgeTop ypadding+          GtkLayerShell.setMargin window GtkLayerShell.EdgeBottom ypadding++          let setAnchor = GtkLayerShell.setAnchor window+          case position of+            TopPos -> do+              setAnchor GtkLayerShell.EdgeTop True+              setAnchor GtkLayerShell.EdgeBottom False+              setAnchor GtkLayerShell.EdgeLeft True+              setAnchor GtkLayerShell.EdgeRight True+            BottomPos -> do+              setAnchor GtkLayerShell.EdgeTop False+              setAnchor GtkLayerShell.EdgeBottom True+              setAnchor GtkLayerShell.EdgeLeft True+              setAnchor GtkLayerShell.EdgeRight True+            LeftPos -> do+              setAnchor GtkLayerShell.EdgeLeft True+              setAnchor GtkLayerShell.EdgeRight False+              setAnchor GtkLayerShell.EdgeTop True+              setAnchor GtkLayerShell.EdgeBottom True+            RightPos -> do+              setAnchor GtkLayerShell.EdgeLeft False+              setAnchor GtkLayerShell.EdgeRight True+              setAnchor GtkLayerShell.EdgeTop True+              setAnchor GtkLayerShell.EdgeBottom True++          let fallbackExclusive =+                if reserveSpace+                  then case position of+                    TopPos -> case heightSize of ExactSize h -> h + 2 * ypadding; _ -> 0+                    BottomPos -> case heightSize of ExactSize h -> h + 2 * ypadding; _ -> 0+                    LeftPos -> case widthSize of ExactSize w -> w + 2 * xpadding; _ -> 0+                    RightPos -> case widthSize of ExactSize w -> w + 2 * xpadding; _ -> 0+                  else 0+          GtkLayerShell.setExclusiveZone window fallbackExclusive++          case monitor of+            Nothing -> logM "StatusNotifier.StandaloneWindow" WARNING "Failed to select a GDK monitor for layer-shell; using fallback sizing/anchors"+            Just m -> do+              GtkLayerShell.setMonitor window m+              isPrim <- Gdk.monitorIsPrimary m+              model <- Gdk.monitorGetModel m+              manuf <- Gdk.monitorGetManufacturer m+              logM "StatusNotifier.StandaloneWindow" INFO $+                printf+                  "Using monitor primary=%s manufacturer=%s model=%s"+                  (show isPrim)+                  (show manuf)+                  (show model)++              monitorGeometry <- Gdk.monitorGetGeometry m+              monitorWidth <- Gdk.getRectangleWidth monitorGeometry+              monitorHeight <- Gdk.getRectangleHeight monitorGeometry+              let availableWidth = monitorWidth - (2 * xpadding)+                  availableHeight = monitorHeight - (2 * ypadding)+                  width =+                    case widthSize of+                      ExactSize w -> w+                      ScreenRatio p ->+                        floor $ p * fromIntegral availableWidth+                  height =+                    case heightSize of+                      ExactSize h -> h+                      ScreenRatio p ->+                        floor $ p * fromIntegral availableHeight+                  clampNonNegative x = if x < 0 then 0 else x+                  centerOffset availSize size =+                    clampNonNegative $ (availSize - size) `div` 2+                  endOffset availSize size =+                    clampNonNegative $ availSize - size++                  (leftMargin, rightMargin, topMargin, bottomMargin) =+                    case position of+                      TopPos ->+                        let offset =+                              if width >= availableWidth+                                then 0+                                else case alignment of+                                  Beginning -> 0+                                  Center -> centerOffset availableWidth width+                                  End -> endOffset availableWidth width+                            l = xpadding + offset+                            r = xpadding+                         in (l, r, ypadding, ypadding)+                      BottomPos ->+                        let offset =+                              if width >= availableWidth+                                then 0+                                else case alignment of+                                  Beginning -> 0+                                  Center -> centerOffset availableWidth width+                                  End -> endOffset availableWidth width+                            l = xpadding + offset+                            r = xpadding+                         in (l, r, ypadding, ypadding)+                      LeftPos ->+                        let offset =+                              if height >= availableHeight+                                then 0+                                else case alignment of+                                  Beginning -> 0+                                  Center -> centerOffset availableHeight height+                                  End -> endOffset availableHeight height+                            t = ypadding + offset+                            b = ypadding+                         in (xpadding, xpadding, t, b)+                      RightPos ->+                        let offset =+                              if height >= availableHeight+                                then 0+                                else case alignment of+                                  Beginning -> 0+                                  Center -> centerOffset availableHeight height+                                  End -> endOffset availableHeight height+                            t = ypadding + offset+                            b = ypadding+                         in (xpadding, xpadding, t, b)++                  exclusive =+                    if reserveSpace+                      then case position of+                        TopPos -> height + topMargin+                        BottomPos -> height + bottomMargin+                        LeftPos -> width + leftMargin+                        RightPos -> width + rightMargin+                      else 0++              Gtk.windowSetDefaultSize window (fromIntegral width) (fromIntegral height)+              let (reqWidth, reqHeight) =+                    case position of+                      TopPos -> (min width availableWidth, height)+                      BottomPos -> (min width availableWidth, height)+                      LeftPos -> (width, min height availableHeight)+                      RightPos -> (width, min height availableHeight)+              Gtk.widgetSetSizeRequest+                window+                (fromIntegral reqWidth)+                (fromIntegral reqHeight)++              GtkLayerShell.setMargin window GtkLayerShell.EdgeLeft leftMargin+              GtkLayerShell.setMargin window GtkLayerShell.EdgeRight rightMargin+              GtkLayerShell.setMargin window GtkLayerShell.EdgeTop topMargin+              GtkLayerShell.setMargin window GtkLayerShell.EdgeBottom bottomMargin++              case position of+                TopPos -> do+                  setAnchor GtkLayerShell.EdgeTop True+                  setAnchor GtkLayerShell.EdgeBottom False+                  if width >= availableWidth+                    then do+                      setAnchor GtkLayerShell.EdgeLeft True+                      setAnchor GtkLayerShell.EdgeRight True+                    else case alignment of+                      Beginning -> do+                        setAnchor GtkLayerShell.EdgeLeft True+                        setAnchor GtkLayerShell.EdgeRight False+                      Center -> do+                        setAnchor GtkLayerShell.EdgeLeft True+                        setAnchor GtkLayerShell.EdgeRight False+                      End -> do+                        setAnchor GtkLayerShell.EdgeLeft False+                        setAnchor GtkLayerShell.EdgeRight True+                BottomPos -> do+                  setAnchor GtkLayerShell.EdgeTop False+                  setAnchor GtkLayerShell.EdgeBottom True+                  if width >= availableWidth+                    then do+                      setAnchor GtkLayerShell.EdgeLeft True+                      setAnchor GtkLayerShell.EdgeRight True+                    else case alignment of+                      Beginning -> do+                        setAnchor GtkLayerShell.EdgeLeft True+                        setAnchor GtkLayerShell.EdgeRight False+                      Center -> do+                        setAnchor GtkLayerShell.EdgeLeft True+                        setAnchor GtkLayerShell.EdgeRight False+                      End -> do+                        setAnchor GtkLayerShell.EdgeLeft False+                        setAnchor GtkLayerShell.EdgeRight True+                LeftPos -> do+                  setAnchor GtkLayerShell.EdgeLeft True+                  setAnchor GtkLayerShell.EdgeRight False+                  if height >= availableHeight+                    then do+                      setAnchor GtkLayerShell.EdgeTop True+                      setAnchor GtkLayerShell.EdgeBottom True+                    else case alignment of+                      Beginning -> do+                        setAnchor GtkLayerShell.EdgeTop True+                        setAnchor GtkLayerShell.EdgeBottom False+                      Center -> do+                        setAnchor GtkLayerShell.EdgeTop True+                        setAnchor GtkLayerShell.EdgeBottom False+                      End -> do+                        setAnchor GtkLayerShell.EdgeTop False+                        setAnchor GtkLayerShell.EdgeBottom True+                RightPos -> do+                  setAnchor GtkLayerShell.EdgeLeft False+                  setAnchor GtkLayerShell.EdgeRight True+                  if height >= availableHeight+                    then do+                      setAnchor GtkLayerShell.EdgeTop True+                      setAnchor GtkLayerShell.EdgeBottom True+                    else case alignment of+                      Beginning -> do+                        setAnchor GtkLayerShell.EdgeTop True+                        setAnchor GtkLayerShell.EdgeBottom False+                      Center -> do+                        setAnchor GtkLayerShell.EdgeTop True+                        setAnchor GtkLayerShell.EdgeBottom False+                      End -> do+                        setAnchor GtkLayerShell.EdgeTop False+                        setAnchor GtkLayerShell.EdgeBottom True++              GtkLayerShell.setExclusiveZone window exclusive++positionP :: Parser StrutPosition+positionP =+  fromMaybe TopPos+    <$> optional+      ( flag'+          TopPos+          ( long "top"+              <> help "Position the bar at the top of the screen"+          )+          <|> flag'+            BottomPos+            ( long "bottom"+                <> help "Position the bar at the bottom of the screen"+            )+          <|> flag'+            LeftPos+            ( long "left"+                <> help "Position the bar on the left side of the screen"+            )+          <|> flag'+            RightPos+            ( long "right"+                <> help "Position the bar on the right side of the screen"+            )+      )++alignmentP :: Parser StrutAlignment+alignmentP =+  fromMaybe Center+    <$> optional+      ( flag'+          Beginning+          ( long "beginning"+              <> help "Use beginning alignment"+          )+          <|> flag'+            Center+            ( long "center"+                <> help "Use center alignment"+            )+          <|> flag'+            End+            ( long "end"+                <> help "Use end alignment"+            )+      )++sizeP :: Parser Int32+sizeP =+  option+    auto+    ( long "size"+        <> short 's'+        <> help "Set the size of the bar"+        <> value 30+        <> metavar "SIZE"+    )++paddingP :: Parser Int32+paddingP =+  option+    auto+    ( long "padding"+        <> short 'p'+        <> help "Set the padding of the bar"+        <> value 0+        <> metavar "PADDING"+    )++monitorNumberP :: Parser [Int32]+monitorNumberP =+  many $+    option+      auto+      ( long "monitor"+          <> short 'm'+          <> help "Display a tray bar on the given monitor"+          <> metavar "MONITOR"+      )++logP :: Parser Priority+logP =+  option+    auto+    ( long "log-level"+        <> short 'l'+        <> help "Set the log level"+        <> metavar "LEVEL"+        <> value WARNING+    )++colorP :: Parser (Maybe String)+colorP =+  optional $+    strOption+      ( long "color"+          <> short 'c'+          <> help "Set the background color of the tray; See https://developer.gnome.org/gdk3/stable/gdk3-RGBA-Colors.html#gdk-rgba-parse for acceptable values"+          <> metavar "COLOR"+      )++expandP :: Parser Bool+expandP =+  switch+    ( long "expand"+        <> help "Let icons expand into the space allocated to the tray"+        <> short 'e'+    )++centerIconsP :: Parser Bool+centerIconsP =+  switch+    ( long "center-icons"+        <> help "Center the tray icons within the bar"+    )++startWatcherP :: Parser Bool+startWatcherP =+  switch+    ( long "watcher"+        <> short 'w'+        <> help "Start a Watcher to handle SNI registration if one does not exist"+    )++noStrutP :: Parser Bool+noStrutP =+  switch+    ( long "no-strut"+        <> help "Do not reserve space for the window (X11: no strut; Wayland: exclusive zone 0)"+    )++barLengthP :: Parser Rational+barLengthP =+  option+    auto+    ( long "length"+        <> help "Set the proportion of the screen that the tray bar should occupy -- values are parsed as haskell rationals (e.g. 1 % 2)"+        <> value 1+    )++overlayScaleP :: Parser Rational+overlayScaleP =+  option+    auto+    ( long "overlay-scale"+        <> short 'o'+        <> help "The proportion of the tray icon's size that should be set for overlay icons."+        <> 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)+    ( long "menu-backend"+        <> help "Menu backend: haskell (default) | libdbusmenu"+        <> value HaskellDBusMenu+        <> metavar "BACKEND"+    )+  where+    parseMenuBackend s =+      case map toLower s of+        "libdbusmenu" -> Right LibDBusMenu+        "haskell" -> Right HaskellDBusMenu+        _ -> Left "expected one of: libdbusmenu, haskell"++getColor :: String -> IO Gdk.RGBA+getColor colorString = do+  rgba <- Gdk.newZeroRGBA+  colorParsed <- Gdk.rGBAParse rgba (T.pack colorString)+  unless colorParsed $ do+    logM "StatusNotifier.Tray" WARNING "Failed to parse provided color"+    void $ Gdk.rGBAParse rgba "#000000"+  return rgba++buildWindows ::+  StrutPosition ->+  StrutAlignment ->+  Int32 ->+  Int32 ->+  [Int32] ->+  Priority ->+  BackendChoice ->+  Maybe String ->+  Bool ->+  Bool ->+  Bool ->+  Bool ->+  Rational ->+  Rational ->+  MenuBackend ->+  TrayIconPreference ->+  IconRecolorMode ->+  IO ()+buildWindows+  pos+  align+  size+  padding+  monitors+  priority+  backendChoice+  maybeColorString+  expand+  centerIcons+  startWatcher+  noStrut+  barLength+  overlayScale+  menuBackend+  iconPreference+  iconRecolorMode = do+    _ <- Gtk.init Nothing+    logger <- getLogger "StatusNotifier"+    saveGlobalLogger $ setLevel priority logger+    detectedBackend <- detectBackend+    let backend =+          case backendChoice of+            BackendAuto -> detectedBackend+            BackendX11Choice -> BackendX11+            BackendWaylandChoice -> BackendWayland+    client <- connectSession+    logRuntimeInfo backendChoice backend+    watcherPresent <- hasStatusNotifierWatcher client+    unless watcherPresent $ do+      logM "StatusNotifier" WARNING $+        "No StatusNotifierWatcher found on D-Bus (org.kde.StatusNotifierWatcher). Tray will likely be empty."+      unless startWatcher $+        logM "StatusNotifier" WARNING $+          "Start a watcher first (recommended) or run with --watcher to start one in-process."+    _ <- getRootLogger+    pid <- getProcessID+    host <-+      Host.build+        Host.defaultParams+          { Host.dbusClient = Just client,+            Host.uniqueIdentifier = printf "standalone-%s" $ show pid,+            Host.startWatcher = startWatcher+          }+        >>= maybe (logM "StatusNotifier" ERROR "Failed to start StatusNotifier host" >> exitFailure) pure+    initialItems <- Host.itemInfoMap host+    logM "StatusNotifier" INFO $ printf "Initial tray items: %d" (Map.size initialItems)+    let c1 =+          defaultStrutConfig+            { strutPosition = pos,+              strutAlignment = align,+              strutXPadding = padding,+              strutYPadding = padding+            }+        defaultRatio = ScreenRatio barLength+        configBase =+          case pos of+            TopPos -> c1 {strutHeight = ExactSize size, strutWidth = defaultRatio}+            BottomPos ->+              c1 {strutHeight = ExactSize size, strutWidth = defaultRatio}+            RightPos ->+              c1 {strutHeight = defaultRatio, strutWidth = ExactSize size}+            LeftPos ->+              c1 {strutHeight = defaultRatio, strutWidth = ExactSize size}+        buildWithConfig config = do+          let orientation =+                case strutPosition config of+                  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+              TrayParams+                { trayOrientation = orientation,+                  trayImageSize = Expand,+                  trayIconExpand = expand,+                  trayIconPreference = iconPreference,+                  trayAlignment = align,+                  trayOverlayScale = overlayScale,+                  trayLeftClickAction = Activate,+                  trayMiddleClickAction = SecondaryActivate,+                  trayRightClickAction = PopupMenu,+                  trayMenuBackend = menuBackend,+                  trayCenterIcons = centerIcons,+                  trayPriorityConfig = defaultTrayPriorityConfig,+                  trayPixbufTransform = mkIconTransform,+                  trayEventHooks = defaultTrayEventHooks+                }+          window <- Gtk.windowNew Gtk.WindowTypeToplevel+          Gtk.windowSetResizable window False+          Gtk.windowSetSkipTaskbarHint window True+          Gtk.windowSetSkipPagerHint window True+          Gtk.windowSetAcceptFocus window False+          Gtk.windowSetFocusOnMap window False+          Gtk.windowSetKeepAbove window True+          Gtk.windowSetTypeHint window Gdk.WindowTypeHintDock+          case backend of+            BackendX11 ->+              when (not noStrut) $ setupStrutWindow config window+            BackendWayland ->+              setupLayerShellWindow config window (not noStrut)+          maybe+            (makeWindowTransparent window)+            ( getColor+                >=> Gtk.widgetOverrideBackgroundColor window [Gtk.StateFlagsNormal]+                  . Just+            )+            maybeColorString+          Gtk.containerAdd window tray+          Gtk.widgetShowAll window+        runForMonitor monitor =+          buildWithConfig configBase {strutMonitor = Just monitor}+    if null monitors+      then buildWithConfig configBase+      else mapM_ runForMonitor monitors+    Gtk.main++parser :: Parser (IO ())+parser =+  buildWindows+    <$> positionP+    <*> alignmentP+    <*> sizeP+    <*> paddingP+    <*> monitorNumberP+    <*> logP+    <*> backendChoiceP+    <*> colorP+    <*> expandP+    <*> centerIconsP+    <*> startWatcherP+    <*> noStrutP+    <*> barLengthP+    <*> overlayScaleP+    <*> menuBackendP+    <*> iconPreferenceP+    <*> iconRecolorModeP++versionOption :: Parser (a -> a)+versionOption =+  infoOption+    (printf "gtk-sni-tray-standalone %s" $ showVersion version)+    ( long "version"+        <> help "Show the version number of gtk-sni-tray"+    )++main :: IO ()+main =+  join $+    execParser $+      info+        (helper <*> versionOption <*> parser)+        ( fullDesc+            <> progDesc "Run a standalone StatusNotifierItem/AppIndicator tray"+        )
gtk-sni-tray.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           gtk-sni-tray-version:        0.1.15.0+version:        0.2.0.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
src/StatusNotifier/Tray.hs view
@@ -1,873 +1,993 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLabels #-}-{-# LANGUAGE CPP #-}-module StatusNotifier.Tray where--import           Control.Concurrent.MVar as MV-import           Data.IORef (newIORef, readIORef, writeIORef)-import           Control.Exception.Base-import           Control.Exception.Enclosed (catchAny)-import           Control.Monad-import           Control.Monad.Trans.Class-import           Control.Monad.Trans.Maybe-import           DBus.Client-import qualified DBus.Internal.Types as DBusTypes-import           Data.Bool (bool)-import qualified Data.ByteString as BS-import           Data.Coerce-import           Data.Foldable (traverse_)-import           Data.GI.Base (unsafeCastTo)-import           Data.GI.Base.GError-import           Data.Int-import           Data.List-import qualified Data.Map.Strict as Map-import           Data.Maybe-import           Data.Ord-import           Data.Ratio-import qualified Data.Text as T-import qualified GI.DbusmenuGtk3.Objects.Menu as DM-import qualified GI.GLib as GLib-import           GI.GLib.Structs.Bytes-import qualified GI.Gdk as Gdk-import           GI.Gdk.Enums-import           GI.Gdk.Structs.EventScroll-import           GI.GdkPixbuf.Enums-import           GI.GdkPixbuf.Objects.Pixbuf as Gdk-import qualified GI.Gtk as Gtk-import           GI.Gtk.Flags-import           GI.Gtk.Objects.IconTheme-import           Graphics.UI.GIGtkStrut-import qualified DBusMenu-import           StatusNotifier.Host.Service-import qualified StatusNotifier.Item.Client as IC-import           System.Directory-import           System.FilePath-import           System.Log.Logger-import           Text.Printf-import           Foreign.Ptr (Ptr)--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-         (show $ info { iconPixmaps = []})-         (show $ length $ iconPixmaps info)--getScaledWidthHeight :: Bool -> Int32 -> Int32 -> Int32 -> (Int32, Int32)-getScaledWidthHeight shouldTargetWidth targetSize width height =-  let getRatio :: Int32 -> Rational-      getRatio toScale =-        fromIntegral targetSize / fromIntegral toScale-      getOther :: Int32 -> Int32 -> Int32-      getOther toScale other = max 1 $ floor $ getRatio toScale * fromIntegral other-  in-    if shouldTargetWidth-    then (targetSize, getOther width height)-    else (getOther height width, targetSize)--scalePixbufToSize :: Int32 -> Gtk.Orientation -> Pixbuf -> IO Pixbuf-scalePixbufToSize size orientation pixbuf = do-  width <- pixbufGetWidth pixbuf-  height <- pixbufGetHeight pixbuf-  let warnAndReturnOrig =-        trayLogger WARNING "Unable to scale pixbuf" >> return pixbuf-  if width <= 0 || height <= 0-  then warnAndReturnOrig-  else do-    let targetWidth = case orientation of-                        Gtk.OrientationHorizontal -> False-                        _ -> True-        (scaledWidth, scaledHeight) =-          getScaledWidthHeight targetWidth size width height-    trayLogger DEBUG $-               printf-               "Scaling pb to %s, actualW: %s, actualH: %s, scaledW: %s, scaledH: %s"-               (show size) (show width) (show height)-               (show scaledWidth) (show scaledHeight)--    trayLogger DEBUG $ printf "targetW: %s, targetH: %s"-                 (show scaledWidth) (show scaledHeight)-    maybe warnAndReturnOrig return =<<-      pixbufScaleSimple pixbuf scaledWidth scaledHeight InterpTypeBilinear--themeLoadFlags :: [IconLookupFlags]-themeLoadFlags = [IconLookupFlagsGenericFallback, IconLookupFlagsUseBuiltin]--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--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)--catchGErrorsAsNothing :: IO a -> IO (Maybe a)-catchGErrorsAsNothing action = catchGErrorsAsLeft action >>= rightToJustLogLeft-       where rightToJustLogLeft (Right value) = return $ Just value-             rightToJustLogLeft (Left err) = do-               trayLogger WARNING $ printf "Encountered error: %s" $ show err-               return Nothing--safePixbufNewFromFile :: FilePath -> IO (Maybe Gdk.Pixbuf)-safePixbufNewFromFile =-  handleResult . catchGErrorsAsNothing . Gdk.pixbufNewFromFile-  where-#if MIN_VERSION_gi_gdkpixbuf(2,0,26)-    handleResult = fmap join-#else-    handleResult = id-#endif--getIconPixbufByName :: Int32 -> T.Text -> Maybe String -> IO (Maybe Pixbuf)-getIconPixbufByName size name themePath = do-  trayLogger DEBUG $ printf "Getting Pixbuf from name for %s" name-  themeForIcon <- getThemeWithOptionalSearchPath themePath--  let panelName = T.pack $ printf "%s-panel" name-  -- 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)--  themedPixbuf <- do-    pbPanel <- tryLoad panelName-    case pbPanel of-      Just _ -> return pbPanel-      Nothing -> tryLoad name--  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-#else-      let handleResult = sequenceA-#endif-      handleResult $ safePixbufNewFromFile <$> maybeFile--getIconPathFromThemePath :: String -> String -> IO (Maybe String)-getIconPathFromThemePath name themePath = if name == "" then return Nothing else do-  trayLogger DEBUG $ printf-    "Trying to load icon %s as filepath with theme path %s"-    name themePath-  pathExists <- doesDirectoryExist themePath-  if pathExists-  then do-    fileNames <- catchAny (listDirectory themePath) (const $ return [])-    trayLogger DEBUG $ printf-      "Found files in theme path %s" (show fileNames)-    return $ (themePath </>) <$> find (isPrefixOf name) fileNames-  else return Nothing--getIconPixbufFromByteString :: Int32 -> Int32 -> BS.ByteString -> IO (Maybe Pixbuf)-getIconPixbufFromByteString width height byteString-  | width <= 0 || height <= 0 = do-      trayLogger WARNING $ printf "Invalid icon dimensions: %dx%d" width height-      return Nothing-  | otherwise = catchGErrorsAsNothing $ do-      trayLogger DEBUG "Getting Pixbuf from bytestring"-      bytes <- bytesNew $ Just byteString-      let bytesPerPixel = 4-          rowStride = width * bytesPerPixel-          sampleBits = 8-      pixbufNewFromBytes bytes ColorspaceRgb True sampleBits width height rowStride--data ItemContext = ItemContext-  { contextName :: DBusTypes.BusName-  , contextMenuPath :: Maybe DBusTypes.ObjectPath-  , contextImage :: Gtk.Image-  , contextButton :: Gtk.EventBox-  }--data TrayImageSize = Expand | TrayImageSize Int32--data TrayClickAction = Activate | SecondaryActivate | PopupMenu--data MenuBackend = LibDBusMenu | HaskellDBusMenu deriving (Eq, Show)--data TrayItemMatcher = TrayItemMatcher-  { trayItemMatcherDescription :: String-  , trayItemMatcherPredicate :: ItemInfo -> Bool-  }--data TrayPriorityConfig = TrayPriorityConfig-  { trayPriorityMatchers :: [TrayItemMatcher]-  }--defaultTrayPriorityConfig :: TrayPriorityConfig-defaultTrayPriorityConfig = TrayPriorityConfig { trayPriorityMatchers = [] }--mkTrayItemMatcher :: String -> (ItemInfo -> Bool) -> TrayItemMatcher-mkTrayItemMatcher = TrayItemMatcher--trayMatchAny :: [TrayItemMatcher] -> TrayItemMatcher-trayMatchAny matchers = mkTrayItemMatcher "any" $ \info ->-  any (\matcher -> trayItemMatcherPredicate matcher info) matchers--trayMatchAll :: [TrayItemMatcher] -> TrayItemMatcher-trayMatchAll matchers = mkTrayItemMatcher "all" $ \info ->-  all (\matcher -> trayItemMatcherPredicate matcher info) matchers--trayMatchNot :: TrayItemMatcher -> TrayItemMatcher-trayMatchNot matcher =-  mkTrayItemMatcher ("not(" <> trayItemMatcherDescription matcher <> ")") $-    not . trayItemMatcherPredicate matcher--normalizeText :: T.Text -> T.Text-normalizeText = T.toCaseFold--containsCI :: T.Text -> T.Text -> Bool-containsCI needle haystack =-  normalizeText needle `T.isInfixOf` normalizeText haystack--equalsCI :: T.Text -> T.Text -> Bool-equalsCI left right = normalizeText left == normalizeText right--matchOnTextFields ::-  String ->-  (T.Text -> T.Text -> Bool) ->-  [ItemInfo -> Maybe T.Text] ->-  T.Text ->-  TrayItemMatcher-matchOnTextFields matcherName comparator fieldGetters target =-  mkTrayItemMatcher matcherName $ \info ->-    any-      (\fieldGetter -> maybe False (comparator target) (fieldGetter info))-      fieldGetters--serviceNameText :: ItemInfo -> T.Text-serviceNameText = T.pack . (coerce :: DBusTypes.BusName -> String) . itemServiceName--servicePathText :: ItemInfo -> T.Text-servicePathText = T.pack . (coerce :: DBusTypes.ObjectPath -> String) . itemServicePath--menuPathText :: ItemInfo -> Maybe T.Text-menuPathText = fmap (T.pack . (coerce :: DBusTypes.ObjectPath -> String)) . menuPath--itemIdText :: ItemInfo -> Maybe T.Text-itemIdText = fmap T.pack . itemId--itemCategoryText :: ItemInfo -> Maybe T.Text-itemCategoryText = fmap T.pack . itemCategory--itemStatusText :: ItemInfo -> Maybe T.Text-itemStatusText = fmap T.pack . itemStatus--iconNameText :: ItemInfo -> T.Text-iconNameText = T.pack . iconName--iconTitleText :: ItemInfo -> T.Text-iconTitleText = T.pack . iconTitle--tooltipTitleText :: ItemInfo -> Maybe T.Text-tooltipTitleText info = (\(_, _, titleText, _) -> T.pack titleText) <$> itemToolTip info--tooltipBodyText :: ItemInfo -> Maybe T.Text-tooltipBodyText info = (\(_, _, _, bodyText) -> T.pack bodyText) <$> itemToolTip info--trayMatchServiceNameContains :: T.Text -> TrayItemMatcher-trayMatchServiceNameContains =-  matchOnTextFields "service-name-contains" containsCI [Just . serviceNameText]--trayMatchServiceNameEquals :: T.Text -> TrayItemMatcher-trayMatchServiceNameEquals =-  matchOnTextFields "service-name-equals" equalsCI [Just . serviceNameText]--trayMatchServicePathContains :: T.Text -> TrayItemMatcher-trayMatchServicePathContains =-  matchOnTextFields "service-path-contains" containsCI [Just . servicePathText]--trayMatchServicePathEquals :: T.Text -> TrayItemMatcher-trayMatchServicePathEquals =-  matchOnTextFields "service-path-equals" equalsCI [Just . servicePathText]--trayMatchMenuPathContains :: T.Text -> TrayItemMatcher-trayMatchMenuPathContains =-  matchOnTextFields "menu-path-contains" containsCI [menuPathText]--trayMatchMenuPathEquals :: T.Text -> TrayItemMatcher-trayMatchMenuPathEquals =-  matchOnTextFields "menu-path-equals" equalsCI [menuPathText]--trayMatchItemIdContains :: T.Text -> TrayItemMatcher-trayMatchItemIdContains =-  matchOnTextFields "item-id-contains" containsCI [itemIdText]--trayMatchItemIdEquals :: T.Text -> TrayItemMatcher-trayMatchItemIdEquals =-  matchOnTextFields "item-id-equals" equalsCI [itemIdText]--trayMatchItemCategoryContains :: T.Text -> TrayItemMatcher-trayMatchItemCategoryContains =-  matchOnTextFields "item-category-contains" containsCI [itemCategoryText]--trayMatchItemCategoryEquals :: T.Text -> TrayItemMatcher-trayMatchItemCategoryEquals =-  matchOnTextFields "item-category-equals" equalsCI [itemCategoryText]--trayMatchStatusContains :: T.Text -> TrayItemMatcher-trayMatchStatusContains =-  matchOnTextFields "item-status-contains" containsCI [itemStatusText]--trayMatchStatusEquals :: T.Text -> TrayItemMatcher-trayMatchStatusEquals =-  matchOnTextFields "item-status-equals" equalsCI [itemStatusText]--trayMatchIconNameContains :: T.Text -> TrayItemMatcher-trayMatchIconNameContains =-  matchOnTextFields "icon-name-contains" containsCI [Just . iconNameText]--trayMatchIconNameEquals :: T.Text -> TrayItemMatcher-trayMatchIconNameEquals =-  matchOnTextFields "icon-name-equals" equalsCI [Just . iconNameText]--trayMatchIconTitleContains :: T.Text -> TrayItemMatcher-trayMatchIconTitleContains =-  matchOnTextFields "icon-title-contains" containsCI [Just . iconTitleText]--trayMatchIconTitleEquals :: T.Text -> TrayItemMatcher-trayMatchIconTitleEquals =-  matchOnTextFields "icon-title-equals" equalsCI [Just . iconTitleText]--trayMatchTooltipContains :: T.Text -> TrayItemMatcher-trayMatchTooltipContains =-  matchOnTextFields "tooltip-contains" containsCI [tooltipTitleText, tooltipBodyText]--trayMatchTooltipEquals :: T.Text -> TrayItemMatcher-trayMatchTooltipEquals =-  matchOnTextFields "tooltip-equals" equalsCI [tooltipTitleText, tooltipBodyText]--trayMatchAnyTextContains :: T.Text -> TrayItemMatcher-trayMatchAnyTextContains =-  matchOnTextFields-    "any-text-contains"-    containsCI-    [ Just . serviceNameText-    , Just . servicePathText-    , menuPathText-    , itemIdText-    , itemCategoryText-    , itemStatusText-    , Just . iconNameText-    , Just . iconTitleText-    , tooltipTitleText-    , tooltipBodyText-    ]--trayMatchIsMenu :: Bool -> TrayItemMatcher-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-  , trayMiddleClickAction :: TrayClickAction-  , trayRightClickAction :: TrayClickAction-  , trayMenuBackend :: MenuBackend-  , trayCenterIcons :: Bool-  }--defaultTrayParams :: TrayParams-defaultTrayParams = TrayParams-  { trayOrientation = Gtk.OrientationHorizontal-  , trayImageSize = Expand-  , trayIconExpand = False-  , trayIconPreference = PreferPixmaps-  , trayAlignment = End-  , trayOverlayScale = 2 % 5-  , trayLeftClickAction = Activate-  , trayMiddleClickAction = SecondaryActivate-  , trayRightClickAction = PopupMenu-  , trayMenuBackend = HaskellDBusMenu-  , trayCenterIcons = False-  }--buildTray :: Host -> Client -> TrayParams -> IO Gtk.Box-buildTray host client params =-  buildTrayWithPixbufTransform host client params Nothing--buildTrayWithPriority :: Host -> Client -> TrayParams -> TrayPriorityConfig -> IO Gtk.Box-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-            }-          client-          TrayParams { trayOrientation = orientation-                     , trayImageSize = imageSize-                     , trayIconExpand = shouldExpand-                     , trayIconPreference = iconPreference-                     , trayAlignment = alignment-                     , trayOverlayScale = overlayScale-                     , trayLeftClickAction = leftClickAction-                     , trayMiddleClickAction = middleClickAction-                     , trayRightClickAction = rightClickAction-                     , trayMenuBackend = menuBackend-                     , trayCenterIcons = centerIcons-                     }-          TrayPriorityConfig { trayPriorityMatchers = priorityMatchers }-          mTransform = do-  trayLogger INFO "Building tray"--  trayBox <- Gtk.boxNew orientation 0-  when centerIcons $ case orientation of-    Gtk.OrientationHorizontal -> Gtk.widgetSetHalign trayBox Gtk.AlignCenter-    _ -> Gtk.widgetSetValign trayBox Gtk.AlignCenter-  Gtk.widgetGetStyleContext trayBox >>=-    flip Gtk.styleContextAddClass "tray-box"-  contextMap <- MV.newMVar Map.empty--  let getContext name = Map.lookup name <$> MV.readMVar contextMap-      showInfo info = show info { iconPixmaps = [] }--      getSize rectangle =-        case orientation of-          Gtk.OrientationHorizontal ->-            Gdk.getRectangleHeight rectangle-          _ ->-            Gdk.getRectangleWidth rectangle--      getInfoAttr fn def name = maybe def fn . Map.lookup name <$> getInfoMap--      getInfo :: ItemInfo -> DBusTypes.BusName -> IO ItemInfo-      getInfo = getInfoAttr id--      getPriorityIndex info =-        fromMaybe-          (length priorityMatchers)-          (findIndex (\matcher -> trayItemMatcherPredicate matcher info) priorityMatchers)--      reorderTrayByPriority = when (not (null priorityMatchers)) $ do-        currentChildren <- Gtk.containerGetChildren trayBox-        contexts <- MV.readMVar contextMap-        contextWidgets <- forM (Map.toList contexts) $-          \(busName, ItemContext { contextButton = button }) -> do-            widget <- Gtk.toWidget button-            return (busName, widget)-        infoMap <- getInfoMap-        let childRows =-              [ let busName = fst <$> find (\(_, widget) -> widget == child) contextWidgets-                    itemInfo = busName >>= (`Map.lookup` infoMap)-                    priority = maybe (length priorityMatchers) getPriorityIndex itemInfo-                 in (priority, currentIndex, child)-                | (currentIndex, child) <- zip [0 :: Int ..] currentChildren-              ]-            sortedChildren =-              [ child-                | (_, _, child) <--                    sortOn (\(priority, currentIndex, _) -> (priority, currentIndex)) childRows-              ]-        forM_ (zip [0 :: Int ..] sortedChildren) $-          \(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-              updateIcon (Just ItemContext { contextImage = image } ) = do-                size <- case imageSize of-                          TrayImageSize size -> return size-                          Expand -> Gtk.widgetGetAllocation image >>= getSize-                getScaledPixBufFromInfo size info-                  >>= applyTransform image-                  >>=-                                  let handlePixbuf mpbuf =-                                        if isJust mpbuf-                                        then Gtk.imageSetFromPixbuf image mpbuf-                                        else trayLogger WARNING $-                                             printf "Failed to get pixbuf for %s" $-                                             showInfo info-                                  in handlePixbuf--      getTooltipText ItemInfo { itemToolTip = Just (_, _, titleText, fullText )}-        | titleText == fullText = fullText-        | titleText == "" = fullText-        | fullText == "" = titleText-        | otherwise = printf "%s: %s" titleText fullText-      getTooltipText _ = ""--      setTooltipText widget info =-        Gtk.widgetSetTooltipText widget $ Just $ T.pack $ getTooltipText info--      updateHandler ItemAdded-                    info@ItemInfo { menuPath = pathForMenu-                                  , itemServiceName = serviceName-                                  , itemServicePath = servicePath-          } =-        do-          let serviceNameStr = (coerce serviceName :: String)-              servicePathStr = coerce servicePath :: String-              logText = printf "Adding widget for %s - %s"-                        serviceNameStr servicePathStr--          trayLogger INFO logText--          eventBox <- Gtk.eventBoxNew-          Gtk.widgetAddEvents eventBox [Gdk.EventMaskScrollMask]-          Gtk.widgetGetStyleContext eventBox >>=-            flip Gtk.styleContextAddClass "tray-icon-button"--          image <- Gtk.imageNew--          case imageSize of-            Expand -> do-              lastAllocation <- MV.newMVar Nothing--              let setPixbuf allocation =-                    do-                      size <- getSize allocation--                      actualWidth <- Gdk.getRectangleWidth allocation-                      actualHeight <- Gdk.getRectangleHeight allocation--                      requestResize <- MV.modifyMVar lastAllocation $ \previous ->-                        let thisTime = Just (size, actualWidth, actualHeight)-                        in return (thisTime, thisTime /= previous)--                      trayLogger DEBUG $-                                 printf-                                 ("Allocating image size %s, width %s," <>-                                  " height %s, resize %s")-                                 (show size)-                                 (show actualWidth)-                                 (show actualHeight)-                                 (show requestResize)--                      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"--          Gtk.containerAdd eventBox image-          setTooltipText eventBox info--          let context =-                ItemContext { contextName = serviceName-                            , contextMenuPath = pathForMenu-                            , contextImage = image-                            , contextButton = eventBox-                            }--              popupGtkMenu gtkMenu mEvent = do-                Gtk.menuAttachToWidget gtkMenu eventBox Nothing-                _ <- Gtk.onWidgetHide gtkMenu $-                  void $ GLib.idleAdd GLib.PRIORITY_LOW $ do-                    Gtk.widgetDestroy gtkMenu-                    return False-                Gtk.widgetShowAll gtkMenu-                Gtk.menuPopupAtPointer gtkMenu mEvent--          _ <- Gtk.onWidgetButtonPressEvent eventBox $ \event -> do-            -- Capture the current event as a Gdk.Event before any-            -- blocking calls (DBus etc.) so menuPopupAtPointer can-            -- use its coordinates for popup positioning.-            currentEvent <- Gtk.getCurrentEvent-            mouseButton <- Gdk.getEventButtonButton event-            x <- round <$> Gdk.getEventButtonXRoot event-            y <- round <$> Gdk.getEventButtonYRoot event-            action <- case mouseButton of-              1 -> bool leftClickAction PopupMenu <$> getInfoAttr-                   itemIsMenu True serviceName-              2 -> return middleClickAction-              _ -> return rightClickAction-            let logActionError actionName e =-                  trayLogger WARNING $ printf "%s failed for %s: %s"-                    (actionName :: String)-                    (coerce serviceName :: String)-                    (show e)-            case action of-              Activate -> catchAny-                (void $ IC.activate client serviceName servicePath x y)-                (logActionError "Activate")-              SecondaryActivate -> catchAny-                (void $ IC.secondaryActivate client-                        serviceName servicePath x y)-                (logActionError "SecondaryActivate")-              PopupMenu -> do-                menuPath' <- getInfoAttr menuPath Nothing serviceName-                traverse_-                  (\p -> catchAny-                    (case menuBackend of-                       LibDBusMenu -> do-                         let sn = T.pack (coerce serviceName :: String)-                             mp = T.pack (coerce p :: String)-                         gtkMenu <- DM.menuNew sn mp >>= unsafeCastTo Gtk.Menu-                         Gtk.menuAttachToWidget gtkMenu eventBox Nothing-                         _ <- Gtk.onWidgetHide gtkMenu $-                           void $ GLib.idleAdd GLib.PRIORITY_DEFAULT_IDLE $ do-                             Gtk.widgetDestroy gtkMenu-                             return False-                         -- libdbusmenu-gtk fetches the menu layout-                         -- asynchronously; showing before the root menuitem-                         -- is available triggers assertion failures. Defer-                         -- the popup until the menu is populated.-                         attemptsRef <- newIORef (0 :: Int)-                         _ <- GLib.timeoutAdd GLib.PRIORITY_DEFAULT 50 $ do-                           n <- readIORef attemptsRef-                           if n >= 100-                             then do-                               Gtk.widgetDestroy gtkMenu-                               return False-                             else do-                               writeIORef attemptsRef (n + 1)-                               children <- Gtk.containerGetChildren gtkMenu-                               if null children-                                 then return True-                                 else do-                                   Gtk.widgetShowAll gtkMenu-                                   -- libdbusmenu is populated asynchronously, so we popup later via a-                                   -- timeout. On Wayland, popups generally need the original trigger-                                   -- event; use menuPopupAtWidget anchored to the EventBox to avoid-                                   -- "no trigger event" and invalid rect_window assertions.-                                   -- Anchor to the actual icon widget so the popup aligns with the-                                   -- visible image, not the full EventBox allocation.-                                   Gtk.menuPopupAtWidget-                                     gtkMenu-                                     image-                                     GravitySouth-                                     GravityNorth-                                     currentEvent-                                   return False-                         return ()-                       HaskellDBusMenu -> do-                         gtkMenu <- DBusMenu.buildMenu client serviceName p-                         popupGtkMenu gtkMenu currentEvent)-                    (logActionError "PopupMenu"))-                  menuPath'-            return False-          _ <- Gtk.onWidgetScrollEvent eventBox $ \event -> do-            direction <- getEventScrollDirection event-            let direction' = case direction of-                               ScrollDirectionUp -> Just "vertical"-                               ScrollDirectionDown -> Just "vertical"-                               ScrollDirectionLeft -> Just "horizontal"-                               ScrollDirectionRight -> Just "horizontal"-                               _ -> Nothing-                delta = case direction of-                          ScrollDirectionUp -> -1-                          ScrollDirectionDown -> 1-                          ScrollDirectionLeft -> -1-                          ScrollDirectionRight -> 1-                          _ -> 0-            traverse_ (\d -> catchAny-              (void $ IC.scroll client serviceName servicePath delta d)-              (\e -> trayLogger WARNING $ printf "Scroll failed for %s: %s"-                (coerce serviceName :: String) (show e))) direction'-            return False--          MV.modifyMVar_ contextMap $ return . Map.insert serviceName context--          Gtk.widgetShowAll eventBox-          let packFn =-                case alignment of-                  End -> Gtk.boxPackEnd-                  _ -> Gtk.boxPackStart--          packFn trayBox eventBox shouldExpand True 0--      updateHandler ItemRemoved ItemInfo { itemServiceName = name }-        = getContext name >>= removeWidget-        where removeWidget Nothing =-                trayLogger WARNING "removeWidget: unrecognized service name."-              removeWidget (Just ItemContext { contextButton = widgetToRemove }) =-                do-                  Gtk.containerRemove trayBox widgetToRemove-                  MV.modifyMVar_ contextMap $ return . Map.delete name--      updateHandler IconUpdated i = updateIconFromInfo i-      updateHandler OverlayIconUpdated i = updateIconFromInfo i--      updateHandler ToolTipUpdated info@ItemInfo { itemServiceName = name } =-        void $ getContext name >>=-             traverse (flip setTooltipText info . contextButton)--      updateHandler _ _ = return ()--      maybeAddOverlayToPixbuf size info pixbuf = do-        _ <- runMaybeT $ do-          let overlayHeight = floor (fromIntegral size * overlayScale)-          overlayPixbuf <--            MaybeT $ getOverlayPixBufFromInfo overlayHeight info >>=-            traverse (scalePixbufToSize overlayHeight Gtk.OrientationHorizontal)-          lift $ do-            actualOHeight <- getPixbufHeight overlayPixbuf-            actualOWidth <- getPixbufWidth overlayPixbuf-            _mainHeight <- getPixbufHeight pixbuf-            _mainWidth <- getPixbufWidth pixbuf-            pixbufComposite overlayPixbuf pixbuf-              0 0-              actualOWidth actualOHeight-              0 0-              1.0 1.0-              InterpTypeBilinear-              255-        return pixbuf--      getScaledPixBufFromInfo size info =-        getPixBufFromInfo size info >>=-        traverse (scalePixbufToSize size orientation >=>-                  maybeAddOverlayToPixbuf size info)--      getPixBufFromInfo size-                        ItemInfo { iconName = name-                                 , iconThemePath = mpath-                                 , iconPixmaps = pixmaps-                                 } = getPixBufFrom size name mpath pixmaps--      getOverlayPixBufFromInfo size-                               ItemInfo-                                     { overlayIconName = name-                                     , iconThemePath = mpath-                                     , overlayIconPixmaps = pixmaps-                                     } = getPixBufFrom size (fromMaybe "" name)-                               mpath pixmaps--      getPixBufFrom size name mpath pixmaps = do-        let tooSmall (w, h, _) = w < size || h < size-            largeEnough = filter (not . tooSmall) pixmaps-            orderer (w1, h1, _) (w2, h2, _) =-              case comparing id w1 w2 of-                EQ -> comparing id h1 h2-                a -> a-            selectedPixmap =-              if null largeEnough-              then maximumBy orderer pixmaps-              else minimumBy orderer largeEnough-            getFromPixmaps (w, h, p) =-              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 case iconPreference of-               PreferThemedIcons ->-                 firstJustM getFromThemed (getFromPixmaps selectedPixmap)-               PreferPixmaps ->-                 firstJustM (getFromPixmaps selectedPixmap) getFromThemed--      uiUpdateHandler updateType info =-        void $ Gdk.threadsAddIdle GLib.PRIORITY_DEFAULT $-             catchAny-               (updateHandler updateType info-                >> reorderTrayByPriority-                >> return False)-               (\e -> do-                 trayLogger WARNING $ printf "Update handler failed: %s" (show e)-                 return False)--  handlerId <- addUHandler uiUpdateHandler-  _ <- Gtk.onWidgetDestroy trayBox $ removeUHandler handlerId-  return trayBox+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE OverloadedStrings #-}++module StatusNotifier.Tray where++import Control.Concurrent.MVar as MV+import Control.Exception.Base+import Control.Exception.Enclosed (catchAny)+import Control.Monad+import Control.Monad.Trans.Class+import Control.Monad.Trans.Maybe+import DBus.Client+import qualified DBus.Internal.Types as DBusTypes+import qualified DBusMenu+import qualified Data.ByteString as BS+import Data.Coerce+import Data.Foldable (traverse_)+import Data.GI.Base (unsafeCastTo)+import Data.GI.Base.GError+import Data.IORef (newIORef, readIORef, writeIORef)+import Data.Int+import Data.List+import qualified Data.Map.Strict as Map+import Data.Maybe+import Data.Ord+import Data.Ratio+import qualified Data.Text as T+import Data.Word+import Foreign.Ptr (Ptr)+import qualified GI.DbusmenuGtk3.Objects.Menu as DM+import qualified GI.GLib as GLib+import GI.GLib.Structs.Bytes+import qualified GI.Gdk as Gdk+import GI.Gdk.Enums+import GI.Gdk.Structs.EventScroll+import GI.GdkPixbuf.Enums+import GI.GdkPixbuf.Objects.Pixbuf as Gdk+import qualified GI.Gtk as Gtk+import GI.Gtk.Flags+import GI.Gtk.Objects.IconTheme+import Graphics.UI.GIGtkStrut+import StatusNotifier.Host.Service+import qualified StatusNotifier.Item.Client as IC+import System.Directory+import System.FilePath+import System.Log.Logger+import Text.Printf++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+      (show $ info {iconPixmaps = []})+      (show $ length $ iconPixmaps info)++getScaledWidthHeight :: Bool -> Int32 -> Int32 -> Int32 -> (Int32, Int32)+getScaledWidthHeight shouldTargetWidth targetSize width height =+  let getRatio :: Int32 -> Rational+      getRatio toScale =+        fromIntegral targetSize / fromIntegral toScale+      getOther :: Int32 -> Int32 -> Int32+      getOther toScale other = max 1 $ floor $ getRatio toScale * fromIntegral other+   in if shouldTargetWidth+        then (targetSize, getOther width height)+        else (getOther height width, targetSize)++scalePixbufToSize :: Int32 -> Gtk.Orientation -> Pixbuf -> IO Pixbuf+scalePixbufToSize size orientation pixbuf = do+  width <- pixbufGetWidth pixbuf+  height <- pixbufGetHeight pixbuf+  let warnAndReturnOrig =+        trayLogger WARNING "Unable to scale pixbuf" >> return pixbuf+  if width <= 0 || height <= 0+    then warnAndReturnOrig+    else do+      let targetWidth = case orientation of+            Gtk.OrientationHorizontal -> False+            _ -> True+          (scaledWidth, scaledHeight) =+            getScaledWidthHeight targetWidth size width height+      trayLogger DEBUG $+        printf+          "Scaling pb to %s, actualW: %s, actualH: %s, scaledW: %s, scaledH: %s"+          (show size)+          (show width)+          (show height)+          (show scaledWidth)+          (show scaledHeight)++      trayLogger DEBUG $+        printf+          "targetW: %s, targetH: %s"+          (show scaledWidth)+          (show scaledHeight)+      maybe warnAndReturnOrig return+        =<< pixbufScaleSimple pixbuf scaledWidth scaledHeight InterpTypeBilinear++themeLoadFlags :: [IconLookupFlags]+themeLoadFlags = [IconLookupFlagsGenericFallback, IconLookupFlagsUseBuiltin]++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++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)++catchGErrorsAsNothing :: IO a -> IO (Maybe a)+catchGErrorsAsNothing action = catchGErrorsAsLeft action >>= rightToJustLogLeft+  where+    rightToJustLogLeft (Right value) = return $ Just value+    rightToJustLogLeft (Left err) = do+      trayLogger WARNING $ printf "Encountered error: %s" $ show err+      return Nothing++safePixbufNewFromFile :: FilePath -> IO (Maybe Gdk.Pixbuf)+safePixbufNewFromFile =+  handleResult . catchGErrorsAsNothing . Gdk.pixbufNewFromFile+  where+#if MIN_VERSION_gi_gdkpixbuf(2,0,26)+    handleResult = fmap join+#else+    handleResult = id+#endif++getIconPixbufByName :: Int32 -> T.Text -> Maybe String -> IO (Maybe Pixbuf)+getIconPixbufByName size name themePath = do+  trayLogger DEBUG $ printf "Getting Pixbuf from name for %s" name+  themeForIcon <- getThemeWithOptionalSearchPath themePath++  let panelName = T.pack $ printf "%s-panel" name+  -- 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)++  themedPixbuf <- do+    pbPanel <- tryLoad panelName+    case pbPanel of+      Just _ -> return pbPanel+      Nothing -> tryLoad name++  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+      fmap join $ sequenceA $ safePixbufNewFromFile <$> maybeFile++getIconPathFromThemePath :: String -> String -> IO (Maybe String)+getIconPathFromThemePath name themePath =+  if name == ""+    then return Nothing+    else do+      trayLogger DEBUG $+        printf+          "Trying to load icon %s as filepath with theme path %s"+          name+          themePath+      pathExists <- doesDirectoryExist themePath+      if pathExists+        then do+          fileNames <- catchAny (listDirectory themePath) (const $ return [])+          trayLogger DEBUG $+            printf+              "Found files in theme path %s"+              (show fileNames)+          return $ (themePath </>) <$> find (isPrefixOf name) fileNames+        else return Nothing++getIconPixbufFromByteString :: Int32 -> Int32 -> BS.ByteString -> IO (Maybe Pixbuf)+getIconPixbufFromByteString width height byteString+  | width <= 0 || height <= 0 = do+      trayLogger WARNING $ printf "Invalid icon dimensions: %dx%d" width height+      return Nothing+  | otherwise = catchGErrorsAsNothing $ do+      trayLogger DEBUG "Getting Pixbuf from bytestring"+      bytes <- bytesNew $ Just byteString+      let bytesPerPixel = 4+          rowStride = width * bytesPerPixel+          sampleBits = 8+      pixbufNewFromBytes bytes ColorspaceRgb True sampleBits width height rowStride++data ItemContext = ItemContext+  { contextName :: DBusTypes.BusName,+    contextMenuPath :: Maybe DBusTypes.ObjectPath,+    contextImage :: Gtk.Image,+    contextButton :: Gtk.EventBox+  }++data TrayImageSize = Expand | TrayImageSize Int32++data TrayClickAction = Activate | SecondaryActivate | PopupMenu deriving (Eq, Show)++data TrayClickContext = TrayClickContext+  { trayClickItemInfo :: ItemInfo,+    trayClickButton :: Word32,+    trayClickXRoot :: Int32,+    trayClickYRoot :: Int32,+    trayClickModifiers :: [Gdk.ModifierType],+    trayClickDefaultAction :: TrayClickAction+  }++data TrayClickDecision+  = UseDefaultClickAction+  | OverrideClickAction TrayClickAction+  | ConsumeClick+  deriving (Eq, Show)++type TrayClickHook = TrayClickContext -> IO TrayClickDecision++data TrayEventHooks = TrayEventHooks+  { trayClickHook :: Maybe TrayClickHook+  }++defaultTrayEventHooks :: TrayEventHooks+defaultTrayEventHooks = TrayEventHooks {trayClickHook = Nothing}++data MenuBackend = LibDBusMenu | HaskellDBusMenu deriving (Eq, Show)++data TrayItemMatcher = TrayItemMatcher+  { trayItemMatcherDescription :: String,+    trayItemMatcherPredicate :: ItemInfo -> Bool+  }++data TrayPriorityConfig = TrayPriorityConfig+  { trayPriorityMatchers :: [TrayItemMatcher]+  }++defaultTrayPriorityConfig :: TrayPriorityConfig+defaultTrayPriorityConfig = TrayPriorityConfig {trayPriorityMatchers = []}++mkTrayItemMatcher :: String -> (ItemInfo -> Bool) -> TrayItemMatcher+mkTrayItemMatcher = TrayItemMatcher++trayMatchAny :: [TrayItemMatcher] -> TrayItemMatcher+trayMatchAny matchers = mkTrayItemMatcher "any" $ \info ->+  any (\matcher -> trayItemMatcherPredicate matcher info) matchers++trayMatchAll :: [TrayItemMatcher] -> TrayItemMatcher+trayMatchAll matchers = mkTrayItemMatcher "all" $ \info ->+  all (\matcher -> trayItemMatcherPredicate matcher info) matchers++trayMatchNot :: TrayItemMatcher -> TrayItemMatcher+trayMatchNot matcher =+  mkTrayItemMatcher ("not(" <> trayItemMatcherDescription matcher <> ")") $+    not . trayItemMatcherPredicate matcher++normalizeText :: T.Text -> T.Text+normalizeText = T.toCaseFold++containsCI :: T.Text -> T.Text -> Bool+containsCI needle haystack =+  normalizeText needle `T.isInfixOf` normalizeText haystack++equalsCI :: T.Text -> T.Text -> Bool+equalsCI left right = normalizeText left == normalizeText right++matchOnTextFields ::+  String ->+  (T.Text -> T.Text -> Bool) ->+  [ItemInfo -> Maybe T.Text] ->+  T.Text ->+  TrayItemMatcher+matchOnTextFields matcherName comparator fieldGetters target =+  mkTrayItemMatcher matcherName $ \info ->+    any+      (\fieldGetter -> maybe False (comparator target) (fieldGetter info))+      fieldGetters++serviceNameText :: ItemInfo -> T.Text+serviceNameText = T.pack . (coerce :: DBusTypes.BusName -> String) . itemServiceName++servicePathText :: ItemInfo -> T.Text+servicePathText = T.pack . (coerce :: DBusTypes.ObjectPath -> String) . itemServicePath++menuPathText :: ItemInfo -> Maybe T.Text+menuPathText = fmap (T.pack . (coerce :: DBusTypes.ObjectPath -> String)) . menuPath++itemIdText :: ItemInfo -> Maybe T.Text+itemIdText = fmap T.pack . itemId++itemCategoryText :: ItemInfo -> Maybe T.Text+itemCategoryText = fmap T.pack . itemCategory++itemStatusText :: ItemInfo -> Maybe T.Text+itemStatusText = fmap T.pack . itemStatus++iconNameText :: ItemInfo -> T.Text+iconNameText = T.pack . iconName++iconTitleText :: ItemInfo -> T.Text+iconTitleText = T.pack . iconTitle++tooltipTitleText :: ItemInfo -> Maybe T.Text+tooltipTitleText info = (\(_, _, titleText, _) -> T.pack titleText) <$> itemToolTip info++tooltipBodyText :: ItemInfo -> Maybe T.Text+tooltipBodyText info = (\(_, _, _, bodyText) -> T.pack bodyText) <$> itemToolTip info++trayMatchServiceNameContains :: T.Text -> TrayItemMatcher+trayMatchServiceNameContains =+  matchOnTextFields "service-name-contains" containsCI [Just . serviceNameText]++trayMatchServiceNameEquals :: T.Text -> TrayItemMatcher+trayMatchServiceNameEquals =+  matchOnTextFields "service-name-equals" equalsCI [Just . serviceNameText]++trayMatchServicePathContains :: T.Text -> TrayItemMatcher+trayMatchServicePathContains =+  matchOnTextFields "service-path-contains" containsCI [Just . servicePathText]++trayMatchServicePathEquals :: T.Text -> TrayItemMatcher+trayMatchServicePathEquals =+  matchOnTextFields "service-path-equals" equalsCI [Just . servicePathText]++trayMatchMenuPathContains :: T.Text -> TrayItemMatcher+trayMatchMenuPathContains =+  matchOnTextFields "menu-path-contains" containsCI [menuPathText]++trayMatchMenuPathEquals :: T.Text -> TrayItemMatcher+trayMatchMenuPathEquals =+  matchOnTextFields "menu-path-equals" equalsCI [menuPathText]++trayMatchItemIdContains :: T.Text -> TrayItemMatcher+trayMatchItemIdContains =+  matchOnTextFields "item-id-contains" containsCI [itemIdText]++trayMatchItemIdEquals :: T.Text -> TrayItemMatcher+trayMatchItemIdEquals =+  matchOnTextFields "item-id-equals" equalsCI [itemIdText]++trayMatchItemCategoryContains :: T.Text -> TrayItemMatcher+trayMatchItemCategoryContains =+  matchOnTextFields "item-category-contains" containsCI [itemCategoryText]++trayMatchItemCategoryEquals :: T.Text -> TrayItemMatcher+trayMatchItemCategoryEquals =+  matchOnTextFields "item-category-equals" equalsCI [itemCategoryText]++trayMatchStatusContains :: T.Text -> TrayItemMatcher+trayMatchStatusContains =+  matchOnTextFields "item-status-contains" containsCI [itemStatusText]++trayMatchStatusEquals :: T.Text -> TrayItemMatcher+trayMatchStatusEquals =+  matchOnTextFields "item-status-equals" equalsCI [itemStatusText]++trayMatchIconNameContains :: T.Text -> TrayItemMatcher+trayMatchIconNameContains =+  matchOnTextFields "icon-name-contains" containsCI [Just . iconNameText]++trayMatchIconNameEquals :: T.Text -> TrayItemMatcher+trayMatchIconNameEquals =+  matchOnTextFields "icon-name-equals" equalsCI [Just . iconNameText]++trayMatchIconTitleContains :: T.Text -> TrayItemMatcher+trayMatchIconTitleContains =+  matchOnTextFields "icon-title-contains" containsCI [Just . iconTitleText]++trayMatchIconTitleEquals :: T.Text -> TrayItemMatcher+trayMatchIconTitleEquals =+  matchOnTextFields "icon-title-equals" equalsCI [Just . iconTitleText]++trayMatchTooltipContains :: T.Text -> TrayItemMatcher+trayMatchTooltipContains =+  matchOnTextFields "tooltip-contains" containsCI [tooltipTitleText, tooltipBodyText]++trayMatchTooltipEquals :: T.Text -> TrayItemMatcher+trayMatchTooltipEquals =+  matchOnTextFields "tooltip-equals" equalsCI [tooltipTitleText, tooltipBodyText]++trayMatchAnyTextContains :: T.Text -> TrayItemMatcher+trayMatchAnyTextContains =+  matchOnTextFields+    "any-text-contains"+    containsCI+    [ Just . serviceNameText,+      Just . servicePathText,+      menuPathText,+      itemIdText,+      itemCategoryText,+      itemStatusText,+      Just . iconNameText,+      Just . iconTitleText,+      tooltipTitleText,+      tooltipBodyText+    ]++trayMatchIsMenu :: Bool -> TrayItemMatcher+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,+    trayMiddleClickAction :: TrayClickAction,+    trayRightClickAction :: TrayClickAction,+    trayMenuBackend :: MenuBackend,+    trayCenterIcons :: Bool,+    trayPriorityConfig :: TrayPriorityConfig,+    trayPixbufTransform :: Maybe PixbufTransform,+    trayEventHooks :: TrayEventHooks+  }++defaultTrayParams :: TrayParams+defaultTrayParams =+  TrayParams+    { trayOrientation = Gtk.OrientationHorizontal,+      trayImageSize = Expand,+      trayIconExpand = False,+      trayIconPreference = PreferPixmaps,+      trayAlignment = End,+      trayOverlayScale = 2 % 5,+      trayLeftClickAction = Activate,+      trayMiddleClickAction = SecondaryActivate,+      trayRightClickAction = PopupMenu,+      trayMenuBackend = HaskellDBusMenu,+      trayCenterIcons = False,+      trayPriorityConfig = defaultTrayPriorityConfig,+      trayPixbufTransform = Nothing,+      trayEventHooks = defaultTrayEventHooks+    }++buildTray :: Host -> Client -> TrayParams -> IO Gtk.Box+buildTray+  Host+    { itemInfoMap = getInfoMap,+      addUpdateHandler = addUHandler,+      removeUpdateHandler = removeUHandler+    }+  client+  TrayParams+    { trayOrientation = orientation,+      trayImageSize = imageSize,+      trayIconExpand = shouldExpand,+      trayIconPreference = iconPreference,+      trayAlignment = alignment,+      trayOverlayScale = overlayScale,+      trayLeftClickAction = leftClickAction,+      trayMiddleClickAction = middleClickAction,+      trayRightClickAction = rightClickAction,+      trayMenuBackend = menuBackend,+      trayCenterIcons = centerIcons,+      trayPriorityConfig =+        TrayPriorityConfig+          { trayPriorityMatchers = priorityMatchers+          },+      trayPixbufTransform = mTransform,+      trayEventHooks =+        TrayEventHooks+          { trayClickHook = mClickHook+          }+    } =+    do+      trayLogger INFO "Building tray"++      trayBox <- Gtk.boxNew orientation 0+      when centerIcons $ case orientation of+        Gtk.OrientationHorizontal -> Gtk.widgetSetHalign trayBox Gtk.AlignCenter+        _ -> Gtk.widgetSetValign trayBox Gtk.AlignCenter+      Gtk.widgetGetStyleContext trayBox+        >>= flip Gtk.styleContextAddClass "tray-box"+      contextMap <- MV.newMVar Map.empty++      let getContext name = Map.lookup name <$> MV.readMVar contextMap+          showInfo info = show info {iconPixmaps = []}++          getSize rectangle =+            case orientation of+              Gtk.OrientationHorizontal ->+                Gdk.getRectangleHeight rectangle+              _ ->+                Gdk.getRectangleWidth rectangle++          getInfoAttr fn def name = maybe def fn . Map.lookup name <$> getInfoMap++          getInfo :: ItemInfo -> DBusTypes.BusName -> IO ItemInfo+          getInfo = getInfoAttr id++          getPriorityIndex info =+            fromMaybe+              (length priorityMatchers)+              (findIndex (\matcher -> trayItemMatcherPredicate matcher info) priorityMatchers)++          reorderTrayByPriority = when (not (null priorityMatchers)) $ do+            currentChildren <- Gtk.containerGetChildren trayBox+            contexts <- MV.readMVar contextMap+            contextWidgets <- forM (Map.toList contexts) $+              \(busName, ItemContext {contextButton = button}) -> do+                widget <- Gtk.toWidget button+                return (busName, widget)+            infoMap <- getInfoMap+            let childRows =+                  [ let busName = fst <$> find (\(_, widget) -> widget == child) contextWidgets+                        itemInfo = busName >>= (`Map.lookup` infoMap)+                        priority = maybe (length priorityMatchers) getPriorityIndex itemInfo+                     in (priority, currentIndex, child)+                  | (currentIndex, child) <- zip [0 :: Int ..] currentChildren+                  ]+                sortedChildren =+                  [ child+                  | (_, _, child) <-+                      sortOn (\(priority, currentIndex, _) -> (priority, currentIndex)) childRows+                  ]+            forM_ (zip [0 :: Int ..] sortedChildren) $+              \(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+              updateIcon (Just ItemContext {contextImage = image}) = do+                size <- case imageSize of+                  TrayImageSize size -> return size+                  Expand -> Gtk.widgetGetAllocation image >>= getSize+                getScaledPixBufFromInfo size info+                  >>= applyTransform image+                  >>= let handlePixbuf mpbuf =+                            if isJust mpbuf+                              then Gtk.imageSetFromPixbuf image mpbuf+                              else+                                trayLogger WARNING $+                                  printf "Failed to get pixbuf for %s" $+                                    showInfo info+                       in handlePixbuf++          getTooltipText ItemInfo {itemToolTip = Just (_, _, titleText, fullText)}+            | titleText == fullText = fullText+            | titleText == "" = fullText+            | fullText == "" = titleText+            | otherwise = printf "%s: %s" titleText fullText+          getTooltipText _ = ""++          setTooltipText widget info =+            Gtk.widgetSetTooltipText widget $ Just $ T.pack $ getTooltipText info++          updateHandler+            ItemAdded+            info@ItemInfo+              { menuPath = pathForMenu,+                itemServiceName = serviceName,+                itemServicePath = servicePath+              } =+              do+                let serviceNameStr = (coerce serviceName :: String)+                    servicePathStr = coerce servicePath :: String+                    logText =+                      printf+                        "Adding widget for %s - %s"+                        serviceNameStr+                        servicePathStr++                trayLogger INFO logText++                eventBox <- Gtk.eventBoxNew+                Gtk.widgetAddEvents eventBox [Gdk.EventMaskScrollMask]+                Gtk.widgetGetStyleContext eventBox+                  >>= flip Gtk.styleContextAddClass "tray-icon-button"++                image <- Gtk.imageNew++                case imageSize of+                  Expand -> do+                    lastAllocation <- MV.newMVar Nothing++                    let setPixbuf allocation =+                          do+                            size <- getSize allocation++                            actualWidth <- Gdk.getRectangleWidth allocation+                            actualHeight <- Gdk.getRectangleHeight allocation++                            requestResize <- MV.modifyMVar lastAllocation $ \previous ->+                              let thisTime = Just (size, actualWidth, actualHeight)+                               in return (thisTime, thisTime /= previous)++                            trayLogger DEBUG $+                              printf+                                ( "Allocating image size %s, width %s,"+                                    <> " height %s, resize %s"+                                )+                                (show size)+                                (show actualWidth)+                                (show actualHeight)+                                (show requestResize)++                            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"++                Gtk.containerAdd eventBox image+                setTooltipText eventBox info++                let context =+                      ItemContext+                        { contextName = serviceName,+                          contextMenuPath = pathForMenu,+                          contextImage = image,+                          contextButton = eventBox+                        }++                    popupGtkMenu gtkMenu mEvent = do+                      Gtk.menuAttachToWidget gtkMenu eventBox Nothing+                      _ <- Gtk.onWidgetHide gtkMenu $+                        void $+                          GLib.idleAdd GLib.PRIORITY_LOW $ do+                            Gtk.widgetDestroy gtkMenu+                            return False+                      Gtk.widgetShowAll gtkMenu+                      Gtk.menuPopupAtPointer gtkMenu mEvent++                _ <- Gtk.onWidgetButtonPressEvent eventBox $ \event -> do+                  -- Capture the current event as a Gdk.Event before any+                  -- blocking calls (DBus etc.) so menuPopupAtPointer can+                  -- use its coordinates for popup positioning.+                  currentEvent <- Gtk.getCurrentEvent+                  currentInfo <- getInfo info serviceName+                  mouseButton <- Gdk.getEventButtonButton event+                  x <- round <$> Gdk.getEventButtonXRoot event+                  y <- round <$> Gdk.getEventButtonYRoot event+                  modifiers <- Gdk.getEventButtonState event+                  let defaultAction = case mouseButton of+                        1 -> if itemIsMenu currentInfo then PopupMenu else leftClickAction+                        2 -> middleClickAction+                        _ -> rightClickAction+                  clickDecision <-+                    maybe+                      (pure UseDefaultClickAction)+                      ( \hook ->+                          hook+                            TrayClickContext+                              { trayClickItemInfo = currentInfo,+                                trayClickButton = mouseButton,+                                trayClickXRoot = x,+                                trayClickYRoot = y,+                                trayClickModifiers = modifiers,+                                trayClickDefaultAction = defaultAction+                              }+                      )+                      mClickHook+                  let mAction = case clickDecision of+                        UseDefaultClickAction -> Just defaultAction+                        OverrideClickAction action -> Just action+                        ConsumeClick -> Nothing+                  let logActionError actionName e =+                        trayLogger WARNING $+                          printf+                            "%s failed for %s: %s"+                            (actionName :: String)+                            (coerce serviceName :: String)+                            (show e)+                  traverse_+                    ( \action -> case action of+                        Activate ->+                          catchAny+                            (void $ IC.activate client serviceName servicePath x y)+                            (logActionError "Activate")+                        SecondaryActivate ->+                          catchAny+                            ( void $+                                IC.secondaryActivate+                                  client+                                  serviceName+                                  servicePath+                                  x+                                  y+                            )+                            (logActionError "SecondaryActivate")+                        PopupMenu -> do+                          let menuPath' = menuPath currentInfo+                          traverse_+                            ( \p ->+                                catchAny+                                  ( case menuBackend of+                                      LibDBusMenu -> do+                                        let sn = T.pack (coerce serviceName :: String)+                                            mp = T.pack (coerce p :: String)+                                        gtkMenu <- DM.menuNew sn mp >>= unsafeCastTo Gtk.Menu+                                        Gtk.menuAttachToWidget gtkMenu eventBox Nothing+                                        _ <- Gtk.onWidgetHide gtkMenu $+                                          void $+                                            GLib.idleAdd GLib.PRIORITY_DEFAULT_IDLE $ do+                                              Gtk.widgetDestroy gtkMenu+                                              return False+                                        -- libdbusmenu-gtk fetches the menu layout+                                        -- asynchronously; showing before the root menuitem+                                        -- is available triggers assertion failures. Defer+                                        -- the popup until the menu is populated.+                                        attemptsRef <- newIORef (0 :: Int)+                                        _ <- GLib.timeoutAdd GLib.PRIORITY_DEFAULT 50 $ do+                                          n <- readIORef attemptsRef+                                          if n >= 100+                                            then do+                                              Gtk.widgetDestroy gtkMenu+                                              return False+                                            else do+                                              writeIORef attemptsRef (n + 1)+                                              children <- Gtk.containerGetChildren gtkMenu+                                              if null children+                                                then return True+                                                else do+                                                  Gtk.widgetShowAll gtkMenu+                                                  -- libdbusmenu is populated asynchronously, so we popup later via a+                                                  -- timeout. On Wayland, popups generally need the original trigger+                                                  -- event; use menuPopupAtWidget anchored to the EventBox to avoid+                                                  -- "no trigger event" and invalid rect_window assertions.+                                                  -- Anchor to the actual icon widget so the popup aligns with the+                                                  -- visible image, not the full EventBox allocation.+                                                  Gtk.menuPopupAtWidget+                                                    gtkMenu+                                                    image+                                                    GravitySouth+                                                    GravityNorth+                                                    currentEvent+                                                  return False+                                        return ()+                                      HaskellDBusMenu -> do+                                        gtkMenu <- DBusMenu.buildMenu client serviceName p+                                        popupGtkMenu gtkMenu currentEvent+                                  )+                                  (logActionError "PopupMenu")+                            )+                            menuPath'+                    )+                    mAction+                  return False+                _ <- Gtk.onWidgetScrollEvent eventBox $ \event -> do+                  direction <- getEventScrollDirection event+                  let direction' = case direction of+                        ScrollDirectionUp -> Just "vertical"+                        ScrollDirectionDown -> Just "vertical"+                        ScrollDirectionLeft -> Just "horizontal"+                        ScrollDirectionRight -> Just "horizontal"+                        _ -> Nothing+                      delta = case direction of+                        ScrollDirectionUp -> -1+                        ScrollDirectionDown -> 1+                        ScrollDirectionLeft -> -1+                        ScrollDirectionRight -> 1+                        _ -> 0+                  traverse_+                    ( \d ->+                        catchAny+                          (void $ IC.scroll client serviceName servicePath delta d)+                          ( \e ->+                              trayLogger WARNING $+                                printf+                                  "Scroll failed for %s: %s"+                                  (coerce serviceName :: String)+                                  (show e)+                          )+                    )+                    direction'+                  return False++                MV.modifyMVar_ contextMap $ return . Map.insert serviceName context++                Gtk.widgetShowAll eventBox+                let packFn =+                      case alignment of+                        End -> Gtk.boxPackEnd+                        _ -> Gtk.boxPackStart++                packFn trayBox eventBox shouldExpand True 0+          updateHandler ItemRemoved ItemInfo {itemServiceName = name} =+            getContext name >>= removeWidget+            where+              removeWidget Nothing =+                trayLogger WARNING "removeWidget: unrecognized service name."+              removeWidget (Just ItemContext {contextButton = widgetToRemove}) =+                do+                  Gtk.containerRemove trayBox widgetToRemove+                  MV.modifyMVar_ contextMap $ return . Map.delete name+          updateHandler IconUpdated i = updateIconFromInfo i+          updateHandler OverlayIconUpdated i = updateIconFromInfo i+          updateHandler ToolTipUpdated info@ItemInfo {itemServiceName = name} =+            void $+              getContext name+                >>= traverse (flip setTooltipText info . contextButton)+          updateHandler _ _ = return ()++          maybeAddOverlayToPixbuf size info pixbuf = do+            _ <- runMaybeT $ do+              let overlayHeight = floor (fromIntegral size * overlayScale)+              overlayPixbuf <-+                MaybeT $+                  getOverlayPixBufFromInfo overlayHeight info+                    >>= traverse (scalePixbufToSize overlayHeight Gtk.OrientationHorizontal)+              lift $ do+                actualOHeight <- getPixbufHeight overlayPixbuf+                actualOWidth <- getPixbufWidth overlayPixbuf+                _mainHeight <- getPixbufHeight pixbuf+                _mainWidth <- getPixbufWidth pixbuf+                pixbufComposite+                  overlayPixbuf+                  pixbuf+                  0+                  0+                  actualOWidth+                  actualOHeight+                  0+                  0+                  1.0+                  1.0+                  InterpTypeBilinear+                  255+            return pixbuf++          getScaledPixBufFromInfo size info =+            getPixBufFromInfo size info+              >>= traverse+                ( scalePixbufToSize size orientation+                    >=> maybeAddOverlayToPixbuf size info+                )++          getPixBufFromInfo+            size+            ItemInfo+              { iconName = name,+                iconThemePath = mpath,+                iconPixmaps = pixmaps+              } = getPixBufFrom size name mpath pixmaps++          getOverlayPixBufFromInfo+            size+            ItemInfo+              { overlayIconName = name,+                iconThemePath = mpath,+                overlayIconPixmaps = pixmaps+              } =+              getPixBufFrom+                size+                (fromMaybe "" name)+                mpath+                pixmaps++          getPixBufFrom size name mpath pixmaps = do+            let tooSmall (w, h, _) = w < size || h < size+                largeEnough = filter (not . tooSmall) pixmaps+                orderer (w1, h1, _) (w2, h2, _) =+                  case comparing id w1 w2 of+                    EQ -> comparing id h1 h2+                    a -> a+                selectedPixmap =+                  if null largeEnough+                    then maximumBy orderer pixmaps+                    else minimumBy orderer largeEnough+                getFromPixmaps (w, h, p) =+                  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 case iconPreference of+                PreferThemedIcons ->+                  firstJustM getFromThemed (getFromPixmaps selectedPixmap)+                PreferPixmaps ->+                  firstJustM (getFromPixmaps selectedPixmap) getFromThemed++          uiUpdateHandler updateType info =+            void $+              Gdk.threadsAddIdle GLib.PRIORITY_DEFAULT $+                catchAny+                  ( updateHandler updateType info+                      >> reorderTrayByPriority+                      >> return False+                  )+                  ( \e -> do+                      trayLogger WARNING $ printf "Update handler failed: %s" (show e)+                      return False+                  )++      handlerId <- addUHandler uiUpdateHandler+      _ <- Gtk.onWidgetDestroy trayBox $ removeUHandler handlerId+      return trayBox