packages feed

taffybar-7.0.0: src/System/Taffybar/Information/Layout/EWMH.hs

{-# LANGUAGE OverloadedStrings #-}

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

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

-- |
-- Module      : System.Taffybar.Information.Layout.EWMH
-- Copyright   : (c) Ivan A. Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan A. Malison
-- Stability   : unstable
-- Portability : unportable
--
-- Shared XMonad layout provider using an X11 property subscription and a
-- broadcast channel + state MVar.
module System.Taffybar.Information.Layout.EWMH
  ( EWMHLayoutProviderConfig (..),
    defaultEWMHLayoutProviderConfig,
    defaultEWMHLayoutState,
    getEWMHLayoutStateChanAndVar,
    getEWMHLayoutStateChanAndVarWith,
    getEWMHLayoutState,
    getEWMHLayoutStateWith,
    switchEWMHLayoutBy,
    xLayoutProp,
  )
where

import Control.Concurrent.MVar
import Control.Concurrent.STM.TChan
import Control.Exception.Enclosed (catchAny)
import Control.Monad (void)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.STM (atomically)
import Control.Monad.Trans.Reader (ask, runReaderT)
import qualified Data.Text as T
import System.Log.Logger (Priority (..), logM)
import System.Taffybar.Context
  ( TaffyIO,
    getStateDefault,
    runX11Def,
    subscribeToPropertyEvents,
    taffyFork,
  )
import System.Taffybar.Information.Layout.Model
import System.Taffybar.Information.X11DesktopInfo
  ( X11Property,
    getAtom,
    readAsString,
    sendCommandEvent,
  )

data EWMHLayoutProviderConfig = EWMHLayoutProviderConfig
  { layoutSnapshotGetter :: TaffyIO T.Text,
    layoutUpdateEvents :: [String]
  }

defaultEWMHLayoutProviderConfig :: EWMHLayoutProviderConfig
defaultEWMHLayoutProviderConfig =
  EWMHLayoutProviderConfig
    { layoutSnapshotGetter = buildEWMHLayoutSnapshot,
      layoutUpdateEvents = [xLayoutProp]
    }

defaultEWMHLayoutState :: LayoutSnapshot
defaultEWMHLayoutState =
  LayoutSnapshot
    { layoutBackend = LayoutBackendXMonad,
      layoutRevision = 0,
      layoutName = ""
    }

newtype EWMHLayoutStateChanVar
  = EWMHLayoutStateChanVar
      (TChan LayoutSnapshot, MVar LayoutSnapshot)

wLog :: (MonadIO m) => Priority -> String -> m ()
wLog level message =
  liftIO $ logM "System.Taffybar.Information.Layout.EWMH" level message

getEWMHLayoutStateChanAndVar ::
  TaffyIO (TChan LayoutSnapshot, MVar LayoutSnapshot)
getEWMHLayoutStateChanAndVar =
  getEWMHLayoutStateChanAndVarWith defaultEWMHLayoutProviderConfig

getEWMHLayoutStateChanAndVarWith ::
  EWMHLayoutProviderConfig ->
  TaffyIO (TChan LayoutSnapshot, MVar LayoutSnapshot)
getEWMHLayoutStateChanAndVarWith cfg = do
  EWMHLayoutStateChanVar chanAndVar <- getStateDefault $ buildEWMHLayoutStateChanVar cfg
  pure chanAndVar

getEWMHLayoutState :: TaffyIO LayoutSnapshot
getEWMHLayoutState =
  getEWMHLayoutStateWith defaultEWMHLayoutProviderConfig

getEWMHLayoutStateWith :: EWMHLayoutProviderConfig -> TaffyIO LayoutSnapshot
getEWMHLayoutStateWith cfg = do
  (_, stateVar) <- getEWMHLayoutStateChanAndVarWith cfg
  liftIO $ readMVar stateVar

buildEWMHLayoutStateChanVar ::
  EWMHLayoutProviderConfig ->
  TaffyIO EWMHLayoutStateChanVar
buildEWMHLayoutStateChanVar cfg = do
  stateChan <- liftIO newBroadcastTChanIO
  stateVar <- liftIO $ newMVar defaultEWMHLayoutState
  taffyFork $ ewmhLayoutStateLoop cfg stateChan stateVar
  pure $ EWMHLayoutStateChanVar (stateChan, stateVar)

ewmhLayoutStateLoop ::
  EWMHLayoutProviderConfig ->
  TChan LayoutSnapshot ->
  MVar LayoutSnapshot ->
  TaffyIO ()
ewmhLayoutStateLoop cfg stateChan stateVar = do
  refreshEWMHLayoutState cfg stateChan stateVar
  setupEWMHLayoutSubscription cfg stateChan stateVar
    `catchAny` \err ->
      wLog WARNING $ "Failed to subscribe to XMonad layout events: " <> show err

setupEWMHLayoutSubscription ::
  EWMHLayoutProviderConfig ->
  TChan LayoutSnapshot ->
  MVar LayoutSnapshot ->
  TaffyIO ()
setupEWMHLayoutSubscription cfg stateChan stateVar = do
  ctx <- ask
  let refreshForEvent _ =
        runReaderT (refreshEWMHLayoutState cfg stateChan stateVar) ctx
  _ <- subscribeToPropertyEvents (layoutUpdateEvents cfg) (liftIO . void . refreshForEvent)
  pure ()

refreshEWMHLayoutState ::
  EWMHLayoutProviderConfig ->
  TChan LayoutSnapshot ->
  MVar LayoutSnapshot ->
  TaffyIO ()
refreshEWMHLayoutState cfg stateChan stateVar = do
  previous <- liftIO $ readMVar stateVar
  name <-
    layoutSnapshotGetter cfg
      `catchAny` \err -> do
        wLog WARNING $ "XMonad layout snapshot update failed: " <> show err
        pure $ layoutName previous
  let next =
        LayoutSnapshot
          { layoutBackend = LayoutBackendXMonad,
            layoutRevision = layoutRevision previous + 1,
            layoutName = name
          }
  liftIO $ do
    _ <- swapMVar stateVar next
    atomically $ writeTChan stateChan next

buildEWMHLayoutSnapshot :: TaffyIO T.Text
buildEWMHLayoutSnapshot =
  T.pack <$> runX11Def "" (readAsString Nothing xLayoutProp)

xLayoutProp :: String
xLayoutProp = "_XMONAD_CURRENT_LAYOUT"

switchEWMHLayoutBy :: Int -> X11Property ()
switchEWMHLayoutBy amount = do
  cmd <- getAtom xLayoutProp
  sendCommandEvent cmd (fromIntegral amount)