packages feed

taffybar-7.2.7: src/System/Taffybar/Widget/Temperature.hs

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : System.Taffybar.Widget.Temperature
-- Copyright   : (c) Ivan Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan Malison <IvanMalison@gmail.com>
-- Stability   : unstable
-- Portability : unportable
--
-- A widget for displaying system temperature. Monitors thermal zones and/or
-- hwmon devices and displays the current temperature with configurable
-- thresholds for warning and critical states.
module System.Taffybar.Widget.Temperature
  ( -- * Combined icon+label widget
    temperatureNew,
    temperatureNewWith,
    temperatureNewChan,
    temperatureNewChanWith,

    -- * Icon-only widget
    temperatureIconNew,
    temperatureIconNewWith,

    -- * Label-only widget
    temperatureLabelNew,
    temperatureLabelNewWith,
    temperatureLabelNewChan,
    temperatureLabelNewChanWith,

    -- * Configuration
    TemperatureConfig (..),
    defaultTemperatureConfig,

    -- * Re-exports
    TemperatureUnit (..),
  )
where

import Control.Monad (void, when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Default (Default (..))
import Data.IORef (newIORef, readIORef, writeIORef)
import Data.List (intercalate)
import qualified Data.Text as T
import qualified GI.Gtk as Gtk
import System.Taffybar.Context (TaffyIO)
import System.Taffybar.Information.Temperature
import System.Taffybar.Util (postGUIASync)
import System.Taffybar.Widget.Generic.ChannelWidget (channelWidgetNew)
import System.Taffybar.Widget.Generic.PollingLabel (pollingLabelNewWithTooltip)
import System.Taffybar.Widget.Util (buildIconLabelBox, widgetSetClassGI)
import qualified Text.StringTemplate as ST

-- | Configuration for the temperature widget
data TemperatureConfig = TemperatureConfig
  { -- | Format string for the label. Available variables:
    -- temp (in configured unit), tempC (Celsius), tempF (Fahrenheit), tempK (Kelvin)
    tempFormat :: String,
    -- | Unit to use for the {temp} variable (default: Celsius)
    tempUnit :: TemperatureUnit,
    -- | Temperature (in Celsius) at which to show warning style (default: 70)
    tempWarningThreshold :: Double,
    -- | Temperature (in Celsius) at which to show critical style (default: 85)
    tempCriticalThreshold :: Double,
    -- | How often to poll for temperature updates, in seconds (default: 30)
    tempPollInterval :: Double,
    -- | Filter function to select which sensors to monitor (default: all)
    tempSensorFilter :: ThermalSensor -> Bool,
    -- | Select additional sensors to include only in the tooltip. Sensors
    -- selected by 'tempSensorFilter' are always included (default: none).
    tempTooltipSensorFilter :: ThermalSensor -> Bool,
    -- | How to aggregate multiple sensor readings (default: maximum)
    tempAggregation :: [TemperatureInfo] -> Maybe Double,
    -- | Nerd font icon character (default U+F2C9, nf-fa-thermometer).
    temperatureIcon :: T.Text
  }

instance Default TemperatureConfig where
  def = defaultTemperatureConfig

-- | Default configuration for the temperature widget
defaultTemperatureConfig :: TemperatureConfig
defaultTemperatureConfig =
  TemperatureConfig
    { tempFormat = "$temp$\176C", -- degree symbol
      tempUnit = Celsius,
      tempWarningThreshold = 70,
      tempCriticalThreshold = 85,
      tempPollInterval = 30,
      tempSensorFilter = const True,
      tempTooltipSensorFilter = const False,
      tempAggregation = \temps ->
        if null temps
          then Nothing
          else Just $ maximum $ map tempCelsius temps,
      temperatureIcon = T.pack "\xF2C9"
    }

-- | Create a combined icon+label temperature widget with default configuration.
temperatureNew :: (MonadIO m) => m Gtk.Widget
temperatureNew = temperatureNewWith defaultTemperatureConfig

-- | Create a combined icon+label temperature widget.
temperatureNewWith :: (MonadIO m) => TemperatureConfig -> m Gtk.Widget
temperatureNewWith config = liftIO $ do
  iconWidget <- temperatureIconNewWith config
  labelWidget <- temperatureLabelNewWith config
  buildIconLabelBox iconWidget labelWidget
    >>= (`widgetSetClassGI` "temperature")

-- | Create a combined icon+label widget backed by the shared channel.
temperatureNewChan :: TaffyIO Gtk.Widget
temperatureNewChan = temperatureNewChanWith defaultTemperatureConfig

-- | Create a combined icon+label widget backed by the shared channel.
temperatureNewChanWith :: TemperatureConfig -> TaffyIO Gtk.Widget
temperatureNewChanWith config = do
  iconWidget <- liftIO $ temperatureIconNewWith config
  labelWidget <- temperatureLabelNewChanWith config
  liftIO $
    buildIconLabelBox iconWidget labelWidget
      >>= (`widgetSetClassGI` "temperature")

-- | Create a temperature icon widget with default configuration.
temperatureIconNew :: (MonadIO m) => m Gtk.Widget
temperatureIconNew = temperatureIconNewWith defaultTemperatureConfig

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

-- | Create a temperature label widget with default configuration.
temperatureLabelNew :: (MonadIO m) => m Gtk.Widget
temperatureLabelNew = temperatureLabelNewWith defaultTemperatureConfig

-- | Create a polling temperature label with custom configuration.
temperatureLabelNewWith :: (MonadIO m) => TemperatureConfig -> m Gtk.Widget
temperatureLabelNewWith config = liftIO $ do
  widget <-
    pollingLabelNewWithTooltip (tempPollInterval config) $
      formatTemperatureInfo config <$> readAllTemperatures
  widgetSetClassGI widget "temperature-label"

-- | Create a channel-driven temperature label with the default configuration.
temperatureLabelNewChan :: TaffyIO Gtk.Widget
temperatureLabelNewChan = temperatureLabelNewChanWith defaultTemperatureConfig

-- | Create a channel-driven temperature label with custom configuration.
-- Sensor discovery and polling are shared by every channel-driven widget.
temperatureLabelNewChanWith :: TemperatureConfig -> TaffyIO Gtk.Widget
temperatureLabelNewChanWith config = do
  chan <- getTemperatureInfoChan $ tempPollInterval config
  initialInfo <- getTemperatureInfoState $ tempPollInterval config

  liftIO $ do
    label <- Gtk.labelNew Nothing
    _ <- widgetSetClassGI label "temperature-label"
    renderedRef <- newIORef Nothing

    let updateLabel info = do
          let rendered@(labelText, tooltipText) = formatTemperatureInfo config info
          previous <- readIORef renderedRef
          when (previous /= Just rendered) $ do
            writeIORef renderedRef (Just rendered)
            postGUIASync $ do
              Gtk.labelSetText label labelText
              Gtk.widgetSetTooltipText label tooltipText

    void $ Gtk.onWidgetRealize label $ updateLabel initialInfo
    Gtk.widgetShowAll label
    Gtk.toWidget =<< channelWidgetNew label chan updateLabel

formatTemperatureInfo :: TemperatureConfig -> [TemperatureInfo] -> (T.Text, Maybe T.Text)
formatTemperatureInfo config allTemperatures =
  case tempAggregation config temperatures of
    Nothing -> ("N/A", tooltipText)
    Just tempC ->
      let tempF = convertTemperature Fahrenheit tempC
          tempK = convertTemperature Kelvin tempC
          tempDisplay = convertTemperature (tempUnit config) tempC
       in (formatTemperature config tempDisplay tempC tempF tempK, tooltipText)
  where
    includedInLabel = tempSensorFilter config . tempSensor
    includedInTooltip info =
      includedInLabel info || tempTooltipSensorFilter config (tempSensor info)
    temperatures = filter includedInLabel allTemperatures
    tooltipTemperatures = filter includedInTooltip allTemperatures
    tooltipText = formatTooltip tooltipTemperatures

-- | Format the temperature label using the template
formatTemperature :: TemperatureConfig -> Double -> Double -> Double -> Double -> T.Text
formatTemperature config tempDisplay tempC tempF tempK =
  let template = ST.newSTMP (tempFormat config)
      template' =
        ST.setManyAttrib
          [ ("temp", formatDouble tempDisplay),
            ("tempC", formatDouble tempC),
            ("tempF", formatDouble tempF),
            ("tempK", formatDouble tempK)
          ]
          template
   in T.pack $ ST.render template'
  where
    formatDouble :: Double -> String
    formatDouble d = show (round d :: Int)

-- | Format tooltip showing all selected sensor readings.
formatTooltip :: [TemperatureInfo] -> Maybe T.Text
formatTooltip [] = Nothing
formatTooltip temps =
  Just $ T.pack $ intercalate "\n" $ map formatSensor temps
  where
    formatSensor info =
      sensorName (tempSensor info)
        ++ ": "
        ++ show (round (tempCelsius info) :: Int)
        ++ "\176C"