packages feed

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

{-# LANGUAGE OverloadedStrings #-}

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

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

-- |
-- Module      : System.Taffybar.Widget.ASUS
-- Copyright   : (c) Ivan A. Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan A. Malison
-- Stability   : unstable
-- Portability : unportable
--
-- This module provides a widget for displaying and controlling the current
-- ASUS platform profile along with CPU frequency and temperature.
--
-- Displays: @\<icon\> \<freq\> \<temp\>@, e.g. @nf-icon 3.2GHz 72°C@.
-- Left-click opens one menu with separate AC and battery profile sections;
-- right-click cycles the live profile.
module System.Taffybar.Widget.ASUS
  ( ASUSWidgetConfig (..),
    defaultASUSWidgetConfig,
    asusWidgetNew,
    asusWidgetNewWithConfig,
  )
where

import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
import DBus (MethodError)
import DBus.Client (Client)
import Data.Default (Default (..))
import Data.IORef (newIORef, readIORef, writeIORef)
import qualified Data.Text as T
import qualified GI.GLib as GLib
import qualified GI.Gdk as Gdk
import qualified GI.Gtk as Gtk
import System.Log.Logger
import System.Taffybar.Context
import System.Taffybar.Information.ASUS
import System.Taffybar.Util (logPrintF, postGUIASync)
import System.Taffybar.Widget.Generic.ChannelWidget
import Text.Printf (printf)

-- | Configuration for the ASUS widget.
data ASUSWidgetConfig = ASUSWidgetConfig
  { -- | Nerd font icon for quiet mode
    asusQuietIcon :: T.Text,
    -- | Nerd font icon for balanced mode
    asusBalancedIcon :: T.Text,
    -- | Nerd font icon for performance mode
    asusPerformanceIcon :: T.Text,
    -- | Show CPU frequency
    asusShowFreq :: Bool,
    -- | Show CPU temperature
    asusShowTemp :: Bool
  }
  deriving (Eq, Show)

-- | Default configuration.
-- Icons: nf-md-leaf (quiet), nf-md-scale_balance (balanced), nf-md-rocket_launch (performance)
defaultASUSWidgetConfig :: ASUSWidgetConfig
defaultASUSWidgetConfig =
  ASUSWidgetConfig
    { asusQuietIcon = T.pack "\xF06C9", -- nf-md-leaf
      asusBalancedIcon = T.pack "\xF0A7A", -- nf-md-scale_balance
      asusPerformanceIcon = T.pack "\xF0E4E", -- nf-md-rocket_launch
      asusShowFreq = True,
      asusShowTemp = True
    }

instance Default ASUSWidgetConfig where
  def = defaultASUSWidgetConfig

asusLogPath :: String
asusLogPath = "System.Taffybar.Widget.ASUS"

asusLogF :: (MonadIO m, Show t) => Priority -> String -> t -> m ()
asusLogF = logPrintF asusLogPath

-- | Create an ASUS widget with default configuration.
asusWidgetNew :: TaffyIO Gtk.Widget
asusWidgetNew = asusWidgetNewWithConfig defaultASUSWidgetConfig

-- | Create an ASUS widget with custom configuration.
asusWidgetNewWithConfig :: ASUSWidgetConfig -> TaffyIO Gtk.Widget
asusWidgetNewWithConfig config = do
  chan <- getASUSInfoChan
  ctx <- ask

  liftIO $ do
    label <- Gtk.labelNew Nothing
    ebox <- Gtk.eventBoxNew
    Gtk.containerAdd ebox label
    styleCtx <- Gtk.widgetGetStyleContext ebox
    Gtk.styleContextAddClass styleCtx "asus-profile"
    renderedRef <- newIORef Nothing

    let updateWidget info = do
          let icon = getTextIcon config (asusProfile info)
              freqText =
                if asusShowFreq config
                  then T.pack $ printf " %.1fGHz" (asusCpuFreqGHz info)
                  else ""
              tempText =
                if asusShowTemp config
                  then T.pack $ printf " %.0f\x00B0C" (asusCpuTempC info)
                  else ""
              labelText = icon <> freqText <> tempText
              tooltipText = asusTooltipText info
              rendered = (labelText, tooltipText, asusProfile info)
          previous <- readIORef renderedRef
          when (previous /= Just rendered) $ do
            writeIORef renderedRef (Just rendered)
            postGUIASync $ do
              when (fmap (\(oldLabel, _, _) -> oldLabel) previous /= Just labelText) $
                Gtk.labelSetText label labelText
              when (fmap (\(_, _, oldProfile) -> oldProfile) previous /= Just (asusProfile info)) $
                updateProfileClasses ebox info
              when (fmap (\(_, oldTooltip, _) -> oldTooltip) previous /= Just tooltipText) $
                Gtk.widgetSetTooltipText ebox (Just tooltipText)

    void $ Gtk.onWidgetRealize ebox $ do
      initialInfo <- runReaderT getASUSInfoState ctx
      updateWidget initialInfo

    setupClickHandler ctx ebox
    Gtk.widgetShowAll ebox
    Gtk.toWidget =<< channelWidgetNew ebox chan updateWidget

-- | Select the nerd font text icon for a given profile.
getTextIcon :: ASUSWidgetConfig -> ASUSPlatformProfile -> T.Text
getTextIcon config Quiet = asusQuietIcon config
getTextIcon config Balanced = asusBalancedIcon config
getTextIcon config Performance = asusPerformanceIcon config

-- | Update CSS classes based on the current profile.
updateProfileClasses :: (Gtk.IsWidget w) => w -> ASUSInfo -> IO ()
updateProfileClasses widget info = do
  let profile = asusProfile info
      allClasses = ["quiet", "balanced", "performance"] :: [T.Text]
      currentClass = case profile of
        Quiet -> "quiet"
        Balanced -> "balanced"
        Performance -> "performance"
  styleCtx <- Gtk.widgetGetStyleContext widget
  mapM_ (Gtk.styleContextRemoveClass styleCtx) allClasses
  Gtk.styleContextAddClass styleCtx currentClass

asusTooltipText :: ASUSInfo -> T.Text
asusTooltipText info =
  let profile = asusProfileToString (asusProfile info)
      acProfile = asusProfileToString (asusACProfile info)
      batteryProfile = asusProfileToString (asusBatteryProfile info)
      powerSource = if asusOnACPower info then "AC power" else "Battery power"
      freqStr = T.pack $ printf "%.1f GHz" (asusCpuFreqGHz info)
      tempStr = T.pack $ printf "%.0f\x00B0C" (asusCpuTempC info)
   in "Active: "
        <> profile
        <> " ("
        <> powerSource
        <> ")\nAC profile: "
        <> acProfile
        <> "\nBattery profile: "
        <> batteryProfile
        <> "\nCPU Freq: "
        <> freqStr
        <> "\nCPU Temp: "
        <> tempStr
        <> "\n\nLeft click: configure profiles"
        <> "\nRight click: cycle active profile"

-- | Set up click handler: left-click opens profile menu, right-click cycles.
setupClickHandler :: Context -> Gtk.EventBox -> IO ()
setupClickHandler ctx ebox = do
  void $ Gtk.onWidgetButtonPressEvent ebox $ \event -> do
    button <- Gdk.getEventButtonButton event
    eventType <- Gdk.getEventButtonType event
    if eventType /= Gdk.EventTypeButtonPress
      then return False
      else case button of
        1 -> do
          showProfileMenu ctx ebox
          return True
        3 -> do
          let client = systemDBusClient ctx
          result <- cycleASUSProfile client
          case result of
            Left err ->
              asusLogF WARNING "Failed to cycle ASUS profile: %s" (show err)
            Right () ->
              return ()
          return True
        _ -> return False

-- | Build and show a popup menu for configuring AC and battery profiles.
showProfileMenu :: Context -> Gtk.EventBox -> IO ()
showProfileMenu ctx ebox = do
  currentEvent <- Gtk.getCurrentEvent
  currentInfo <- runReaderT getASUSInfoState ctx

  menu <- Gtk.menuNew
  Gtk.menuAttachToWidget menu ebox Nothing

  let activeProfile = asusProfileToString $ asusProfile currentInfo
      powerSource = if asusOnACPower currentInfo then "AC" else "Battery"
      statusText = "Active: " <> activeProfile <> " (" <> powerSource <> ")"

  statusItem <- Gtk.menuItemNewWithLabel statusText
  Gtk.widgetSetSensitive statusItem False
  Gtk.menuShellAppend menu statusItem

  separator <- Gtk.separatorMenuItemNew
  Gtk.menuShellAppend menu separator

  appendProfileSection
    ctx
    menu
    "AC profile"
    (asusProfile currentInfo)
    (asusACProfile currentInfo)
    (asusOnACPower currentInfo)
    setASUSACProfile

  sectionSeparator <- Gtk.separatorMenuItemNew
  Gtk.menuShellAppend menu sectionSeparator

  appendProfileSection
    ctx
    menu
    "Battery profile"
    (asusProfile currentInfo)
    (asusBatteryProfile currentInfo)
    (not $ asusOnACPower currentInfo)
    setASUSBatteryProfile

  void $
    Gtk.onWidgetHide menu $
      void $
        GLib.idleAdd GLib.PRIORITY_LOW $ do
          Gtk.widgetDestroy menu
          return False

  Gtk.widgetShowAll menu
  Gtk.menuPopupAtPointer menu currentEvent

-- | Add a visible section for one source-specific default profile. When the
-- source is currently active, changing its default also changes the live
-- profile.
appendProfileSection ::
  Context ->
  Gtk.Menu ->
  T.Text ->
  ASUSPlatformProfile ->
  ASUSPlatformProfile ->
  Bool ->
  (Client -> ASUSPlatformProfile -> IO (Either MethodError ())) ->
  IO ()
appendProfileSection
  ctx
  menu
  labelText
  liveProfile
  selectedProfile
  sourceIsActive
  setter = do
    let activeSuffix = if sourceIsActive then " (active)" else ""
        sectionLabel =
          labelText
            <> ": "
            <> asusProfileToString selectedProfile
            <> activeSuffix
        profiles = [Quiet, Balanced, Performance]

    headerItem <- Gtk.menuItemNewWithLabel sectionLabel
    Gtk.widgetSetSensitive headerItem False
    Gtk.menuShellAppend menu headerItem

    forM_ profiles $ \profile -> do
      let prefix = if profile == selectedProfile then "  \x2713 " else "     "
          itemLabel = prefix <> asusProfileToString profile
      item <- Gtk.menuItemNewWithLabel itemLabel
      Gtk.menuShellAppend menu item
      void $ Gtk.onMenuItemActivate item $ do
        let client = systemDBusClient ctx
        savedResult <- setter client profile
        case savedResult of
          Left err ->
            asusLogF WARNING "Failed to set saved ASUS profile: %s" (show err)
          Right () -> do
            -- asusd applies any AC/battery default immediately, even if that
            -- power source is inactive. Keep the current source on its existing
            -- profile when editing the other source's default.
            let profileToApply = if sourceIsActive then profile else liveProfile
            activeResult <- setASUSProfile client profileToApply
            case activeResult of
              Left err ->
                asusLogF WARNING "Failed to apply active ASUS profile: %s" (show err)
              Right () -> return ()