packages feed

taffybar-4.1.2: src/System/Taffybar/Widget/HyprlandWindows.hs

{-# LANGUAGE OverloadedStrings #-}

-----------------------------------------------------------------------------
-- |
-- Module      : System.Taffybar.Widget.HyprlandWindows
-- Copyright   : (c) Ivan A. Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan A. Malison
-- Stability   : unstable
-- Portability : unportable
--
-- Menu widget that shows the title of the currently focused Hyprland window
-- and that, when clicked, displays a menu from which the user may select a
-- window to which to switch focus.
-----------------------------------------------------------------------------

module System.Taffybar.Widget.HyprlandWindows where

import           Control.Concurrent (killThread)
import           Control.Monad (forM_, void)
import           Control.Monad.Trans.Class
import           Control.Monad.Trans.Reader
import           Control.Monad.Trans.Maybe
import           Data.Default (Default(..))
import           Data.List (find)
import qualified Data.Text as T
import qualified GI.Gtk as Gtk
import           System.Log.Logger (Priority(..))
import           System.Taffybar.Context
import           System.Taffybar.Hyprland (runHyprlandCommandRawT)
import qualified System.Taffybar.Information.Hyprland as Hypr
import           System.Taffybar.Util
import           System.Taffybar.Widget.Generic.DynamicMenu
import           System.Taffybar.Widget.Generic.ScalingImage (scalingImage)
import           System.Taffybar.Widget.Util
import           System.Taffybar.Widget.HyprlandWorkspaces
  ( HyprlandWindow(..)
  , HyprlandWindowIconPixbufGetter
  , HyprlandClient
  , defaultHyprlandGetWindowIconPixbuf
  , getActiveWindowAddress
  , runHyprctlJson
  , windowFromClient
  )

-- | Window menu widget configuration for Hyprland.
data HyprlandWindowsConfig = HyprlandWindowsConfig
  { getMenuLabel :: HyprlandWindow -> TaffyIO T.Text
  -- ^ A monadic function used to build labels for windows in the menu.
  , getActiveLabel :: Maybe HyprlandWindow -> TaffyIO T.Text
  -- ^ Action to build the label text for the active window.
  , getActiveWindowIconPixbuf :: Maybe HyprlandWindowIconPixbufGetter
  -- ^ Optional function to retrieve a pixbuf to show next to the window label.
  , updateIntervalSeconds :: Double
  }

truncatedGetMenuLabel :: Int -> HyprlandWindow -> TaffyIO T.Text
truncatedGetMenuLabel maxLength window =
  return $ truncateText maxLength (T.pack $ windowTitle window)

defaultGetMenuLabel :: HyprlandWindow -> TaffyIO T.Text
defaultGetMenuLabel = truncatedGetMenuLabel 35

defaultGetActiveLabel :: Maybe HyprlandWindow -> TaffyIO T.Text
defaultGetActiveLabel = maybe (return "") defaultGetMenuLabel

defaultHyprlandWindowsConfig :: HyprlandWindowsConfig
defaultHyprlandWindowsConfig =
  HyprlandWindowsConfig
  { getMenuLabel = defaultGetMenuLabel
  , getActiveLabel = defaultGetActiveLabel
  , getActiveWindowIconPixbuf = Just defaultHyprlandGetWindowIconPixbuf
  , updateIntervalSeconds = 1
  }

instance Default HyprlandWindowsConfig where
  def = defaultHyprlandWindowsConfig

-- | Create a new Hyprland Windows widget.
hyprlandWindowsNew :: HyprlandWindowsConfig -> TaffyIO Gtk.Widget
hyprlandWindowsNew config = do
  hbox <- lift $ Gtk.boxNew Gtk.OrientationHorizontal 0

  refreshIcon <- case getActiveWindowIconPixbuf config of
    Just getIcon -> do
      (rf, icon) <- buildWindowsIcon getIcon
      Gtk.boxPackStart hbox icon True True 0
      pure rf
    Nothing -> pure (pure ())

  (setLabelTitle, label) <- buildWindowsLabel
  Gtk.boxPackStart hbox label True True 0
  let refreshLabel = do
        activeWindow <- getActiveHyprlandWindow
        labelText <- getActiveLabel config activeWindow
        lift $ setLabelTitle labelText

  let refresh = refreshLabel >> lift refreshIcon
  ctx <- ask
  void refresh
  threadId <- lift $ foreverWithDelay (updateIntervalSeconds config) $
    void $ runReaderT refresh ctx

  _ <- lift $ Gtk.onWidgetUnrealize hbox $ killThread threadId

  Gtk.widgetShowAll hbox
  boxWidget <- Gtk.toWidget hbox

  runTaffy <- asks (flip runReaderT)
  menu <- dynamicMenuNew
    DynamicMenuConfig { dmClickWidget = boxWidget
                      , dmPopulateMenu = runTaffy . fillMenu config
                      }

  widgetSetClassGI menu "windows"

buildWindowsLabel :: TaffyIO (T.Text -> IO (), Gtk.Widget)
buildWindowsLabel = do
  label <- lift $ Gtk.labelNew Nothing
  let setLabelTitle title = postGUIASync $ Gtk.labelSetMarkup label title
  (setLabelTitle,) <$> Gtk.toWidget label

buildWindowsIcon :: HyprlandWindowIconPixbufGetter -> TaffyIO (IO (), Gtk.Widget)
buildWindowsIcon windowIconPixbufGetter = do
  runTaffy <- asks (flip runReaderT)
  let getActiveWindowPixbuf size = runTaffy . runMaybeT $ do
        wd <- MaybeT getActiveHyprlandWindow
        MaybeT $ windowIconPixbufGetter size wd

  (imageWidget, updateImage) <- scalingImage getActiveWindowPixbuf Gtk.OrientationHorizontal
  return (postGUIASync updateImage, imageWidget)

getActiveHyprlandWindow :: TaffyIO (Maybe HyprlandWindow)
getActiveHyprlandWindow = find windowActive <$> getHyprlandWindows

getHyprlandWindows :: TaffyIO [HyprlandWindow]
getHyprlandWindows = do
  activeAddr <- getActiveWindowAddress
  clientsResult <- runHyprctlJson ["-j", "clients"]
  case clientsResult of
    Left err ->
      logPrintF "System.Taffybar.Widget.HyprlandWindows" WARNING
        "hyprctl clients failed: %s" err >>
      return []
    Right clients ->
      return $ map (windowFromClient activeAddr) (clients :: [HyprlandClient])

fillMenu :: Gtk.IsMenuShell a => HyprlandWindowsConfig -> a -> ReaderT Context IO ()
fillMenu config menu = ask >>= \context -> do
  windowIds <- getHyprlandWindows
  forM_ windowIds $ \windowData ->
    lift $ do
      labelText <- runReaderT (getMenuLabel config windowData) context
      let focusCallback = runReaderT (focusHyprlandWindow windowData) context >>
                          return True
      item <- Gtk.menuItemNewWithLabel labelText
      _ <- Gtk.onWidgetButtonPressEvent item $ const focusCallback
      Gtk.menuShellAppend menu item
      Gtk.widgetShow item

focusHyprlandWindow :: HyprlandWindow -> TaffyIO ()
focusHyprlandWindow windowData = do
  result <-
    runHyprlandCommandRawT $
      Hypr.hyprCommand
        [ "dispatch"
        , "focuswindow"
        , "address:" <> T.unpack (windowAddress windowData)
        ]
  case result of
    Left err ->
      logPrintF "System.Taffybar.Widget.HyprlandWindows" WARNING
        "Failed to focus window: %s" (show err)
    Right _ -> return ()