packages feed

taffybar-4.1.2: 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.Gdk.Structs.EventScroll as GdkEvent
import qualified GI.Gdk.Enums as Gdk
import qualified GI.Gtk as Gtk
import qualified GI.GLib as G
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)
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
  , backlightIcon :: T.Text
  -- ^ Nerd font icon character (default U+F0EB, \xF0EB).
  }

-- | 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))
  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

-- | 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

-- | 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)

  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

    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

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