taffybar-5.1.0: src/System/Taffybar/Information/Wlsunset.hs
-----------------------------------------------------------------------------
-- |
-- Module : System.Taffybar.Information.Wlsunset
-- Copyright : (c) Ivan A. Malison
-- License : BSD3-style (see LICENSE)
--
-- Maintainer : Ivan A. Malison
-- Stability : unstable
-- Portability : unportable
--
-- This module provides process-level management of @wlsunset@, a
-- Wayland day\/night gamma adjustor. It polls for the running state of
-- the process and tracks mode cycling (auto → forced-warm → forced-cool
-- → auto) via @SIGUSR1@.
-----------------------------------------------------------------------------
module System.Taffybar.Information.Wlsunset
( -- * Types
WlsunsetMode(..)
, WlsunsetState(..)
, WlsunsetConfig(..)
-- * State access
, getWlsunsetChan
, getWlsunsetState
-- * Actions
, cycleWlsunsetMode
, startWlsunset
, stopWlsunset
, toggleWlsunset
) where
import Control.Concurrent (threadDelay)
import Control.Concurrent.MVar
import Control.Concurrent.STM.TChan
import Control.Exception.Enclosed (catchAny)
import Control.Monad (void, when)
import Control.Monad.IO.Class
import Control.Monad.STM (atomically)
import Control.Monad.Trans.Class
import Data.Default (Default(..))
import System.Log.Logger
import System.Posix.Signals (signalProcess, sigUSR1)
import System.Posix.Types (CPid(..))
import System.Process (readProcess, spawnCommand)
import System.Taffybar.Context
import System.Taffybar.Util (logPrintF)
import Text.Read (readMaybe)
-- | The three operating modes that wlsunset cycles through when it
-- receives @SIGUSR1@.
data WlsunsetMode
= WlsunsetAuto -- ^ Normal day/night schedule
| WlsunsetForcedWarm -- ^ Forced warm (night) temperature
| WlsunsetForcedCool -- ^ Forced cool (day) temperature
deriving (Eq, Show, Ord, Enum, Bounded)
-- | Observable state of the wlsunset process.
data WlsunsetState = WlsunsetState
{ wlsunsetRunning :: Bool
, wlsunsetMode :: WlsunsetMode
} deriving (Eq, Show)
-- | Configuration for the wlsunset monitor.
data WlsunsetConfig = WlsunsetConfig
{ -- | Full shell command used to start wlsunset (e.g.
-- @\"wlsunset -l 38.9 -L -77.0\"@).
wlsunsetCommand :: String
-- | How often (in seconds) to poll for process status.
, wlsunsetPollIntervalSec :: Int
} deriving (Eq, Show)
instance Default WlsunsetConfig where
def = WlsunsetConfig
{ wlsunsetCommand = "wlsunset"
, wlsunsetPollIntervalSec = 2
}
-- | Internal state bundle stored in 'contextState' via 'getStateDefault'.
newtype WlsunsetChanVar =
WlsunsetChanVar (TChan WlsunsetState, MVar WlsunsetState, WlsunsetConfig)
wlsunsetLogPath :: String
wlsunsetLogPath = "System.Taffybar.Information.Wlsunset"
wlsunsetLog :: MonadIO m => Priority -> String -> m ()
wlsunsetLog priority = liftIO . logM wlsunsetLogPath priority
wlsunsetLogF :: (MonadIO m, Show t) => Priority -> String -> t -> m ()
wlsunsetLogF = logPrintF wlsunsetLogPath
-- ---------------------------------------------------------------------------
-- Process helpers
-- ---------------------------------------------------------------------------
-- | Check whether wlsunset is running by calling @pgrep -x wlsunset@.
-- Returns a list of matching PIDs (empty when not running).
pgrepWlsunset :: IO [CPid]
pgrepWlsunset =
(parsePids <$> readProcess "pgrep" ["-x", "wlsunset"] "")
`catchAny` (\_ -> return [])
where
parsePids = map (CPid . fromIntegral) . concatMap toList . lines
toList s = case (readMaybe s :: Maybe Int) of
Just n -> [n]
Nothing -> []
-- | Send @SIGUSR1@ to a wlsunset process to cycle its mode.
sendUSR1 :: CPid -> IO ()
sendUSR1 = signalProcess sigUSR1
-- ---------------------------------------------------------------------------
-- State management
-- ---------------------------------------------------------------------------
getWlsunsetChanVar :: WlsunsetConfig -> TaffyIO WlsunsetChanVar
getWlsunsetChanVar cfg =
getStateDefault $ WlsunsetChanVar <$> monitorWlsunset cfg
-- | Get a broadcast channel that receives 'WlsunsetState' updates.
getWlsunsetChan :: WlsunsetConfig -> TaffyIO (TChan WlsunsetState)
getWlsunsetChan cfg = do
WlsunsetChanVar (chan, _, _) <- getWlsunsetChanVar cfg
return chan
-- | Get the current 'WlsunsetState'.
getWlsunsetState :: WlsunsetConfig -> TaffyIO WlsunsetState
getWlsunsetState cfg = do
WlsunsetChanVar (_, var, _) <- getWlsunsetChanVar cfg
lift $ readMVar var
-- | Start the polling loop that monitors wlsunset.
monitorWlsunset
:: WlsunsetConfig
-> TaffyIO (TChan WlsunsetState, MVar WlsunsetState, WlsunsetConfig)
monitorWlsunset cfg = do
let initialState = WlsunsetState { wlsunsetRunning = False
, wlsunsetMode = WlsunsetAuto
}
stateVar <- liftIO $ newMVar initialState
chan <- liftIO newBroadcastTChanIO
taffyFork $ do
wlsunsetLog DEBUG "Starting wlsunset polling loop"
let loop = do
liftIO $ pollWlsunset chan stateVar
liftIO $ threadDelay (wlsunsetPollIntervalSec cfg * 1000000)
loop
loop
return (chan, stateVar, cfg)
-- | A single poll iteration: check process status, update state, and
-- broadcast if changed.
pollWlsunset :: TChan WlsunsetState -> MVar WlsunsetState -> IO ()
pollWlsunset chan var = do
pids <- pgrepWlsunset
let isRunning = not (null pids)
modifyMVar_ var $ \old -> do
let wasRunning = wlsunsetRunning old
-- When the process freshly appears, reset mode to Auto.
newMode
| not wasRunning && isRunning = WlsunsetAuto
| not isRunning = WlsunsetAuto
| otherwise = wlsunsetMode old
new = WlsunsetState { wlsunsetRunning = isRunning
, wlsunsetMode = newMode
}
when (new /= old) $ do
wlsunsetLogF DEBUG "Wlsunset state changed: %s" new
atomically $ writeTChan chan new
return new
-- ---------------------------------------------------------------------------
-- Actions
-- ---------------------------------------------------------------------------
-- | Cycle wlsunset mode by sending @SIGUSR1@ to the process.
-- The mode cycles: Auto → ForcedWarm → ForcedCool → Auto.
cycleWlsunsetMode :: WlsunsetConfig -> TaffyIO ()
cycleWlsunsetMode cfg = do
WlsunsetChanVar (chan, var, _) <- getWlsunsetChanVar cfg
liftIO $ do
pids <- pgrepWlsunset
case pids of
[] -> wlsunsetLog DEBUG "cycleWlsunsetMode: wlsunset not running"
_ -> do
mapM_ sendUSR1 pids
modifyMVar_ var $ \old -> do
let newMode = case wlsunsetMode old of
WlsunsetAuto -> WlsunsetForcedWarm
WlsunsetForcedWarm -> WlsunsetForcedCool
WlsunsetForcedCool -> WlsunsetAuto
new = old { wlsunsetMode = newMode }
wlsunsetLogF DEBUG "Cycled wlsunset mode: %s" newMode
atomically $ writeTChan chan new
return new
-- | Start the wlsunset process using the configured command.
startWlsunset :: WlsunsetConfig -> TaffyIO ()
startWlsunset cfg = liftIO $ do
wlsunsetLog DEBUG $ "Starting wlsunset: " ++ wlsunsetCommand cfg
void $ spawnCommand (wlsunsetCommand cfg)
-- | Stop wlsunset by sending @SIGTERM@ (signal 15) to all instances.
stopWlsunset :: WlsunsetConfig -> TaffyIO ()
stopWlsunset _cfg = liftIO $ do
pids <- pgrepWlsunset
case pids of
[] -> wlsunsetLog DEBUG "stopWlsunset: wlsunset not running"
_ -> do
wlsunsetLog DEBUG "Stopping wlsunset (SIGTERM)"
mapM_ (signalProcess 15) pids
-- | Toggle wlsunset: stop it if running, start it if not.
toggleWlsunset :: WlsunsetConfig -> TaffyIO ()
toggleWlsunset cfg = do
st <- getWlsunsetState cfg
if wlsunsetRunning st
then stopWlsunset cfg
else startWlsunset cfg