packages feed

taffybar-7.2.0: src/System/Taffybar/Widget/WakeupDebug.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Debug widget for coordinated wakeup scheduler visibility.
module System.Taffybar.Widget.WakeupDebug
  ( WakeupDebugWidgetConfig (..),
    defaultWakeupDebugWidgetConfig,
    wakeupDebugWidgetNew,
    wakeupDebugWidgetNewWithConfig,
  )
where

import Control.Concurrent (forkIO, killThread, threadDelay)
import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TChan (TChan, readTChan)
import Control.Monad (forever, void, when)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Reader (ask, runReaderT)
import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef)
import qualified Data.Set as Set
import qualified Data.Text as T
import Data.Word (Word64)
import qualified GI.Gdk as Gdk
import qualified GI.Gtk as Gtk
import Numeric (showFFloat)
import System.Taffybar.Context (TaffyIO)
import System.Taffybar.Information.Wakeup
  ( WakeupEvent (..),
    WakeupSchedulerEvent (..),
    getRegisteredWakeupIntervalsNanoseconds,
    getWakeupSchedulerEvents,
  )
import System.Taffybar.Util (postGUIASync)
import System.Taffybar.Widget.Util (onClick, widgetSetClassGI)

-- | Configuration for 'wakeupDebugWidgetNewWithConfig'.
data WakeupDebugWidgetConfig = WakeupDebugWidgetConfig
  { wakeupDebugFlashDurationMilliseconds :: Int,
    wakeupDebugExpandedByDefault :: Bool
  }
  deriving (Eq, Ord, Show)

defaultWakeupDebugWidgetConfig :: WakeupDebugWidgetConfig
defaultWakeupDebugWidgetConfig =
  WakeupDebugWidgetConfig
    { wakeupDebugFlashDurationMilliseconds = 180,
      wakeupDebugExpandedByDefault = False
    }

data WakeupDebugState = WakeupDebugState
  { wakeupDebugLastEvents :: [WakeupEvent],
    wakeupDebugRegisteredIntervals :: [Word64],
    wakeupDebugExpanded :: Bool
  }

wakeupDebugWidgetNew :: TaffyIO Gtk.Widget
wakeupDebugWidgetNew =
  wakeupDebugWidgetNewWithConfig defaultWakeupDebugWidgetConfig

wakeupDebugWidgetNewWithConfig :: WakeupDebugWidgetConfig -> TaffyIO Gtk.Widget
wakeupDebugWidgetNewWithConfig config = do
  context <- ask
  schedulerEvents <- getWakeupSchedulerEvents
  registered <- getRegisteredWakeupIntervalsNanoseconds
  liftIO $ do
    label <- Gtk.labelNew (Nothing :: Maybe T.Text)
    eventBox <- Gtk.eventBoxNew
    _ <- widgetSetClassGI eventBox "wakeup-debug"
    _ <- widgetSetClassGI label "wakeup-debug-label"
    Gtk.eventBoxSetVisibleWindow eventBox False
    Gtk.containerAdd eventBox label

    flashTokenRef <- newIORef (0 :: Int)
    stateRef <-
      newIORef
        WakeupDebugState
          { wakeupDebugLastEvents = [],
            wakeupDebugRegisteredIntervals = registered,
            wakeupDebugExpanded = wakeupDebugExpandedByDefault config
          }

    refreshWidget stateRef label eventBox

    let refreshRegisteredIntervals = runReaderT getRegisteredWakeupIntervalsNanoseconds context

    _ <-
      Gtk.onWidgetButtonPressEvent eventBox $
        onClick [Gdk.EventTypeButtonPress] $ do
          intervals <- refreshRegisteredIntervals
          _ <-
            atomicModifyIORef' stateRef $ \state ->
              let nextState =
                    state
                      { wakeupDebugRegisteredIntervals = intervals,
                        wakeupDebugExpanded = not (wakeupDebugExpanded state)
                      }
               in (nextState, nextState)
          refreshWidget stateRef label eventBox

    _ <- Gtk.onWidgetRealize eventBox $ do
      listenerThread <- forkIO $ wakeupDebugLoop schedulerEvents refreshRegisteredIntervals stateRef label eventBox flashTokenRef config
      void $ Gtk.onWidgetUnrealize eventBox $ killThread listenerThread

    Gtk.widgetShowAll eventBox
    Gtk.toWidget eventBox

wakeupDebugLoop ::
  TChan WakeupSchedulerEvent ->
  IO [Word64] ->
  IORef WakeupDebugState ->
  Gtk.Label ->
  Gtk.EventBox ->
  IORef Int ->
  WakeupDebugWidgetConfig ->
  IO ()
