taffybar-5.2.0: src/System/Taffybar/Widget/SimpleClock.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Text clock widget with optional popup calendar.
module System.Taffybar.Widget.SimpleClock
( textClockNew,
textClockNewWith,
defaultClockConfig,
ClockConfig (..),
ClockUpdateStrategy (..),
)
where
import Control.Monad.IO.Class
import Data.Default (Default (..))
import Data.Maybe
import qualified Data.Text as T
import Data.Time.Calendar (toGregorian)
import qualified Data.Time.Clock as Clock
import Data.Time.Format
import Data.Time.LocalTime
import qualified Data.Time.Locale.Compat as L
import qualified GI.Gdk as Gdk
import GI.Gtk
import System.Taffybar.Util (VariableDelayConfig, defaultVariableDelayConfig)
import System.Taffybar.Widget.Generic.PollingLabel
import System.Taffybar.Widget.Util
-- | This module implements a very simple text-based clock widget. The widget
-- also toggles a calendar widget when clicked. This calendar is not fancy at
-- all and has no data backend.
makeCalendar :: IO TimeZone -> IO Window
makeCalendar tzfn = do
container <- windowNew WindowTypeToplevel
cal <- calendarNew
containerAdd container cal
_ <- onWidgetShow container $ resetCalendarDate cal tzfn
-- Hide the calendar instead of destroying it
_ <- onWidgetDeleteEvent container $ \_ -> widgetHide container >> return True
return container
resetCalendarDate :: Calendar -> IO TimeZone -> IO ()
resetCalendarDate cal tzfn = do
tz <- tzfn
current <- Clock.getCurrentTime
let (y, m, d) = toGregorian $ localDay $ utcToLocalTime tz current
calendarSelectMonth cal (fromIntegral m - 1) (fromIntegral y)
calendarSelectDay cal (fromIntegral d)
toggleCalendar :: (IsWidget w) => w -> Window -> IO Bool
toggleCalendar w c = do
isVis <- widgetGetVisible c
if isVis
then widgetHide c
else do
attachPopup w "Calendar" c
displayPopup w c
return True
-- | Create the widget. I recommend passing @Nothing@ for the TimeLocale
-- parameter. The format string can include Pango markup
-- (<http://developer.gnome.org/pango/stable/PangoMarkupFormat.html>).
textClockNew ::
(MonadIO m) => Maybe L.TimeLocale -> String -> Double -> m GI.Gtk.Widget
textClockNew userLocale format interval =
textClockNewWith cfg
where
cfg =
def
{ clockTimeLocale = userLocale,
clockFormatString = format,
clockUpdateStrategy = ConstantInterval interval
}
-- | Strategy for scheduling clock updates.
data ClockUpdateStrategy
= -- | Re-render at a fixed interval in seconds.
ConstantInterval Double
| -- | Re-render near rounded time boundaries.
-- The first argument is the rounding step in seconds and the second is an
-- offset in seconds.
RoundedTargetInterval Int Double
deriving (Eq, Ord, Show)
-- | Configuration for 'textClockNewWith'.
data ClockConfig = ClockConfig
{ clockTimeZone :: Maybe TimeZone,
clockTimeLocale :: Maybe L.TimeLocale,
clockFormatString :: String,
clockUpdateStrategy :: ClockUpdateStrategy,
clockVariableDelayConfig :: VariableDelayConfig Double
}
deriving (Eq, Ord, Show)
-- | A clock configuration that defaults to the current locale
defaultClockConfig :: ClockConfig
defaultClockConfig =
ClockConfig
{ clockTimeZone = Nothing,
clockTimeLocale = Nothing,
clockFormatString = "%a %b %_d %r",
clockUpdateStrategy = RoundedTargetInterval 5 0.0,
clockVariableDelayConfig = defaultVariableDelayConfig
}
instance Default ClockConfig where
def = defaultClockConfig
-- | A configurable text-based clock widget. It currently allows for
-- a configurable time zone through the 'ClockConfig'.
--
-- See also 'textClockNew'.
textClockNewWith :: (MonadIO m) => ClockConfig -> m Widget
textClockNewWith
ClockConfig
{ clockTimeZone = userZone,
clockTimeLocale = userLocale,
clockFormatString = formatString,
clockUpdateStrategy = updateStrategy,
clockVariableDelayConfig = variableDelayConfig
} = liftIO $ do
let getTZ = maybe getCurrentTimeZone return userZone
locale = fromMaybe L.defaultTimeLocale userLocale
let getUserZonedTime =
utcToZonedTime <$> getTZ <*> Clock.getCurrentTime
doTimeFormat zonedTime = T.pack $ formatTime locale formatString zonedTime
getRoundedTimeAndNextTarget = do
zonedTime <- getUserZonedTime
return $ case updateStrategy of
ConstantInterval interval ->
(doTimeFormat zonedTime, Nothing, interval)
RoundedTargetInterval roundSeconds offset ->
let roundSecondsDiffTime = fromIntegral roundSeconds
addTheRound = addLocalTime roundSecondsDiffTime
localTime = zonedTimeToLocalTime zonedTime
ourLocalTimeOfDay = localTimeOfDay localTime
seconds = round $ todSec ourLocalTimeOfDay
secondsFactor = seconds `div` roundSeconds
displaySeconds = secondsFactor * roundSeconds
baseLocalTimeOfDay =
ourLocalTimeOfDay {todSec = fromIntegral displaySeconds}
ourLocalTime =
localTime {localTimeOfDay = baseLocalTimeOfDay}
roundedLocalTime =
if seconds `mod` roundSeconds > roundSeconds `div` 2
then addTheRound ourLocalTime
else ourLocalTime
roundedZonedTime =
zonedTime {zonedTimeToLocalTime = roundedLocalTime}
nextTarget = addTheRound ourLocalTime
amountToWait = realToFrac $ diffLocalTime nextTarget localTime
in (doTimeFormat roundedZonedTime, Nothing, amountToWait - offset)
let pollingConfig =
defaultPollingLabelConfig
{ pollingLabelVariableDelayConfig = variableDelayConfig
}
label <- pollingLabelWithVariableDelayWithConfig pollingConfig getRoundedTimeAndNextTarget
_ <- widgetSetClassGI label "text-clock-label"
ebox <- eventBoxNew
_ <- widgetSetClassGI ebox "text-clock"
containerAdd ebox label
eventBoxSetVisibleWindow ebox False
cal <- makeCalendar getTZ
_ <-
onWidgetButtonPressEvent ebox $
onClick [Gdk.EventTypeButtonPress] $
toggleCalendar label cal
widgetShowAll ebox
toWidget ebox