taffybar-7.2.5: src/System/Taffybar/WindowIcon.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE LambdaCase #-}
-- | Helpers for resolving and constructing window icons from EWMH metadata,
-- desktop entries, icon themes, and browser tab image hooks.
module System.Taffybar.WindowIcon where
import qualified Control.Concurrent.MVar as MV
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Reader (ask, asks, runReaderT)
import Data.Bits
import Data.Data (Typeable)
import Data.Int
import Data.List
import qualified Data.Map as M
import Data.Maybe
import qualified Data.MultiMap as MM
import Data.Ord
import qualified Data.Text as T
import Data.Word
import Foreign.Marshal.Alloc
import Foreign.Marshal.Array
import Foreign.Ptr
import Foreign.Storable
import qualified GI.GdkPixbuf.Enums as Gdk
import qualified GI.GdkPixbuf.Objects.Pixbuf as Gdk
import qualified GI.Gtk as Gtk
import System.Environment.XDG.DesktopEntry
import System.Log.Logger
import System.Taffybar.Context
import System.Taffybar.Hooks
import System.Taffybar.Information.EWMHDesktopInfo
import System.Taffybar.Information.SafeX11 (Event (PropertyEvent))
import System.Taffybar.Information.X11DesktopInfo
import System.Taffybar.Util
import System.Taffybar.Widget.Util
import Text.Printf
-- | Packed 32-bit RGBA color value used by 'pixBufFromColor'.
type ColorRGBA = Word32
data WindowIconCacheKey
= DesktopEntryIconCacheKey Int32 String
| ClassIconCacheKey Int32 String
| EWMHIconCacheKey Int32 X11Window
deriving (Eq, Ord, Show)
newtype WindowIconCache = WindowIconCache
{ windowIconCacheEntries :: MV.MVar (M.Map WindowIconCacheKey Gdk.Pixbuf)
}
deriving (Typeable)
getWindowIconCache :: TaffyIO WindowIconCache
getWindowIconCache = getStateDefault buildWindowIconCache
buildWindowIconCache :: TaffyIO WindowIconCache
buildWindowIconCache = do
cacheEntries <- liftIO $ MV.newMVar M.empty
let cache = WindowIconCache {windowIconCacheEntries = cacheEntries}
context <- ask
iconTheme <- liftIO Gtk.iconThemeGetDefault
_ <- liftIO $ Gtk.onIconThemeChanged iconTheme $ do
invalidateThemeWindowIconCacheEntries cacheEntries
void $ runReaderT refreshTaffyWindows context
maybeX11Context <- asks x11ContextVar
forM_ maybeX11Context $
const $
void $
subscribeToPropertyEvents [ewmhWMIcon] $ \case
PropertyEvent _ _ _ _ windowId' _ _ _ ->
liftIO $ invalidateEWMHWindowIconCacheEntriesForWindow cacheEntries windowId'
_ -> return ()
return cache
getCachedWindowIcon ::
WindowIconCacheKey ->
TaffyIO (Maybe Gdk.Pixbuf) ->
TaffyIO (Maybe Gdk.Pixbuf)
getCachedWindowIcon cacheKey buildIcon = do
WindowIconCache {windowIconCacheEntries = entriesVar} <- getWindowIconCache
cached <- liftIO $ M.lookup cacheKey <$> MV.readMVar entriesVar
case cached of
Just icon -> return $ Just icon
Nothing -> do
icon <- buildIcon
liftIO $
forM_ icon $
\resolvedIcon ->
MV.modifyMVar_ entriesVar (return . M.insert cacheKey resolvedIcon)
return icon
isThemeIconCacheKey :: WindowIconCacheKey -> Bool
isThemeIconCacheKey (DesktopEntryIconCacheKey _ _) = True
isThemeIconCacheKey (ClassIconCacheKey _ _) = True
isThemeIconCacheKey (EWMHIconCacheKey _ _) = False
invalidateThemeWindowIconCacheEntries ::
MV.MVar (M.Map WindowIconCacheKey Gdk.Pixbuf) ->
IO ()
invalidateThemeWindowIconCacheEntries entriesVar =
MV.modifyMVar_ entriesVar $
return . M.filterWithKey (\k _ -> not (isThemeIconCacheKey k))
invalidateEWMHWindowIconCacheEntriesForWindow ::
MV.MVar (M.Map WindowIconCacheKey Gdk.Pixbuf) ->
X11Window ->
IO ()
invalidateEWMHWindowIconCacheEntriesForWindow entriesVar changedWindow =
MV.modifyMVar_ entriesVar $
return . M.filterWithKey keepEntry
where
keepEntry (EWMHIconCacheKey _ cachedWindow) _ = cachedWindow /= changedWindow
keepEntry _ _ = True
invalidateWindowIconCacheForWindow ::
X11Window ->
TaffyIO ()
invalidateWindowIconCacheForWindow windowId' = do
WindowIconCache {windowIconCacheEntries = entriesVar} <- getWindowIconCache
liftIO $ invalidateEWMHWindowIconCacheEntriesForWindow entriesVar windowId'
-- | Convert a C array of integer pixels in the ARGB format to the ABGR format.
-- Returns an unmanged Ptr that points to a block of memory that must be freed
-- manually.
pixelsARGBToBytesABGR ::
(Storable a, Bits a, Num a, Integral a) =>
Ptr a -> Int -> IO (Ptr Word8)
pixelsARGBToBytesABGR ptr size = do
target <- mallocArray (size * 4)
let writeIndex i = do
bits <- peekElemOff ptr i
let b = toByte bits
g = toByte $ bits `shift` (-8)
r = toByte $ bits `shift` (-16)
a = toByte $ bits `shift` (-24)
baseTarget = 4 * i
doPoke offset = pokeElemOff target (baseTarget + offset)
toByte = fromIntegral . (.&. 0xFF)
doPoke 0 r
doPoke 1 g
doPoke 2 b
doPoke 3 a
writeIndexAndNext i
| i >= size = return ()
| otherwise = writeIndex i >> writeIndexAndNext (i + 1)
writeIndexAndNext 0
return target
-- | Pick the best icon candidate for a target size from EWMH icon variants.
selectEWMHIcon :: Int32 -> [EWMHIcon] -> Maybe EWMHIcon
selectEWMHIcon imgSize icons = listToMaybe prefIcon
where
sortedIcons = sortBy (comparing ewmhHeight) icons
smallestLargerIcon =
take 1 $ dropWhile ((<= fromIntegral imgSize) . ewmhHeight) sortedIcons
largestIcon = take 1 $ sortBy (comparing (Down . ewmhHeight)) icons
prefIcon = smallestLargerIcon ++ largestIcon
-- | Create a pixbuf from the best matching EWMH icon for the requested size.
getPixbufFromEWMHIcons :: Int32 -> [EWMHIcon] -> IO (Maybe Gdk.Pixbuf)
getPixbufFromEWMHIcons size = traverse pixBufFromEWMHIcon . selectEWMHIcon size
-- | Create a pixbuf from the pixel data in an EWMHIcon.
pixBufFromEWMHIcon :: EWMHIcon -> IO Gdk.Pixbuf
pixBufFromEWMHIcon EWMHIcon {ewmhWidth = w, ewmhHeight = h, ewmhPixelsARGB = px} = do
let width = fromIntegral w
height = fromIntegral h
rowStride = width * 4
wPtr <- pixelsARGBToBytesABGR px (w * h)
Gdk.pixbufNewFromData
wPtr
Gdk.ColorspaceRgb
True
8
width
height
rowStride
(Just free)
-- | Read EWMH icon data from an X11 window and convert it into a pixbuf.
getIconPixBufFromEWMH :: Int32 -> X11Window -> X11Property (Maybe Gdk.Pixbuf)
getIconPixBufFromEWMH size x11WindowId = runMaybeT $ do
ewmhData <- MaybeT $ getWindowIconsData x11WindowId
MaybeT $ lift $ withEWMHIcons ewmhData (getPixbufFromEWMHIcons size)
-- | Resolve and cache the EWMH icon for an X11 window.
getCachedIconPixBufFromEWMH :: Int32 -> X11Window -> TaffyIO (Maybe Gdk.Pixbuf)
getCachedIconPixBufFromEWMH size x11WindowId =
getCachedWindowIcon
(EWMHIconCacheKey size x11WindowId)
(runX11Def Nothing $ getIconPixBufFromEWMH size x11WindowId)
-- | Create a pixbuf with the indicated RGBA color.
pixBufFromColor ::
(MonadIO m) =>
Int32 -> Word32 -> m Gdk.Pixbuf
pixBufFromColor imgSize c = do
pixbuf <-
fromMaybe
(error "pixBufFromColor: failed to allocate pixbuf")
<$> Gdk.pixbufNew Gdk.ColorspaceRgb True 8 imgSize imgSize
Gdk.pixbufFill pixbuf c
return pixbuf
-- | Resolve a desktop entry by X11 class name.
getDirectoryEntryByClass ::
String ->
TaffyIO (Maybe DesktopEntry)
getDirectoryEntryByClass klass = do
entries <- MM.lookup klass <$> getDirectoryEntriesByClassName
when (length entries > 1) $
liftIO $
logM "System.Taffybar.WindowIcon" DEBUG $
printf
"Class \"%s\" has multiple desktop entries: %s"
klass
(intercalate ", " $ map deFilename entries)
return $ listToMaybe entries
-- | Try icon lookup for each parsed class component until one succeeds.
getWindowIconForAllClasses ::
(Monad m) =>
(p -> String -> m (Maybe a)) -> p -> String -> m (Maybe a)
getWindowIconForAllClasses doOnClass size klass =
foldr combine (return Nothing) $ parseWindowClasses klass
where
combine theClass =
maybeTCombine (doOnClass size theClass)
-- | Resolve a window icon through desktop-entry icon metadata.
getWindowIconFromDesktopEntryByClasses ::
Int32 -> String -> TaffyIO (Maybe Gdk.Pixbuf)
getWindowIconFromDesktopEntryByClasses =
getWindowIconForAllClasses getWindowIconFromDesktopEntryByClass
where
getWindowIconFromDesktopEntryByClass size klass =
runMaybeT $ do
entry <- MaybeT $ getDirectoryEntryByClass klass
lift $
logPrintF
"System.Taffybar.WindowIcon"
DEBUG
"Using desktop entry for icon %s"
(deFilename entry, klass)
MaybeT $ lift $ getImageForDesktopEntry size entry
-- | Resolve and cache a window icon through desktop-entry icon metadata.
getCachedWindowIconFromDesktopEntryByClasses ::
Int32 -> String -> TaffyIO (Maybe Gdk.Pixbuf)
getCachedWindowIconFromDesktopEntryByClasses size klass =
getCachedWindowIcon
(DesktopEntryIconCacheKey size klass)
(getWindowIconFromDesktopEntryByClasses size klass)
-- | Resolve a window icon directly by class names in the icon theme.
getWindowIconFromClasses :: Int32 -> String -> IO (Maybe Gdk.Pixbuf)
getWindowIconFromClasses =
getWindowIconForAllClasses getWindowIconFromClass
where
getWindowIconFromClass size klass = loadPixbufByName size (T.pack klass)
-- | Resolve and cache a window icon directly by class names in the icon theme.
getCachedWindowIconFromClasses ::
Int32 -> String -> TaffyIO (Maybe Gdk.Pixbuf)
getCachedWindowIconFromClasses size klass =
getCachedWindowIcon
(ClassIconCacheKey size klass)
(liftIO $ getWindowIconFromClasses size klass)