taffybar-5.2.0: src/System/Taffybar/Widget/Wlsunset.hs
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- |
-- Module : System.Taffybar.Widget.Wlsunset
-- Copyright : (c) Ivan A. Malison
-- License : BSD3-style (see LICENSE)
--
-- Maintainer : Ivan A. Malison
-- Stability : unstable
-- Portability : unportable
--
-- This module provides a clickable widget for controlling @wlsunset@, a
-- Wayland day\/night gamma adjustor.
--
-- The widget displays a sun icon whose CSS class reflects the current
-- wlsunset state:
--
-- * @wlsunset-high-temp@ -- forced high (day) temperature, no shift
-- * @wlsunset-low-temp@ -- forced low (night) temperature
-- * @wlsunset-off@ -- process stopped
-- * (no extra class) -- automatic\/running normally
--
-- Left-clicking opens a popup menu for mode selection and start\/stop.
-- Right-clicking quick-toggles the process on\/off.
module System.Taffybar.Widget.Wlsunset
( wlsunsetNew,
wlsunsetNewWithConfig,
WlsunsetWidgetConfig (..),
defaultWlsunsetWidgetConfig,
)
where
import Control.Monad (forM_, replicateM_, void)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Reader (ask, runReaderT)
import Data.Default (Default (..))
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.Taffybar.Context (Context, TaffyIO)
import System.Taffybar.Information.Wlsunset
import System.Taffybar.Util (postGUIASync)
import System.Taffybar.Widget.Generic.ChannelWidget (channelWidgetNew)
-- | Widget-level configuration for the wlsunset widget. Wraps the
-- underlying 'WlsunsetConfig' from the Information layer.
data WlsunsetWidgetConfig = WlsunsetWidgetConfig
{ -- | The underlying information-layer config.
wlsunsetWidgetInfoConfig :: WlsunsetConfig,
-- | Text icon to display (default: sun icon U+F0599, nf-md-white_balance_sunny).
wlsunsetWidgetIcon :: T.Text
}
deriving (Eq, Show)
-- | Default widget configuration.
defaultWlsunsetWidgetConfig :: WlsunsetWidgetConfig
defaultWlsunsetWidgetConfig =
WlsunsetWidgetConfig
{ wlsunsetWidgetInfoConfig = def,
wlsunsetWidgetIcon = T.pack "\xF0599"
}
instance Default WlsunsetWidgetConfig where
def = defaultWlsunsetWidgetConfig
-- | All CSS classes that the widget may toggle based on state.
allStateClasses :: [T.Text]
allStateClasses = ["wlsunset-high-temp", "wlsunset-low-temp", "wlsunset-off"]
-- | Create a wlsunset widget with the default configuration.
wlsunsetNew :: TaffyIO Gtk.Widget
wlsunsetNew = wlsunsetNewWithConfig defaultWlsunsetWidgetConfig
-- | Create a wlsunset widget with a custom configuration.
--
-- The widget is a clickable event box containing a nerd-font sun icon.
-- CSS classes are updated reactively via a broadcast channel from the
-- Information layer. Left-click opens a popup menu; right-click toggles
-- the process.
wlsunsetNewWithConfig :: WlsunsetWidgetConfig -> TaffyIO Gtk.Widget
wlsunsetNewWithConfig widgetCfg = do
let infoCfg = wlsunsetWidgetInfoConfig widgetCfg
chan <- getWlsunsetChan infoCfg
ctx <- ask
liftIO $ do
label <- Gtk.labelNew (Just (wlsunsetWidgetIcon widgetCfg))
ebox <- Gtk.eventBoxNew
Gtk.containerAdd ebox label
styleCtx <- Gtk.widgetGetStyleContext ebox
Gtk.styleContextAddClass styleCtx "wlsunset"
let updateWidget st = postGUIASync $ do
updateStateClasses ebox st
updateTooltip infoCfg ebox st
void $ Gtk.onWidgetRealize ebox $ do
initialState <- runReaderT (getWlsunsetState infoCfg) ctx
updateWidget initialState
setupClickHandler ctx ebox infoCfg
Gtk.widgetShowAll ebox
Gtk.toWidget =<< channelWidgetNew ebox chan updateWidget
-- ---------------------------------------------------------------------------
-- CSS class management
-- ---------------------------------------------------------------------------
-- | Update CSS classes on a widget based on the current wlsunset state.
updateStateClasses :: (Gtk.IsWidget w) => w -> WlsunsetState -> IO ()
updateStateClasses widget st = do
styleCtx <- Gtk.widgetGetStyleContext widget
-- Remove all state classes first
mapM_ (Gtk.styleContextRemoveClass styleCtx) allStateClasses
-- Add the appropriate class
forM_ (stateToClass st) (Gtk.styleContextAddClass styleCtx)
-- | Map a 'WlsunsetState' to its CSS class, or 'Nothing' for auto/running.
stateToClass :: WlsunsetState -> Maybe T.Text
stateToClass st
| not (wlsunsetRunning st) = Just "wlsunset-off"
| otherwise = case wlsunsetMode st of
WlsunsetAuto -> Nothing
WlsunsetForcedHighTemp -> Just "wlsunset-high-temp"
WlsunsetForcedLowTemp -> Just "wlsunset-low-temp"
-- ---------------------------------------------------------------------------
-- Tooltip
-- ---------------------------------------------------------------------------
-- | Update the tooltip text to reflect the current state.
updateTooltip :: (Gtk.IsWidget w) => WlsunsetConfig -> w -> WlsunsetState -> IO ()
updateTooltip _cfg widget st = do
let highT = T.pack $ show (wlsunsetEffectiveHighTemp st) ++ "K"
lowT = T.pack $ show (wlsunsetEffectiveLowTemp st) ++ "K"
text = case (wlsunsetRunning st, wlsunsetMode st) of
(False, _) -> "wlsunset: stopped"
(True, WlsunsetAuto) -> "wlsunset: automatic (" <> lowT <> " – " <> highT <> ")"
(True, WlsunsetForcedHighTemp) -> "wlsunset: high temp (" <> highT <> ")"
(True, WlsunsetForcedLowTemp) -> "wlsunset: low temp (" <> lowT <> ")"
Gtk.widgetSetTooltipText widget (Just text)
-- ---------------------------------------------------------------------------
-- Click handling
-- ---------------------------------------------------------------------------
-- | Set up button-press handlers on the event box.
-- Left-click (button 1) opens a popup menu.
-- Right-click (button 3) quick-toggles the process.
setupClickHandler :: Context -> Gtk.EventBox -> WlsunsetConfig -> IO ()
setupClickHandler ctx ebox infoCfg =
void $ Gtk.onWidgetButtonPressEvent ebox $ \event -> do
eventType <- Gdk.getEventButtonType event
button <- Gdk.getEventButtonButton event
if eventType /= Gdk.EventTypeButtonPress
then return False
else case button of
1 -> do
showPopupMenu ctx ebox infoCfg
return True
3 -> do
runReaderT (toggleWlsunset infoCfg) ctx
return True
_ -> return False
-- ---------------------------------------------------------------------------
-- Popup menu
-- ---------------------------------------------------------------------------
-- | Temperature presets from 2500K to 6500K in 500K increments.
tempPresets :: [Int]
tempPresets = [2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500]
-- | Build and show a popup menu attached to the event box.
showPopupMenu :: Context -> Gtk.EventBox -> WlsunsetConfig -> IO ()
showPopupMenu ctx ebox infoCfg = do
currentEvent <- Gtk.getCurrentEvent
st <- runReaderT (getWlsunsetState infoCfg) ctx
menu <- Gtk.menuNew
Gtk.menuAttachToWidget menu ebox Nothing
-- Mode items (disabled when not running)
let running = wlsunsetRunning st
currentMode = wlsunsetMode st
effectiveHigh = wlsunsetEffectiveHighTemp st
effectiveLow = wlsunsetEffectiveLowTemp st
highT = T.pack $ show effectiveHigh ++ "K"
lowT = T.pack $ show effectiveLow ++ "K"
modes =
[ ("Automatic", WlsunsetAuto),
("High Temp (" <> highT <> ")", WlsunsetForcedHighTemp),
("Low Temp (" <> lowT <> ")", WlsunsetForcedLowTemp)
]
forM_ modes $ \(labelText, targetMode) -> do
let prefix =
if running && currentMode == targetMode
then "\x2713 " :: T.Text -- checkmark
else " "
item <- Gtk.menuItemNewWithLabel (prefix <> labelText)
Gtk.widgetSetSensitive item running
void $
Gtk.onMenuItemActivate item $
runReaderT (cycleToMode infoCfg currentMode targetMode) ctx
Gtk.menuShellAppend menu item
-- Separator before temperature presets
sep1 <- Gtk.separatorMenuItemNew
Gtk.menuShellAppend menu sep1
-- Temperature presets submenu
tempSubMenuItem <- Gtk.menuItemNewWithLabel ("Set Temperature" :: T.Text)
tempSubmenu <- Gtk.menuNew
Gtk.menuItemSetSubmenu tempSubMenuItem (Just tempSubmenu)
let isFixedTemp = effectiveLow == effectiveHigh
forM_ tempPresets $ \temp -> do
let label = T.pack $ show temp ++ "K"
prefix =
if running && isFixedTemp && effectiveLow == temp
then "\x2713 " :: T.Text
else " "
tempItem <- Gtk.menuItemNewWithLabel (prefix <> label)
Gtk.widgetSetSensitive tempItem running
void $
Gtk.onMenuItemActivate tempItem $
runReaderT (restartWlsunsetWithTemps infoCfg temp temp) ctx
Gtk.menuShellAppend tempSubmenu tempItem
-- "Reset to default" item in the submenu
resetSep <- Gtk.separatorMenuItemNew
Gtk.menuShellAppend tempSubmenu resetSep
let defaultLow = wlsunsetLowTemp infoCfg
defaultHigh = wlsunsetHighTemp infoCfg
resetLabel =
T.pack $
"Reset (" ++ show defaultLow ++ "K – " ++ show defaultHigh ++ "K)"
resetPrefix =
if running && effectiveLow == defaultLow && effectiveHigh == defaultHigh
then "\x2713 " :: T.Text
else " "
resetItem <- Gtk.menuItemNewWithLabel (resetPrefix <> resetLabel)
Gtk.widgetSetSensitive resetItem running
void $
Gtk.onMenuItemActivate resetItem $
runReaderT (restartWlsunsetWithTemps infoCfg defaultLow defaultHigh) ctx
Gtk.menuShellAppend tempSubmenu resetItem
Gtk.menuShellAppend menu tempSubMenuItem
-- Separator before toggle
sep2 <- Gtk.separatorMenuItemNew
Gtk.menuShellAppend menu sep2
-- Start/Stop item
let toggleLabel =
if running
then "Stop wlsunset" :: T.Text
else "Start wlsunset"
toggleItem <- Gtk.menuItemNewWithLabel toggleLabel
void $
Gtk.onMenuItemActivate toggleItem $
runReaderT (toggleWlsunset infoCfg) ctx
Gtk.menuShellAppend menu toggleItem
-- Cleanup: destroy menu after it hides
void $
Gtk.onWidgetHide menu $
void $
GLib.idleAdd GLib.PRIORITY_LOW $ do
Gtk.widgetDestroy menu
return False
Gtk.widgetShowAll menu
Gtk.menuPopupAtPointer menu currentEvent
-- ---------------------------------------------------------------------------
-- Mode cycling helper
-- ---------------------------------------------------------------------------
-- | Cycle from the current mode to a target mode. The mode ring is:
-- Auto -> ForcedHighTemp -> ForcedLowTemp -> Auto. We calculate the number of
-- SIGUSR1 signals needed and send them.
cycleToMode :: WlsunsetConfig -> WlsunsetMode -> WlsunsetMode -> TaffyIO ()
cycleToMode infoCfg currentMode targetMode = do
let cyclesNeeded = cyclesToReach currentMode targetMode
replicateM_ cyclesNeeded (cycleWlsunsetMode infoCfg)
-- | Calculate how many SIGUSR1 cycles are needed to go from one mode
-- to another in the ring Auto -> ForcedHighTemp -> ForcedLowTemp -> Auto.
cyclesToReach :: WlsunsetMode -> WlsunsetMode -> Int
cyclesToReach from to
| from == to = 0
| otherwise = (toOrd to - toOrd from) `mod` 3
where
toOrd :: WlsunsetMode -> Int
toOrd WlsunsetAuto = 0
toOrd WlsunsetForcedHighTemp = 1
toOrd WlsunsetForcedLowTemp = 2