packages feed

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

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

-- |
-- Module      : System.Taffybar.Widget.NvidiaTemperature
-- Copyright   : (c) Ivan Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan Malison <IvanMalison@gmail.com>
-- Stability   : unstable
-- Portability : unportable
--
-- NVIDIA GPU temperature widgets backed by @nvidia-smi@.
module System.Taffybar.Widget.NvidiaTemperature
  ( -- * Combined icon+label widget
    nvidiaTemperatureNew,
    nvidiaTemperatureNewWith,
    nvidiaTemperatureNewChan,
    nvidiaTemperatureNewChanWith,

    -- * Icon-only widget
    nvidiaTemperatureIconNew,
    nvidiaTemperatureIconNewWith,

    -- * Label-only widget
    nvidiaTemperatureLabelNew,
    nvidiaTemperatureLabelNewWith,
    nvidiaTemperatureLabelNewChan,
    nvidiaTemperatureLabelNewChanWith,

    -- * Configuration
    NvidiaTemperatureConfig (..),
    defaultNvidiaTemperatureConfig,
  )
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 (find, intercalate, maximumBy)
import Data.Maybe (catMaybes, mapMaybe)
import Data.Ord (comparing)
import qualified Data.Text as T
import qualified GI.Gtk as Gtk
import System.Taffybar.Context (TaffyIO)
import System.Taffybar.Information.Nvidia
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 Text.Printf (printf)
import qualified Text.StringTemplate as ST

-- | Configuration for the NVIDIA temperature widget.
data NvidiaTemperatureConfig = NvidiaTemperatureConfig
  { -- | Path or command name for @nvidia-smi@.
    nvidiaTemperatureCommand :: FilePath,
    -- | GPU index to display. 'Nothing' displays the hottest available GPU.
    nvidiaTemperatureGpuIndex :: Maybe Int,
    -- | Label template. Available variables are @gpu@, @name@, and @tempC@.
    nvidiaTemperatureFormat :: String,
    -- | Text displayed when no temperature is available.
    nvidiaTemperatureFallback :: T.Text,
    -- | Polling period in seconds.
    nvidiaTemperaturePollInterval :: Double,
    -- | Icon used by the combined icon and label widget.
    nvidiaTemperatureIcon :: T.Text
  }

instance Default NvidiaTemperatureConfig where
  def = defaultNvidiaTemperatureConfig

-- | Default to GPU 0, refreshed every thirty seconds.
defaultNvidiaTemperatureConfig :: NvidiaTemperatureConfig
defaultNvidiaTemperatureConfig =
  NvidiaTemperatureConfig
    { nvidiaTemperatureCommand = "nvidia-smi",
      nvidiaTemperatureGpuIndex = Just 0,
      nvidiaTemperatureFormat = "GPU $tempC$\176C",
      nvidiaTemperatureFallback = "GPU N/A",
      nvidiaTemperaturePollInterval = 30,
      nvidiaTemperatureIcon = "\xF2C9"
    }

-- | Create a combined icon and label widget with the default configuration.
nvidiaTemperatureNew :: (MonadIO m) => m Gtk.Widget
nvidiaTemperatureNew = nvidiaTemperatureNewWith defaultNvidiaTemperatureConfig

-- | Create a combined icon and label widget.
nvidiaTemperatureNewWith :: (MonadIO m) => NvidiaTemperatureConfig -> m Gtk.Widget
nvidiaTemperatureNewWith config = liftIO $ do
  iconWidget <- nvidiaTemperatureIconNewWith config
  labelWidget <- nvidiaTemperatureLabelNewWith config
  buildIconLabelBox iconWidget labelWidget
    >>= (`widgetSetClassGI` "nvidia-temperature")

-- | Create a combined icon and label widget backed by the shared channel.
nvidiaTemperatureNewChan :: TaffyIO Gtk.Widget
nvidiaTemperatureNewChan = nvidiaTemperatureNewChanWith defaultNvidiaTemperatureConfig

