packages feed

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

{-# LANGUAGE OverloadedStrings #-}

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

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

-- |
-- Module      : System.Taffybar.Widget.Backlight
-- Copyright   : (c) Ivan A. Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan A. Malison
-- Stability   : unstable
-- Portability : unportable
--
-- Simple backlight widget using /sys/class/backlight and optional
-- brightnessctl adjustments.
module System.Taffybar.Widget.Backlight
  ( BacklightWidgetConfig (..),
    defaultBacklightWidgetConfig,
    backlightIconNew,
    backlightIconNewWith,
    backlightLabelNew,
    backlightLabelNewWith,
    backlightLabelNewChan,
    backlightLabelNewChanWith,
    backlightNew,
    backlightNewWith,
    backlightNewChan,
    backlightNewChanWith,
  )
where

import Control.Exception (SomeException, catch)
import Control.Monad (void)
import Control.Monad.IO.Class
import Data.Default (Default (..))
import qualified Data.Text as T
import qualified GI.GLib as G
import qualified GI.Gdk.Enums as Gdk
import qualified GI.Gdk.Structs.EventScroll as GdkEvent
import qualified GI.Gtk as Gtk
import System.Taffybar.Context (TaffyIO)
import qualified System.Taffybar.Information.Backlight as BL
import System.Taffybar.Util (postGUIASync, runCommand)
import System.Taffybar.Widget.Generic.ChannelWidget
import System.Taffybar.Widget.Generic.PollingLabel
import System.Taffybar.Widget.Util (buildIconLabelBox, widgetSetClassGI)
import Text.StringTemplate

-- | Configuration for the backlight widget.
data BacklightWidgetConfig = BacklightWidgetConfig
  { backlightPollingInterval :: Double,
    backlightDevice :: Maybe FilePath,
    backlightFormat :: String,
    backlightUnknownFormat :: String,
    backlightTooltipFormat :: Maybe String,
    backlightScrollStepPercent :: Maybe Int,
    backlightBrightnessctlPath :: FilePath,
    -- | Nerd font icon character (default U+F0EB, \xF0EB).
    backlightIcon :: T.Text
  }

-- | Default backlight widget configuration.
defaultBacklightWidgetConfig :: BacklightWidgetConfig
defaultBacklightWidgetConfig =
  BacklightWidgetConfig
    { backlightPollingInterval = 2,
      backlightDevice = Nothing,
      backlightFormat = "bl: $percent$%",
      backlightUnknownFormat = "bl: n/a",
      backlightTooltipFormat =
        Just "Device: $device$\nBrightness: $brightness$/$max$ ($percent$%)",
      backlightScrollStepPercent = Just 5,
      backlightBrightnessctlPath = "brightnessctl",
      backlightIcon = T.pack "\xF0EB"
    }

instance Default BacklightWidgetConfig where
  def = defaultBacklightWidgetConfig

-- | Create a backlight icon widget with default configuration.
backlightIconNew :: (MonadIO m) => m Gtk.Widget
backlightIconNew = backlightIconNewWith defaultBacklightWidgetConfig

-- | Create a backlight icon widget with the provided configuration.
backlightIconNewWith :: (MonadIO m) => BacklightWidgetConfig -> m Gtk.Widget
backlightIconNewWith config = liftIO $ do
  label <- Gtk.labelNew (Just (backlightIcon config))
  _ <- widgetSetClassGI label "backlight-icon"
  Gtk.widgetShowAll label
  Gtk.toWidget label

-- | Create a combined icon+label backlight widget with default configuration.
backlightNew :: (MonadIO m) => m Gtk.Widget
backlightNew = backlightNewWith defaultBacklightWidgetConfig

-- | Create a combined icon+label backlight widget.
backlightNewWith :: (MonadIO m) => BacklightWidgetConfig -> m Gtk.Widget
backlightNewWith config = liftIO $ do
  iconWidget <- backlightIconNewWith config
  labelWidget <- backlightLabelNewWith config
  buildIconLabelBox iconWidget labelWidget >>= (`widgetSetClassGI` "backlight")

-- | Create a combined icon+label backlight widget (channel-driven).
backlightNewChan :: TaffyIO Gtk.Widget
backlightNewChan = backlightNewChanWith defaultBacklightWidgetConfig

-- | Create a combined icon+label backlight widget (channel-driven).
backlightNewChanWith :: BacklightWidgetConfig -> TaffyIO Gtk.Widget
backlightNewChanWith config = do
  iconWidget <- liftIO $ backlightIconNewWith config
  labelWidget <- backlightLabelNewChanWith config
  liftIO $
    buildIconLabelBox iconWidget labelWidget
      >>= (`widgetSetClassGI` "backlight")

-- | Create a backlight widget with default configuration.
backlightLabelNew :: (MonadIO m) => m Gtk.Widget
backlightLabelNew = backlightLabelNewWith defaultBacklightWidgetConfig

-- | Create a backlight widget with the provided configuration.
backlightLabelNewWith :: (MonadIO m) => BacklightWidgetConfig -> m Gtk.Widget
backlightLabelNewWith config = liftIO $ do
  widget <-
    pollingLabelNewWithTooltip
      (backlightPollingInterval config)
      (formatBacklightWidget config)
  _ <- widgetSetClassGI widget "backlight-label"

  case backlightScrollStepPercent config of
    Nothing -> return ()
    Just step | step <= 0 -> return ()
    Just step -> do
      _ <- Gtk.onWidgetScrollEvent widget $ \scrollEvent -> do
        dir <- GdkEvent.getEventScrollDirection scrollEvent
        case dir of
          Gdk.ScrollDirectionUp -> adjustBacklight config step >> return True
          Gdk.ScrollDirectionDown -> adjustBacklight config (-step) >> return True
          Gdk.ScrollDirectionLeft -> adjustBacklight config step >> return True
          Gdk.ScrollDirectionRight -> adjustBacklight config (-step) >> return True
          _ -> return False
      return ()

  Gtk.widgetShowAll widget
  return widget

-- | Create a backlight widget driven by a 'TChan' (no polling in the widget).
--
-- Monitoring is handled in 'System.Taffybar.Information.Backlight' (udev when
-- possible, with interval refresh as a fallback).
backlightLabelNewChan :: TaffyIO Gtk.Widget
backlightLabelNewChan = backlightLabelNewChanWith defaultBacklightWidgetConfig

-- | Create a backlight widget driven by a 'TChan' (no polling in the widget).
backlightLabelNewChanWith :: BacklightWidgetConfig -> TaffyIO Gtk.Widget
backlightLabelNewChanWith config = do
  chan <-
    BL.getBacklightInfoChanWithInterval
      (backlightDevice config)
      (backlightPollingInterval config)
  initialInfo <- BL.getBacklightInfoState (backlightDevice config)

  liftIO $ do
    label <- Gtk.labelNew Nothing
    _ <- widgetSetClassGI label "backlight-label"

    let updateLabel info = do
          (labelText, tooltipText) <- formatBacklightWidgetFromInfo config info
          postGUIASync $ do
            Gtk.labelSetMarkup label labelText
            Gtk.widgetSetTooltipMarkup label tooltipText

        refreshNow = BL.getBacklightInfo (backlightDevice config) >>= updateLabel

    void $ Gtk.onWidgetRealize label $ updateLabel initialInfo

    case backlightScrollStepPercent config of
      Nothing -> return ()
      Just step | step <= 0 -> return ()
      Just step -> do
        _ <- Gtk.onWidgetScrollEvent label $ \scrollEvent -> do
          dir <- GdkEvent.getEventScrollDirection scrollEvent
          let doAdjust delta = do
                adjustBacklight config delta
                refreshNow
                return True
          case dir of
            Gdk.ScrollDirectionUp -> doAdjust step
            Gdk.ScrollDirectionDown -> doAdjust (-step)
            Gdk.ScrollDirectionLeft -> doAdjust step
            Gdk.ScrollDirectionRight -> doAdjust (-step)
            _ -> return False
        return ()

    Gtk.widgetShowAll label
    (Gtk.toWidget =<< channelWidgetNew label chan updateLabel)
      >>= (`widgetSetClassGI` "backlight-label")

formatBacklightWidget ::
  BacklightWidgetConfig ->
  IO (T.Text, Maybe T.Text)
formatBacklightWidget config = do
  info <- BL.getBacklightInfo (backlightDevice config)
  formatBacklightWidgetFromInfo config info

formatBacklightWidgetFromInfo ::
  BacklightWidgetConfig ->
  Maybe BL.BacklightInfo ->
  IO (T.Text, Maybe T.Text)
formatBacklightWidgetFromInfo config info =
  case info of
    Nothing -> return (T.pack $ backlightUnknownFormat config, Nothing)
    Just bl -> do
      attrs <- buildAttrs bl
      let labelText = renderTemplate (backlightFormat config) attrs
          tooltipText = fmap (`renderTemplate` attrs) (backlightTooltipFormat config)
      return (T.pack labelText, T.pack <$> tooltipText)

buildAttrs :: BL.BacklightInfo -> IO [(String, String)]
buildAttrs info = do
  let brightnessText = show $ BL.backlightBrightness info
      maxText = show $ BL.backlightMaxBrightness info
      percentText = show $ BL.backlightPercent info
      deviceText = BL.backlightDevice info
  brightness <- escapeText $ T.pack brightnessText
  maxBrightness <- escapeText $ T.pack maxText
  percent <- escapeText $ T.pack percentText
  device <- escapeText $ T.pack deviceText
  return
    [ ("brightness", brightness),
      ("max", maxBrightness),
      ("percent", percent),
      ("device", device)
    ]

renderTemplate :: String -> [(String, String)] -> String
renderTemplate template attrs = render $ setManyAttrib attrs (newSTMP template)

escapeText :: T.Text -> IO String
escapeText input = T.unpack <$> G.markupEscapeText input (-1)

adjustBacklight :: BacklightWidgetConfig -> Int -> IO ()
adjustBacklight config delta =
  void $ safeRunCommand (backlightBrightnessctlPath config) (brightnessctlArgs delta)
  where
    brightnessctlArgs change =
      let step = abs change
          direction = if change >= 0 then "+" else "-"
          deviceArgs = maybe [] (\dev -> ["-d", dev]) (backlightDevice config)
       in deviceArgs ++ ["set", show step ++ "%" ++ direction]

safeRunCommand :: FilePath -> [String] -> IO (Either String String)
safeRunCommand cmd args =
  catch (runCommand cmd args) $ \(e :: SomeException) ->
    return $ Left $ show e