packages feed

taffybar-7.2.5: src/System/Taffybar/Widget/Workspaces.hs

{-# LANGUAGE OverloadedStrings #-}

-----------------------------------------------------------------------------

-----------------------------------------------------------------------------

-- |
-- Module      : System.Taffybar.Widget.Workspaces
-- Copyright   : (c) Ivan A. Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan A. Malison
-- Stability   : unstable
-- Portability : unportable
--
-- Channel-driven workspace widget that renders backend-agnostic workspace state.
module System.Taffybar.Widget.Workspaces
  ( WorkspacesConfig (..),
    WorkspaceWidgetController (..),
    ControllerConstructor,
    defaultWidgetBuilder,
    WindowIconPixbufGetter,
    defaultWorkspacesConfig,
    defaultEWMHWorkspacesConfig,
    workspacesNew,
    hideEmpty,
    sortWindowsByPosition,
    sortWindowsByStackIndex,
    scaledWindowIconPixbufGetter,
    constantScaleWindowIconPixbufGetter,
    handleIconGetterException,
    getWindowIconPixbufFromClassHints,
    getWindowIconPixbufFromDesktopEntry,
    getWindowIconPixbufFromClass,
    getWindowIconPixbufByClassHints,
    getWindowIconPixbufFromEWMH,
    defaultGetWindowIconPixbuf,
    unscaledDefaultGetWindowIconPixbuf,
    addCustomIconsToDefaultWithFallbackByPath,
    addCustomIconsAndFallback,
    defaultOnWorkspaceClick,
    defaultOnWorkspaceClickEWMH,
    defaultOnWindowClick,
  )
where

import qualified Control.Concurrent.MVar as MV
import Control.Concurrent.STM.TChan (TChan)
import Control.Monad (foldM, forM_, guard, when)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Trans.Reader (ask, asks, runReaderT)
import Data.Default (Default (..))
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Int (Int32)
import Data.List (elemIndex)
import qualified Data.Map.Strict as M
import Data.Maybe (listToMaybe)
import qualified Data.Text as T
import Data.Word (Word64)
import qualified GI.Gdk.Enums as Gdk
import qualified GI.Gdk.Structs.EventScroll as Gdk
import qualified GI.GdkPixbuf.Objects.Pixbuf as Gdk
import qualified GI.Gtk as Gtk
import System.Taffybar.Context (Backend (..), TaffyIO, backend)
import System.Taffybar.Information.EWMHDesktopInfo
  ( ewmhWMIcon,
  )
import System.Taffybar.Information.Workspaces.EWMH
  ( EWMHWorkspaceProviderConfig,
    defaultEWMHWorkspaceProviderConfig,
    getEWMHWorkspaceStateChanAndVarWith,
    workspaceUpdateEvents,
  )
import System.Taffybar.Information.Workspaces.Hyprland
  ( HyprlandWorkspaceProviderConfig,
    defaultHyprlandWorkspaceProviderConfig,
    getHyprlandWorkspaceStateChanAndVarWith,
  )
import System.Taffybar.Information.Workspaces.Model
import System.Taffybar.Information.Workspaces.Support
  ( WindowIconPixbufGetter,
    addCustomIconsAndFallback,
    addCustomIconsToDefaultWithFallbackByPath,
    constantScaleWindowIconPixbufGetter,
    defaultGetWindowIconPixbuf,
    defaultOnWindowClick,
    defaultOnWorkspaceClick,
    defaultOnWorkspaceClickEWMH,
    getWindowIconPixbufByClassHints,
    getWindowIconPixbufFromClass,
    getWindowIconPixbufFromClassHints,
    getWindowIconPixbufFromDesktopEntry,
    getWindowIconPixbufFromEWMH,
    handleIconGetterException,
    scaledWindowIconPixbufGetter,
    sortWindowsByPosition,
    sortWindowsByStackIndex,
    unscaledDefaultGetWindowIconPixbuf,
  )
import System.Taffybar.Util (postGUIASync)
import System.Taffybar.Widget.Generic.AutoSizeImage (ImageScaleStrategy)
import System.Taffybar.Widget.Generic.ChannelWidget (channelWidgetNew)
import System.Taffybar.Widget.Generic.ScalingImage
  ( getScalingImageStrategy,
    scalingImageNew,
  )
import System.Taffybar.Widget.Util
  ( WindowIconWidget (..),
    buildBottomLeftAlignedBox,
    buildContentsBox,
    buildOverlayWithPassThrough,
    computeIconStripLayout,
    mkWindowIconWidgetBase,
    syncWidgetPool,
    updateWidgetClasses,
    widgetSetClassGI,
    windowStatusClassFromFlags,
  )
import System.Taffybar.WindowIcon
  ( pixBufFromColor,
  )

data WorkspaceWidgetController = WorkspaceWidgetController
  { controllerWidget :: Gtk.Widget,
    controllerUpdate :: WorkspaceInfo -> TaffyIO (),
    controllerRefreshIcons :: WorkspaceInfo -> TaffyIO ()
  }

type ControllerConstructor =
  WorkspacesConfig -> WorkspaceInfo -> TaffyIO WorkspaceWidgetController

data WorkspacesConfig = WorkspacesConfig
  { widgetGap :: Int,
    widgetBuilder :: ControllerConstructor,
    iconSize :: Maybe Int32,
    maxIcons :: Maybe Int,
    minIcons :: Int,
    getWindowIconPixbuf :: WindowIconPixbufGetter,
    labelSetter :: WorkspaceInfo -> TaffyIO String,
    showWorkspaceFn :: WorkspaceInfo -> Bool,
    iconSort :: [WindowInfo] -> TaffyIO [WindowInfo],
    hyprlandWorkspaceProviderConfig :: HyprlandWorkspaceProviderConfig,
    urgentWorkspaceState :: Bool,
    onWorkspaceClick :: WorkspaceInfo -> TaffyIO (),
    onWindowClick :: WindowInfo -> TaffyIO ()
  }

data WorkspaceState
  = Active
  | Visible
  | Hidden
  | Empty
  | Urgent
  deriving (Show, Eq)

getCSSClass :: (Show s) => s -> T.Text
getCSSClass = T.toLower . T.pack . show

cssWorkspaceStates :: [T.Text]
cssWorkspaceStates = map getCSSClass [Active, Visible, Hidden, Empty, Urgent]

setWorkspaceWidgetStatusClass ::
  (MonadIO m, Gtk.IsWidget a) => WorkspaceState -> a -> m ()
setWorkspaceWidgetStatusClass ws widget =
  updateWidgetClasses
    widget
    [getCSSClass ws]
    cssWorkspaceStates

-- | Build the common overlay layout used by workspace widgets:
-- window icons are the base content and the workspace label is overlaid in the
-- bottom-left corner.
buildWorkspaceIconLabelOverlay ::
  (MonadIO m) =>
  Gtk.Widget ->
  Gtk.Widget ->
  m Gtk.Widget
buildWorkspaceIconLabelOverlay iconsWidget labelWidget = do
  base <- buildContentsBox iconsWidget
  overlayLabel <- buildBottomLeftAlignedBox "overlay-box" labelWidget
  buildOverlayWithPassThrough base [overlayLabel]

mkWorkspaceIconWidget ::
  ImageScaleStrategy ->
  Maybe Int32 ->
  Bool ->
  (Int32 -> a -> IO (Maybe Gdk.Pixbuf)) ->
  (Int32 -> IO Gdk.Pixbuf) ->
  IO (WindowIconWidget a)
mkWorkspaceIconWidget strategy mSize transparentOnNone getPixbufFor mkTransparent = do
  base <- mkWindowIconWidgetBase mSize
  let getPixbuf size = do
        mWin <- MV.readMVar (iconWindow base)
        case mWin of
          Nothing ->
            if transparentOnNone
              then Just <$> mkTransparent size
              else return Nothing
          Just w -> do
            pb <- getPixbufFor size w
            case pb of
              Just _ -> return pb
              Nothing ->
                if transparentOnNone
                  then Just <$> mkTransparent size
                  else return Nothing
  (imageWidget, refreshImage) <-
    scalingImageNew
      strategy
      getPixbuf
      Gtk.OrientationHorizontal
  _ <- widgetSetClassGI imageWidget "window-icon"
  forM_ mSize $ \s ->
    Gtk.widgetSetSizeRequest imageWidget (fromIntegral s) (fromIntegral s)
  Gtk.containerAdd (iconContainer base) imageWidget
  return base {iconImage = imageWidget, iconForceUpdate = refreshImage}

data WorkspaceEntry = WorkspaceEntry
  { entryWrapper :: Gtk.Widget,
    entryButton :: Gtk.EventBox,
    entryController :: WorkspaceWidgetController,
    entryWorkspaceRef :: IORef WorkspaceInfo,
    entryLastWorkspace :: WorkspaceInfo
  }

data WorkspaceCache = WorkspaceCache
  { cacheEntries :: M.Map WorkspaceIdentity WorkspaceEntry,
    cacheOrder :: [WorkspaceIdentity],
    cacheLastWorkspaces :: [WorkspaceInfo],
    cacheLastRevision :: Word64
  }

defaultChannelEWMHWorkspaceProviderConfig :: EWMHWorkspaceProviderConfig
defaultChannelEWMHWorkspaceProviderConfig =
  defaultEWMHWorkspaceProviderConfig
    { workspaceUpdateEvents =
        ewmhWMIcon : workspaceUpdateEvents defaultEWMHWorkspaceProviderConfig
    }

defaultEWMHWorkspaceStateSource ::
  TaffyIO (TChan WorkspaceSnapshot, MV.MVar WorkspaceSnapshot)
defaultEWMHWorkspaceStateSource =
  getEWMHWorkspaceStateChanAndVarWith defaultChannelEWMHWorkspaceProviderConfig

autoWorkspaceStateSource :: WorkspacesConfig -> TaffyIO (TChan WorkspaceSnapshot, MV.MVar WorkspaceSnapshot)
autoWorkspaceStateSource cfg = do
  backendType <- asks backend
  case backendType of
    BackendWayland -> getHyprlandWorkspaceStateChanAndVarWith (hyprlandWorkspaceProviderConfig cfg)
    BackendX11 -> defaultEWMHWorkspaceStateSource

defaultWorkspacesConfig :: WorkspacesConfig
defaultWorkspacesConfig =
  WorkspacesConfig
    { widgetGap = 0,
      widgetBuilder = defaultWidgetBuilder,
      iconSize = Just 16,
      maxIcons = Nothing,
      minIcons = 0,
      getWindowIconPixbuf = defaultGetWindowIconPixbuf,
      labelSetter = return . T.unpack . workspaceName . workspaceIdentity,
      showWorkspaceFn = \ws -> hideEmpty ws && not (workspaceIsSpecial ws),
      iconSort = pure . sortWindowsByPosition,
      hyprlandWorkspaceProviderConfig = defaultHyprlandWorkspaceProviderConfig,
      urgentWorkspaceState = False,
      onWorkspaceClick = defaultOnWorkspaceClick,
      onWindowClick = defaultOnWindowClick
    }

defaultEWMHWorkspacesConfig :: WorkspacesConfig
defaultEWMHWorkspacesConfig = defaultWorkspacesConfig

defaultWidgetBuilder :: ControllerConstructor
defaultWidgetBuilder cfg wsInfo = do
  wsRef <- liftIO $ newIORef wsInfo
  lastWorkspaceRef <- liftIO $ newIORef wsInfo
  iconsRef <- liftIO $ newIORef []
  iconsBox <- liftIO $ Gtk.boxNew Gtk.OrientationHorizontal 0
  label <- liftIO $ Gtk.labelNew Nothing
  _ <- widgetSetClassGI label "workspace-label"
  iconsWidget <- Gtk.toWidget iconsBox
  labelWidget <- Gtk.toWidget label
  contents <- buildWorkspaceIconLabelOverlay iconsWidget labelWidget
  let updateController forceIcons newWs = do
        oldWs <- liftIO $ readIORef lastWorkspaceRef
        liftIO $ writeIORef wsRef newWs
        labelText <- labelSetter cfg newWs
        let wsState = toCSSState cfg newWs
            windowsChanged = workspaceWindows oldWs /= workspaceWindows newWs
        liftIO $ Gtk.labelSetMarkup label (T.pack labelText)
        setWorkspaceWidgetStatusClass wsState contents
        setWorkspaceWidgetStatusClass wsState label
        currentIcons <- liftIO $ readIORef iconsRef
        let needsIconUpdate =
              forceIcons
                || null currentIcons
                || windowsChanged
        updatedIcons <-
          if needsIconUpdate
            then updateWorkspaceIcons cfg wsRef iconsBox currentIcons newWs
            else return currentIcons
        when forceIcons $
          liftIO $
            forM_ updatedIcons iconForceUpdate
        liftIO $ writeIORef iconsRef updatedIcons
        liftIO $ writeIORef lastWorkspaceRef newWs
      controller =
        WorkspaceWidgetController
          { controllerWidget = contents,
            controllerUpdate = updateController False,
            controllerRefreshIcons = updateController True
          }
  controllerRefreshIcons controller wsInfo
  return controller

instance Default WorkspacesConfig where
  def = defaultWorkspacesConfig

hideEmpty :: WorkspaceInfo -> Bool
hideEmpty WorkspaceInfo {workspaceState = WorkspaceEmpty} = False
hideEmpty _ = True

workspacesNew :: WorkspacesConfig -> TaffyIO Gtk.Widget
workspacesNew cfg = do
  ctx <- ask
  cont <- liftIO $ Gtk.boxNew Gtk.OrientationHorizontal (fromIntegral $ widgetGap cfg)
  _ <- widgetSetClassGI cont "workspaces"
  cacheVar <- liftIO $ MV.newMVar (WorkspaceCache M.empty [] [] 0)
  (chan, snapshotVar) <- autoWorkspaceStateSource cfg
  initialSnapshot <- liftIO $ MV.readMVar snapshotVar
  renderWorkspaces cfg cont cacheVar initialSnapshot
  _ <-
    liftIO $
      Gtk.onWidgetRealize cont $ do
        latestSnapshot <- MV.readMVar snapshotVar
        postGUIASync $
          runReaderT
            (renderWorkspaces cfg cont cacheVar latestSnapshot)
            ctx
  _ <-
    liftIO $
      channelWidgetNew
        cont
        chan
        (\snapshot -> postGUIASync $ runReaderT (renderWorkspaces cfg cont cacheVar snapshot) ctx)
  Gtk.toWidget cont

renderWorkspaces ::
  WorkspacesConfig ->
  Gtk.Box ->
  MV.MVar WorkspaceCache ->
  WorkspaceSnapshot ->
  TaffyIO ()
renderWorkspaces cfg cont cacheVar snapshot = do
  ctx <- ask
  liftIO $
    MV.modifyMVar_ cacheVar $ \oldCache ->
      runReaderT (updateCache cfg cont cacheVar snapshot oldCache) ctx

updateCache ::
  WorkspacesConfig ->
  Gtk.Box ->
  MV.MVar WorkspaceCache ->
  WorkspaceSnapshot ->
  WorkspaceCache ->
  TaffyIO WorkspaceCache
updateCache cfg cont cacheVar snapshot oldCache = do
  let workspaces = snapshotWorkspaces snapshot
      forceIconRefresh =
        snapshotBackend snapshot == WorkspaceBackendEWMH
          && snapshotRevision snapshot /= cacheLastRevision oldCache
      applyVisibility wsInfo entry =
        if showWorkspaceFn cfg wsInfo
          then liftIO $ Gtk.widgetShow (entryButton entry)
          else liftIO $ Gtk.widgetHide (entryButton entry)
  if workspaces == cacheLastWorkspaces oldCache && not forceIconRefresh
    then do
      forM_ (M.elems $ cacheEntries oldCache) $ \entry ->
        applyVisibility (entryLastWorkspace entry) entry
      return oldCache
    else do
      let oldEntries = cacheEntries oldCache
          buildOrUpdate (cacheAcc, entriesAcc) wsInfo = do
            let wsKey = workspaceIdentity wsInfo
            entry <-
              case M.lookup wsKey oldEntries of
                Just existing
                  | entryLastWorkspace existing == wsInfo ->
                      if forceIconRefresh
                        then refreshWorkspaceEntryIcons existing wsInfo
                        else return existing
                  | otherwise -> updateWorkspaceEntry existing wsInfo
                Nothing -> buildWorkspaceEntry cfg cacheVar wsInfo
            return (M.insert wsKey entry cacheAcc, (wsInfo, entry) : entriesAcc)
      (newEntries, orderedEntriesRev) <-
        foldM buildOrUpdate (M.empty, []) workspaces
      let orderedEntries = reverse orderedEntriesRev
          removed = M.difference oldEntries newEntries
          added = M.difference newEntries oldEntries
      forM_ (M.elems removed) $
        liftIO . Gtk.containerRemove cont . entryWrapper
      forM_ (M.elems added) $ \entry -> do
        liftIO $ Gtk.containerAdd cont (entryWrapper entry)
        liftIO $ Gtk.widgetShowAll (entryWrapper entry)
      let desiredOrder = map (workspaceIdentity . fst) orderedEntries
          needsReorder =
            desiredOrder /= cacheOrder oldCache
              || not (M.null added)
              || not (M.null removed)
      when needsReorder $
        forM_ (zip [0 :: Int ..] orderedEntries) $ \(position, (_wsInfo, entry)) ->
          liftIO $ Gtk.boxReorderChild cont (entryWrapper entry) (fromIntegral position)
      forM_ orderedEntries $ uncurry applyVisibility
      return
        WorkspaceCache
          { cacheEntries = newEntries,
            cacheOrder = desiredOrder,
            cacheLastWorkspaces = workspaces,
            cacheLastRevision = snapshotRevision snapshot
          }

buildWorkspaceEntry ::
  WorkspacesConfig ->
  MV.MVar WorkspaceCache ->
  WorkspaceInfo ->
  TaffyIO WorkspaceEntry
buildWorkspaceEntry cfg cacheVar wsInfo = do
  wsRef <- liftIO $ newIORef wsInfo
  controller <- widgetBuilder cfg cfg wsInfo
  button <- liftIO Gtk.eventBoxNew
  _ <- widgetSetClassGI button "workspace-button"
  liftIO $ Gtk.eventBoxSetVisibleWindow button False
  liftIO $ Gtk.containerAdd button (controllerWidget controller)
  wrapperBox <- liftIO $ Gtk.boxNew Gtk.OrientationHorizontal 0
  liftIO $ Gtk.containerAdd wrapperBox button
  wrapper <- Gtk.toWidget wrapperBox
  _ <- widgetSetClassGI wrapper "workspace-wrapper"
  ctx <- ask
  _ <-
    liftIO $
      Gtk.onWidgetButtonPressEvent button $
        const $ do
          currentWs <- readIORef wsRef
          runReaderT (onWorkspaceClick cfg currentWs) ctx
          return True
  _ <-
    liftIO $
      Gtk.onWidgetScrollEvent button $ \scrollEvent -> do
        direction <- Gdk.getEventScrollDirection scrollEvent
        let scrollPrevious = runReaderT (switchWorkspaceRelative cfg cacheVar wsRef True) ctx
            scrollNext = runReaderT (switchWorkspaceRelative cfg cacheVar wsRef False) ctx
        case direction of
          Gdk.ScrollDirectionUp -> scrollPrevious
          Gdk.ScrollDirectionLeft -> scrollPrevious
          Gdk.ScrollDirectionDown -> scrollNext
          Gdk.ScrollDirectionRight -> scrollNext
          _ -> return False
  return
    WorkspaceEntry
      { entryWrapper = wrapper,
        entryButton = button,
        entryController = controller,
        entryWorkspaceRef = wsRef,
        entryLastWorkspace = wsInfo
      }

updateWorkspaceEntry ::
  WorkspaceEntry ->
  WorkspaceInfo ->
  TaffyIO WorkspaceEntry
updateWorkspaceEntry entry wsInfo = do
  liftIO $ writeIORef (entryWorkspaceRef entry) wsInfo
  controllerUpdate (entryController entry) wsInfo
  return entry {entryLastWorkspace = wsInfo}

refreshWorkspaceEntryIcons ::
  WorkspaceEntry ->
  WorkspaceInfo ->
  TaffyIO WorkspaceEntry
refreshWorkspaceEntryIcons entry wsInfo = do
  liftIO $ writeIORef (entryWorkspaceRef entry) wsInfo
  controllerRefreshIcons (entryController entry) wsInfo
  return entry {entryLastWorkspace = wsInfo}

switchWorkspaceRelative ::
  WorkspacesConfig ->
  MV.MVar WorkspaceCache ->
  IORef WorkspaceInfo ->
  Bool ->
  TaffyIO Bool
switchWorkspaceRelative cfg cacheVar wsRef moveBackward = do
  currentWs <- liftIO $ readIORef wsRef
  cache <- liftIO $ MV.readMVar cacheVar
  let identities = cacheOrder cache
      currentIdentity = workspaceIdentity currentWs
      step = if moveBackward then (-1) else 1
      getTargetIdentity = do
        idx <- elemIndex currentIdentity identities
        let total = length identities
        guard (total > 0)
        let wrappedIndex = (idx + step + total) `mod` total
        listToMaybe $ drop wrappedIndex identities
  case getTargetIdentity >>= (`M.lookup` cacheEntries cache) of
    Nothing -> return False
    Just targetEntry -> do
      targetWorkspace <- liftIO $ readIORef (entryWorkspaceRef targetEntry)
      onWorkspaceClick cfg targetWorkspace
      return True

activateWorkspaceIcon ::
  WorkspacesConfig ->
  WorkspaceInfo ->
  Maybe WindowInfo ->
  TaffyIO ()
activateWorkspaceIcon cfg currentWs maybeWindow = do
  backendType <- asks backend
  case (backendType, maybeWindow) of
    (BackendX11, Just windowInfo) -> onWindowClick cfg windowInfo
    _ -> onWorkspaceClick cfg currentWs

updateWorkspaceIcons ::
  WorkspacesConfig ->
  IORef WorkspaceInfo ->
  Gtk.Box ->
  [WindowIconWidget WindowInfo] ->
  WorkspaceInfo ->
  TaffyIO [WindowIconWidget WindowInfo]
updateWorkspaceIcons cfg workspaceRef iconsBox iconWidgets wsInfo = do
  sortedWindows <- iconSort cfg (workspaceWindows wsInfo)
  let (effectiveMinIcons, _targetLen, paddedWindows) =
        computeIconStripLayout
          (minIcons cfg)
          (maxIcons cfg)
          sortedWindows
      buildOne i = buildIconWidget cfg workspaceRef (i < effectiveMinIcons)
      updateOne = updateIconWidget
  syncWidgetPool iconsBox iconWidgets paddedWindows buildOne iconContainer updateOne

buildIconWidget ::
  WorkspacesConfig ->
  IORef WorkspaceInfo ->
  Bool ->
  TaffyIO (WindowIconWidget WindowInfo)
buildIconWidget cfg workspaceRef transparentOnNone = do
  ctx <- ask
  strategy <- getScalingImageStrategy
  let getPB size windowInfo = runReaderT (getWindowIconPixbuf cfg size windowInfo) ctx
  iconWidget <-
    liftIO $
      mkWorkspaceIconWidget
        strategy
        (iconSize cfg)
        transparentOnNone
        getPB
        (`pixBufFromColor` 0)
  _ <-
    liftIO $
      Gtk.onWidgetButtonPressEvent (iconContainer iconWidget) $
        const $ do
          maybeWindow <- MV.readMVar (iconWindow iconWidget)
          currentWs <- readIORef workspaceRef
          runReaderT (activateWorkspaceIcon cfg currentWs maybeWindow) ctx
          return True
  return iconWidget

updateIconWidget :: WindowIconWidget WindowInfo -> Maybe WindowInfo -> TaffyIO ()
updateIconWidget iconWidget windowData = do
  oldWindowData <- liftIO $ MV.swapMVar (iconWindow iconWidget) windowData
  Gtk.widgetSetTooltipText (iconContainer iconWidget) (windowTitle <$> windowData)
  let pixbufKeyChanged =
        (windowIconPixbufKey <$> oldWindowData) /= (windowIconPixbufKey <$> windowData)
  when pixbufKeyChanged $
    liftIO $
      iconForceUpdate iconWidget
  let statusString = maybe "inactive" getWindowStatusString windowData
      stateClasses =
        case windowData of
          Just windowInfo | windowPinned windowInfo -> [statusString, "pinned"]
          _ -> [statusString]
  updateWidgetClasses
    (iconContainer iconWidget)
    stateClasses
    windowIconStatusClasses

windowIconPixbufKey :: WindowInfo -> (WindowIdentity, Word64, [T.Text], T.Text)
windowIconPixbufKey windowInfo =
  (windowIdentity windowInfo, windowUpdateRevision windowInfo, windowClassHints windowInfo, windowTitle windowInfo)

windowIconStatusClasses :: [T.Text]
windowIconStatusClasses = ["active", "urgent", "minimized", "normal", "inactive", "pinned"]

getWindowStatusString :: WindowInfo -> T.Text
getWindowStatusString windowInfo =
  windowStatusClassFromFlags
    (windowMinimized windowInfo)
    (windowActive windowInfo)
    (windowUrgent windowInfo)

toCSSState :: WorkspacesConfig -> WorkspaceInfo -> WorkspaceState
toCSSState cfg wsInfo
  | urgentWorkspaceState cfg
      && workspaceState wsInfo == WorkspaceHidden
      && workspaceHasUrgentWindow wsInfo =
      Urgent
  | otherwise =
      case workspaceState wsInfo of
        WorkspaceActive -> Active
        WorkspaceVisible -> Visible
        WorkspaceHidden -> Hidden
        WorkspaceEmpty -> Empty