-- | Create a combined icon and label widget backed by the shared channel.
nvidiaTemperatureNewChanWith :: NvidiaTemperatureConfig -> TaffyIO Gtk.Widget
nvidiaTemperatureNewChanWith config = do
  iconWidget <- liftIO $ nvidiaTemperatureIconNewWith config
  labelWidget <- nvidiaTemperatureLabelNewChanWith config
  liftIO $
    buildIconLabelBox iconWidget labelWidget
      >>= (`widgetSetClassGI` "nvidia-temperature")

-- | Create an icon widget with the default configuration.
nvidiaTemperatureIconNew :: (MonadIO m) => m Gtk.Widget
nvidiaTemperatureIconNew = nvidiaTemperatureIconNewWith defaultNvidiaTemperatureConfig

-- | Create an icon widget with the provided configuration.
nvidiaTemperatureIconNewWith :: (MonadIO m) => NvidiaTemperatureConfig -> m Gtk.Widget
nvidiaTemperatureIconNewWith config = liftIO $ do
  label <- Gtk.labelNew $ Just $ nvidiaTemperatureIcon config
  _ <- widgetSetClassGI label "nvidia-temperature-icon"
  Gtk.widgetShowAll label
  Gtk.toWidget label

-- | Create a label widget with the default configuration.
nvidiaTemperatureLabelNew :: (MonadIO m) => m Gtk.Widget
nvidiaTemperatureLabelNew = nvidiaTemperatureLabelNewWith defaultNvidiaTemperatureConfig

-- | Create a label widget that polls for rich NVIDIA information.
nvidiaTemperatureLabelNewWith :: (MonadIO m) => NvidiaTemperatureConfig -> m Gtk.Widget
nvidiaTemperatureLabelNewWith config = liftIO $ do
  widget <-
    pollingLabelNewWithTooltip (nvidiaTemperaturePollInterval config) $
      formatWidget config <$> readNvidiaGpuInfoWith (nvidiaTemperatureCommand config)
  widgetSetClassGI widget "nvidia-temperature-label"

-- | Create a label driven by the shared NVIDIA information channel.
nvidiaTemperatureLabelNewChan :: TaffyIO Gtk.Widget
nvidiaTemperatureLabelNewChan = nvidiaTemperatureLabelNewChanWith defaultNvidiaTemperatureConfig

-- | Create a label driven by the shared NVIDIA information channel.
nvidiaTemperatureLabelNewChanWith :: NvidiaTemperatureConfig -> TaffyIO Gtk.Widget
nvidiaTemperatureLabelNewChanWith config = do
  let command = nvidiaTemperatureCommand config
      interval = nvidiaTemperaturePollInterval config
  chan <- getNvidiaGpuInfoChanWith command interval
  initialInfo <- getNvidiaGpuInfoStateWith command interval

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

    let updateLabel info = do
          let rendered@(labelText, tooltipText) = formatWidget 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

formatWidget :: NvidiaTemperatureConfig -> [NvidiaGpuInfo] -> (T.Text, Maybe T.Text)
formatWidget config info =
  ( maybe (nvidiaTemperatureFallback config) (uncurry $ formatLabel config) $
      selectTemperature config info,
    formatTooltip info
  )

selectTemperature :: NvidiaTemperatureConfig -> [NvidiaGpuInfo] -> Maybe (NvidiaGpuInfo, Double)
selectTemperature config info =
  case nvidiaTemperatureGpuIndex config of
    Just index -> do
      gpu <- find ((== index) . nvidiaInfoIndex) info
      temperature <- nvidiaInfoTemperatureCelsius gpu
      pure (gpu, temperature)
    Nothing ->
      case mapMaybe withTemperature info of
        [] -> Nothing
        temperatures -> Just $ maximumBy (comparing snd) temperatures
  where
    withTemperature gpu = (gpu,) <$> nvidiaInfoTemperatureCelsius gpu