wakeupDebugLoop schedulerEvents refreshRegisteredIntervals stateRef label eventBox flashTokenRef config =
  forever $ do
    wakeupEvent <- atomically $ readTChan schedulerEvents
    intervals <- refreshRegisteredIntervals
    _ <-
      atomicModifyIORef' stateRef $ \state ->
        let nextState =
              state
                { wakeupDebugLastEvents = wakeupDueEvents wakeupEvent,
                  wakeupDebugRegisteredIntervals = intervals
                }
         in (nextState, nextState)
    refreshWidget stateRef label eventBox
    triggerFlash flashTokenRef (wakeupDebugFlashDurationMilliseconds config) eventBox

refreshWidget :: IORef WakeupDebugState -> Gtk.Label -> Gtk.EventBox -> IO ()
refreshWidget stateRef label eventBox = do
  state <- readIORef stateRef
  postGUIASync $ do
    Gtk.labelSetText label (renderWakeupDebugLabel state)
    Gtk.widgetSetTooltipText eventBox (Just (renderWakeupDebugTooltip state))

triggerFlash :: IORef Int -> Int -> Gtk.EventBox -> IO ()
triggerFlash tokenRef flashDurationMs eventBox = do
  token <- atomicModifyIORef' tokenRef $ \cur -> let next = cur + 1 in (next, next)
  postGUIASync $ setFlashClass eventBox True
  _ <- forkIO $ do
    threadDelay (flashDurationMs * 1000)
    latestToken <- readIORef tokenRef
    when (latestToken == token) $
      postGUIASync $
        setFlashClass eventBox False
  pure ()

setFlashClass :: Gtk.EventBox -> Bool -> IO ()
setFlashClass eventBox shouldSet = do
  context <- Gtk.widgetGetStyleContext eventBox
  if shouldSet
    then Gtk.styleContextAddClass context "wakeup-debug-hit"
    else Gtk.styleContextRemoveClass context "wakeup-debug-hit"

renderWakeupDebugLabel :: WakeupDebugState -> T.Text
renderWakeupDebugLabel state =
  if wakeupDebugExpanded state
    then
      T.intercalate
        "\n"
        [ "wake " <> formatEventIntervals (wakeupDebugLastEvents state),
          "reg " <> formatIntervals (wakeupDebugRegisteredIntervals state)
        ]
    else "wake " <> formatEventIntervals (wakeupDebugLastEvents state)

renderWakeupDebugTooltip :: WakeupDebugState -> T.Text
renderWakeupDebugTooltip state =
  T.intercalate
    "\n"
    [ "Click to toggle registered interval list",
      "Last wakeup intervals: " <> formatEventIntervals (wakeupDebugLastEvents state),
      "Last wakeup ticks: " <> formatEventTicks (wakeupDebugLastEvents state),
      "Registered intervals: " <> formatIntervals (wakeupDebugRegisteredIntervals state)
    ]

formatEventIntervals :: [WakeupEvent] -> T.Text
formatEventIntervals events =
  formatIntervals $ fmap wakeupIntervalNanoseconds events

formatEventTicks :: [WakeupEvent] -> T.Text
formatEventTicks events
  | null events = "none"
  | otherwise = T.intercalate ", " $ fmap renderEvent events
  where
    renderEvent event =
      formatIntervalNanoseconds (wakeupIntervalNanoseconds event)
        <> "#"
        <> T.pack (show (wakeupTickCount event))

formatIntervals :: [Word64] -> T.Text
formatIntervals intervals
  | null intervals = "none"
  | otherwise =
      T.intercalate ", " $
        fmap formatIntervalNanoseconds $
          Set.toAscList $
            Set.fromList intervals

formatIntervalNanoseconds :: Word64 -> T.Text
formatIntervalNanoseconds nanoseconds
  | nanoseconds `mod` secondsToNanoseconds == 0 =
      T.pack (show (nanoseconds `div` secondsToNanoseconds)) <> "s"
  | nanoseconds `mod` millisecondsToNanoseconds == 0 =
      T.pack (show (nanoseconds `div` millisecondsToNanoseconds)) <> "ms"
  | nanoseconds `mod` microsecondsToNanoseconds == 0 =
      T.pack (show (nanoseconds `div` microsecondsToNanoseconds)) <> "us"
  | otherwise = T.pack (showFFloat (Just 3) secondsValue "s")
  where
    secondsValue :: Double
    secondsValue = fromIntegral nanoseconds / 1000000000

secondsToNanoseconds :: Word64
secondsToNanoseconds = 1000000000

millisecondsToNanoseconds :: Word64
millisecondsToNanoseconds = 1000000

microsecondsToNanoseconds :: Word64
microsecondsToNanoseconds = 1000