packages feed

taffybar-5.2.0: src/System/Taffybar/Widget/HyprlandLayout.hs

{-# LANGUAGE OverloadedStrings #-}

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

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

-- |
-- Module      : System.Taffybar.Widget.HyprlandLayout
-- Copyright   : (c) Ivan A. Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan A. Malison <IvanMalison@gmail.com>
-- Stability   : unstable
-- Portability : unportable
--
-- Simple text widget that shows the Hyprland layout used in the currently
-- active workspace.
module System.Taffybar.Widget.HyprlandLayout
  ( HyprlandLayoutConfig (..),
    defaultHyprlandLayoutConfig,
    hyprlandLayoutNew,
  )
where

import Control.Applicative ((<|>))
import Control.Concurrent (killThread)
import Control.Monad (void)
import Control.Monad.Trans.Class
import Control.Monad.Trans.Reader
import Data.Aeson (FromJSON (..), withObject, (.:?))
import Data.Default (Default (..))
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import GI.Gdk
import qualified GI.Gtk as Gtk
import System.Log.Logger (Priority (..))
import System.Taffybar.Context
import System.Taffybar.Hyprland
  ( runHyprlandCommandJsonT,
    runHyprlandCommandRawT,
  )
import qualified System.Taffybar.Information.Hyprland as Hypr
import System.Taffybar.Information.Wakeup (taffyForeverWithDelay)
import System.Taffybar.Util
import System.Taffybar.Widget.Util

-- | Configuration for 'hyprlandLayoutNew'.
data HyprlandLayoutConfig = HyprlandLayoutConfig
  { formatLayout :: T.Text -> TaffyIO T.Text,
    updateIntervalSeconds :: Double,
    onLeftClick :: Maybe [String],
    onRightClick :: Maybe [String]
  }

instance Default HyprlandLayoutConfig where
  def = defaultHyprlandLayoutConfig

-- | Default Hyprland layout widget configuration.
defaultHyprlandLayoutConfig :: HyprlandLayoutConfig
defaultHyprlandLayoutConfig =
  HyprlandLayoutConfig
    { formatLayout = return,
      updateIntervalSeconds = 1,
      onLeftClick = Nothing,
      onRightClick = Nothing
    }

-- | Create a new Hyprland Layout widget.
hyprlandLayoutNew :: HyprlandLayoutConfig -> TaffyIO Gtk.Widget
hyprlandLayoutNew config = do
  ctx <- ask
  label <- lift $ Gtk.labelNew (Nothing :: Maybe T.Text)
  _ <- widgetSetClassGI label "layout-label"

  let refresh = do
        layoutText <- getHyprlandLayoutText
        markup <- formatLayout config layoutText
        lift $ postGUIASync $ Gtk.labelSetMarkup label markup

  void refresh
  threadId <- taffyForeverWithDelay (updateIntervalSeconds config) (void refresh)

  ebox <- lift Gtk.eventBoxNew
  lift $ Gtk.containerAdd ebox label
  _ <- lift $ Gtk.onWidgetButtonPressEvent ebox $ dispatchButtonEvent ctx config
  _ <- lift $ Gtk.onWidgetUnrealize ebox $ killThread threadId
  lift $ Gtk.widgetShowAll ebox
  Gtk.toWidget ebox

-- | Call the configured dispatch action depending on click.
dispatchButtonEvent :: Context -> HyprlandLayoutConfig -> EventButton -> IO Bool
dispatchButtonEvent context config btn = do
  pressType <- getEventButtonType btn
  buttonNumber <- getEventButtonButton btn
  case pressType of
    EventTypeButtonPress ->
      case buttonNumber of
        1 -> runReaderT (dispatchMaybe $ onLeftClick config) context >> return True
        3 -> runReaderT (dispatchMaybe $ onRightClick config) context >> return True
        _ -> return False
    _ -> return False

-- | Dispatch a Hyprland command if provided.
dispatchMaybe :: Maybe [String] -> TaffyIO ()
dispatchMaybe maybeArgs =
  case maybeArgs of
    Nothing -> return ()
    Just args -> do
      result <- runHyprlandCommandRawT (Hypr.hyprCommand ("dispatch" : args))
      case result of
        Left err ->
          logPrintF
            "System.Taffybar.Widget.HyprlandLayout"
            WARNING
            "Failed to dispatch Hyprland command: %s"
            (show err)
        Right _ -> return ()

-- Hyprland JSON helpers

newtype HyprlandActiveWorkspace = HyprlandActiveWorkspace
  { hawLayout :: Maybe Text
  }
  deriving (Show, Eq)

instance FromJSON HyprlandActiveWorkspace where
  parseJSON = withObject "HyprlandActiveWorkspace" $ \v -> do
    layout <- v .:? "layout" <|> v .:? "layoutName" <|> v .:? "layoutname"
    return $ HyprlandActiveWorkspace layout

getHyprlandLayoutText :: TaffyIO T.Text
getHyprlandLayoutText = do
  result <- runHyprctlJson ["-j", "activeworkspace"]
  case result of
    Left err ->
      logPrintF
        "System.Taffybar.Widget.HyprlandLayout"
        WARNING
        "hyprctl activeworkspace failed: %s"
        err
        >> return ""
    Right (HyprlandActiveWorkspace layout) ->
      return $ fromMaybe "" layout

runHyprctlJson :: (FromJSON a) => [String] -> TaffyIO (Either String a)
runHyprctlJson args = do
  let args' =
        case args of
          ("-j" : rest) -> rest
          _ -> args
  result <- runHyprlandCommandJsonT (Hypr.hyprCommandJson args')
  pure $ case result of
    Left err -> Left (show err)
    Right out -> Right out