formatLabel :: NvidiaTemperatureConfig -> NvidiaGpuInfo -> Double -> T.Text
formatLabel config info temperature =
  T.pack $ ST.render template
  where
    template =
      ST.setManyAttrib
        [ ("gpu", show $ nvidiaInfoIndex info),
          ("name", T.unpack $ nvidiaInfoName info),
          ("tempC", show (round temperature :: Int))
        ]
        $ ST.newSTMP
        $ nvidiaTemperatureFormat config

formatTooltip :: [NvidiaGpuInfo] -> Maybe T.Text
formatTooltip [] = Nothing
formatTooltip info =
  Just $ T.pack $ intercalate "\n\n" $ map formatGpu info

formatGpu :: NvidiaGpuInfo -> String
formatGpu info = intercalate "\n" $ header : catMaybes detailLines
  where
    header =
      T.unpack (nvidiaInfoName info)
        ++ " (GPU "
        ++ show (nvidiaInfoIndex info)
        ++ ")"
    detailLines =
      [ temperatureLine info,
        measurementLine "Memory temperature" "\176C" $ nvidiaInfoMemoryTemperatureCelsius info,
        utilizationLine info,
        memoryLine info,
        powerLine info,
        measurementLine "Fan" "%" $ nvidiaInfoFanSpeedPercent info,
        ("Performance state: " ++) . T.unpack <$> nvidiaInfoPerformanceState info
      ]

temperatureLine :: NvidiaGpuInfo -> Maybe String
temperatureLine info = do
  temperature <- nvidiaInfoTemperatureCelsius info
  let details =
        catMaybes
          [ ("target " ++) . formatMeasurement "\176C" <$> nvidiaInfoTargetTemperatureCelsius info,
            ("headroom " ++) . formatMeasurement "\176C" <$> nvidiaInfoThermalHeadroomCelsius info
          ]
      suffix = if null details then "" else " (" ++ intercalate ", " details ++ ")"
  pure $ "Temperature: " ++ formatMeasurement "\176C" temperature ++ suffix

utilizationLine :: NvidiaGpuInfo -> Maybe String
utilizationLine info =
  prefixedValues
    "Utilization: "
    [ ("GPU " ++) . formatMeasurement "%" <$> nvidiaInfoGpuUtilizationPercent info,
      ("memory " ++) . formatMeasurement "%" <$> nvidiaInfoMemoryUtilizationPercent info
    ]

memoryLine :: NvidiaGpuInfo -> Maybe String
memoryLine info =
  case (nvidiaInfoMemoryUsedMiB info, nvidiaInfoMemoryTotalMiB info) of
    (Just used, Just total) ->
      Just $ "VRAM: " ++ formatMeasurement " MiB" used ++ " / " ++ formatMeasurement " MiB" total
    (Just used, Nothing) -> measurementLine "VRAM" " MiB" $ Just used
    _ -> Nothing

powerLine :: NvidiaGpuInfo -> Maybe String
powerLine info =
  case (nvidiaInfoPowerDrawWatts info, nvidiaInfoPowerLimitWatts info) of
    (Just draw, Just limit) ->
      Just $ "Power: " ++ formatMeasurement " W" draw ++ " / " ++ formatMeasurement " W" limit
    (Just draw, Nothing) -> measurementLine "Power" " W" $ Just draw
    _ -> Nothing

measurementLine :: String -> String -> Maybe Double -> Maybe String
measurementLine name unit = fmap $ ((name ++ ": ") ++) . formatMeasurement unit

prefixedValues :: String -> [Maybe String] -> Maybe String
prefixedValues prefix values =
  case catMaybes values of
    [] -> Nothing
    present -> Just $ prefix ++ intercalate ", " present

formatMeasurement :: String -> Double -> String
formatMeasurement unit value = formatNumber value ++ unit

formatNumber :: Double -> String
formatNumber value
  | abs (value - fromIntegral (round value :: Int)) < 0.05 = show (round value :: Int)
  | otherwise = printf "%.1f" value