taffybar-4.1.2: src/System/Taffybar/Widget/PowerProfiles.hs
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-- |
-- Module : System.Taffybar.Widget.PowerProfiles
-- Copyright : (c) Ivan A. Malison
-- License : BSD3-style (see LICENSE)
--
-- Maintainer : Ivan A. Malison
-- Stability : unstable
-- Portability : unportable
--
-- This module provides widgets for displaying and controlling the current
-- power profile using the power-profiles-daemon DBus service.
--
-- Three widget variants are provided:
--
-- * 'powerProfilesIconNew' -- a dynamic nerd-font text icon
-- * 'powerProfilesLabelNew' -- a text label showing the profile name
-- * 'powerProfilesNew' -- combined icon + label
--
-- All variants cycle through profiles on left-click.
-----------------------------------------------------------------------------
module System.Taffybar.Widget.PowerProfiles
( PowerProfilesConfig(..)
, defaultPowerProfilesConfig
, powerProfilesIconNew
, powerProfilesIconNewWithConfig
, powerProfilesLabelNew
, powerProfilesLabelNewWithConfig
, powerProfilesNew
, powerProfilesNewWithConfig
) where
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
import Data.Default (Default(..))
import qualified Data.Text as T
import qualified GI.Gdk as Gdk
import qualified GI.Gtk as Gtk
import System.Log.Logger
import System.Taffybar.Context
import System.Taffybar.Information.PowerProfiles
import System.Taffybar.Util (postGUIASync, logPrintF)
import System.Taffybar.Widget.Generic.ChannelWidget
import System.Taffybar.Widget.Util (buildIconLabelBox)
-- | Configuration for the power profiles widget.
data PowerProfilesConfig = PowerProfilesConfig
{ -- | Format string for the label. Use @$profile$@ for the profile name.
powerProfilesFormat :: String
-- | Nerd font text icon for power-saver mode (default U+F06C9, nf-md-leaf).
, powerProfilesPowerSaverTextIcon :: T.Text
-- | Nerd font text icon for balanced mode (default U+F0A7A, nf-md-scale_balance).
, powerProfilesBalancedTextIcon :: T.Text
-- | Nerd font text icon for performance mode (default U+F0E4E, nf-md-rocket_launch).
, powerProfilesPerformanceTextIcon :: T.Text
} deriving (Eq, Show)
-- | Default configuration for the power profiles widget.
defaultPowerProfilesConfig :: PowerProfilesConfig
defaultPowerProfilesConfig = PowerProfilesConfig
{ powerProfilesFormat = "$profile$"
, powerProfilesPowerSaverTextIcon = T.pack "\xF06C9"
, powerProfilesBalancedTextIcon = T.pack "\xF0A7A"
, powerProfilesPerformanceTextIcon = T.pack "\xF0E4E"
}
instance Default PowerProfilesConfig where
def = defaultPowerProfilesConfig
powerProfilesLogPath :: String
powerProfilesLogPath = "System.Taffybar.Widget.PowerProfiles"
powerProfilesLogF :: (MonadIO m, Show t) => Priority -> String -> t -> m ()
powerProfilesLogF = logPrintF powerProfilesLogPath
-- ---------------------------------------------------------------------------
-- Icon-only widget (dynamic nerd font text icon)
-- | Create a power profiles icon widget with default configuration.
powerProfilesIconNew :: TaffyIO Gtk.Widget
powerProfilesIconNew = powerProfilesIconNewWithConfig defaultPowerProfilesConfig
-- | Create a power profiles icon widget with custom configuration.
--
-- The icon is a nerd-font text label that updates dynamically when the
-- active profile changes. CSS classes @power-saver@, @balanced@, and
-- @performance@ are toggled on the label.
powerProfilesIconNewWithConfig :: PowerProfilesConfig -> TaffyIO Gtk.Widget
powerProfilesIconNewWithConfig config = do
chan <- getPowerProfileInfoChan
ctx <- ask
liftIO $ do
label <- Gtk.labelNew Nothing
let updateIcon info = postGUIASync $ do
let iconText = getTextIcon config (currentProfile info)
Gtk.labelSetText label iconText
updateProfileClasses label info
void $ Gtk.onWidgetRealize label $ do
initialInfo <- runReaderT getPowerProfileInfoState ctx
updateIcon initialInfo
Gtk.widgetShowAll label
Gtk.toWidget =<< channelWidgetNew label chan updateIcon
-- ---------------------------------------------------------------------------
-- Label-only widget (text label with format string)
-- | Create a power profiles label widget with default configuration.
powerProfilesLabelNew :: TaffyIO Gtk.Widget
powerProfilesLabelNew = powerProfilesLabelNewWithConfig defaultPowerProfilesConfig
-- | Create a power profiles label widget with custom configuration.
--
-- The label displays the profile name formatted via 'powerProfilesFormat'.
-- CSS classes @power-saver@, @balanced@, and @performance@ are toggled on
-- the label.
powerProfilesLabelNewWithConfig :: PowerProfilesConfig -> TaffyIO Gtk.Widget
powerProfilesLabelNewWithConfig config = do
chan <- getPowerProfileInfoChan
ctx <- ask
liftIO $ do
label <- Gtk.labelNew Nothing
let updateLabel info = postGUIASync $ do
let profileName = T.unpack $ powerProfileToString (currentProfile info)
labelText = formatLabel (powerProfilesFormat config) profileName
Gtk.labelSetMarkup label (T.pack labelText)
updateProfileClasses label info
void $ Gtk.onWidgetRealize label $ do
initialInfo <- runReaderT getPowerProfileInfoState ctx
updateLabel initialInfo
Gtk.widgetShowAll label
Gtk.toWidget =<< channelWidgetNew label chan updateLabel
-- ---------------------------------------------------------------------------
-- Combined icon + label widget
-- | Create a combined icon + label power profiles widget with default
-- configuration.
powerProfilesNew :: TaffyIO Gtk.Widget
powerProfilesNew = powerProfilesNewWithConfig defaultPowerProfilesConfig
-- | Create a combined icon + label power profiles widget.
--
-- Wraps the icon and label in a horizontal box (via 'buildIconLabelBox')
-- inside an event box with click-to-cycle and tooltip. CSS classes
-- @power-profiles@, @power-saver@, @balanced@, and @performance@ are
-- applied to the event box.
powerProfilesNewWithConfig :: PowerProfilesConfig -> TaffyIO Gtk.Widget
powerProfilesNewWithConfig config = do
chan <- getPowerProfileInfoChan
ctx <- ask
iconWidget <- powerProfilesIconNewWithConfig config
labelWidget <- powerProfilesLabelNewWithConfig config
liftIO $ do
box <- buildIconLabelBox iconWidget labelWidget
ebox <- Gtk.eventBoxNew
Gtk.containerAdd ebox box
styleCtx <- Gtk.widgetGetStyleContext ebox
Gtk.styleContextAddClass styleCtx "power-profiles"
let updateWidget info = postGUIASync $ do
updateProfileClasses ebox info
updateTooltip ebox info
void $ Gtk.onWidgetRealize ebox $ do
initialInfo <- runReaderT getPowerProfileInfoState ctx
updateWidget initialInfo
setupClickHandler ctx ebox
Gtk.widgetShowAll ebox
Gtk.toWidget =<< channelWidgetNew ebox chan updateWidget
-- ---------------------------------------------------------------------------
-- Helpers
-- | Select the nerd font text icon for a given profile.
getTextIcon :: PowerProfilesConfig -> PowerProfile -> T.Text
getTextIcon config PowerSaver = powerProfilesPowerSaverTextIcon config
getTextIcon config Balanced = powerProfilesBalancedTextIcon config
getTextIcon config Performance = powerProfilesPerformanceTextIcon config
-- | Format the label string, replacing @$profile$@ with the profile name.
formatLabel :: String -> String -> String
formatLabel fmt profile = replace "$profile$" profile fmt
where
replace :: String -> String -> String -> String
replace old new = T.unpack . T.replace (T.pack old) (T.pack new) . T.pack
-- | Update CSS classes based on the current profile. Works with any widget.
updateProfileClasses :: Gtk.IsWidget w => w -> PowerProfileInfo -> IO ()
updateProfileClasses widget info = do
let profile = currentProfile info
allClasses = ["power-saver", "balanced", "performance"]
currentClass = case profile of
PowerSaver -> "power-saver"
Balanced -> "balanced"
Performance -> "performance"
styleCtx <- Gtk.widgetGetStyleContext widget
mapM_ (Gtk.styleContextRemoveClass styleCtx) allClasses
Gtk.styleContextAddClass styleCtx currentClass
-- | Update tooltip with current profile info.
updateTooltip :: Gtk.IsWidget w => w -> PowerProfileInfo -> IO ()
updateTooltip widget info = do
let profile = currentProfile info
profileName = powerProfileToString profile
degradedText = case performanceDegraded info of
Nothing -> ""
Just reason -> "\nPerformance degraded: " <> reason
tooltipText = "Power Profile: " <> profileName <> degradedText
Gtk.widgetSetTooltipText widget (Just tooltipText)
-- | Set up click handler to cycle profiles on left-click.
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 && button == 1
then do
info <- runReaderT getPowerProfileInfoState ctx
let client = systemDBusClient ctx
result <- cycleProfile client info
case result of
Left err ->
powerProfilesLogF WARNING "Failed to cycle power profile: %s" (show err)
Right () ->
return ()
return True
else return False