packages feed

reflex-vty-1.0.0.0: src-bin/Example/CPU.hs

-- |
--   Description: A CPU usage indicator
module Example.CPU where

import Control.Exception
import Control.Monad.Fix
import Control.Monad.IO.Class
import Data.Ratio
import qualified Data.Text as T
import Data.Time
import Data.Word
import qualified Graphics.Vty as V
import Reflex
import Text.Printf

import Data.Text.Zipper (TextAlignment (..))
import Reflex.Vty

-- | Each constructor represents a cpu statistic column as presented in @/proc/stat@
data CpuStat
  = CpuStat_User
  | CpuStat_Nice
  | CpuStat_System
  | CpuStat_Idle
  | CpuStat_Iowait
  | CpuStat_Irq
  | CpuStat_Softirq
  | CpuStat_Steal
  | CpuStat_Guest
  | CpuStat_GuestNice
  deriving (Bounded, Enum, Eq, Ord, Read, Show)

-- | Read @/proc/stat@
getCpuStat :: IO (Maybe (CpuStat -> Word64))
getCpuStat = do
  s <- readFile "/proc/stat"
  _ <- evaluate $ length s -- Make readFile strict
  pure $ do
    cpuSummaryLine : _ <- pure $ lines s
    [user, nice, system, idle, iowait, irq, softirq, steal, guest, guestNice] <- pure $ map read $ words $ drop 4 cpuSummaryLine
    pure $ \case
      CpuStat_User -> user
      CpuStat_Nice -> nice
      CpuStat_System -> system
      CpuStat_Idle -> idle
      CpuStat_Iowait -> iowait
      CpuStat_Irq -> irq
      CpuStat_Softirq -> softirq
      CpuStat_Steal -> steal
      CpuStat_Guest -> guest
      CpuStat_GuestNice -> guestNice

sumStats :: (CpuStat -> Word64) -> [CpuStat] -> Word64
sumStats get stats = sum $ get <$> stats

-- | user + nice + system + irq + softirq + steal
nonIdleStats :: [CpuStat]
nonIdleStats =
  [ CpuStat_User
  , CpuStat_Nice
  , CpuStat_System
  , CpuStat_Irq
  , CpuStat_Softirq
  , CpuStat_Steal
  ]

-- | idle + iowait
idleStats :: [CpuStat]
idleStats =
  [ CpuStat_Idle
  , CpuStat_Iowait
  ]

-- | Draws the cpu usage percent as a live-updating bar graph. The output should look like:
--
-- > ╔══════ CPU Usage:  38% ══════╗
-- > ║                             ║
-- > ║                             ║
-- > ║                             ║
-- > ║                             ║
-- > ║                             ║
-- > ║                             ║
-- > ║█████████████████████████████║
-- > ║█████████████████████████████║
-- > ║█████████████████████████████║
-- > ║█████████████████████████████║
-- > ╚═════════════════════════════╝
cpuStats
  :: ( Reflex t
     , MonadFix m
     , MonadHold t m
     , MonadIO (Performable m)
     , MonadIO m
     , PerformEvent t m
     , PostBuild t m
     , TriggerEvent t m
     , HasDisplayRegion t m
     , HasImageWriter t m
     , HasLayout t m
     , HasFocus t m
     , HasInput t m
     , HasFocusReader t m
     , HasTheme t m
     )
  => m ()
cpuStats = do
  tick <- tickLossy 0.25 =<< liftIO getCurrentTime
  cpuStat :: Event t (Word64, Word64) <- fmap (fmapMaybe id) $
    performEvent $
      ffor tick $ \_ -> do
        get <- liftIO getCpuStat
        pure $ case get of
          Nothing -> Nothing
          Just get' -> Just (sumStats get' nonIdleStats, sumStats get' idleStats)
  active <- foldDyn cpuPercentStep ((0, 0), 0) cpuStat
  let pct = fmap snd active
  chart pct

chart
  :: ( MonadFix m
     , MonadHold t m
     , HasFocus t m
     , HasLayout t m
     , HasImageWriter t m
     , HasInput t m
     , HasDisplayRegion t m
     , HasFocusReader t m
     , HasTheme t m
     )
  => Dynamic t (Ratio Word64) -> m ()
chart pct = do
  let title = ffor pct $ \x ->
        mconcat
          [ " CPU Usage: "
          , T.pack (printf "%3d" $ (ceiling $ x * 100 :: Int))
          , "% "
          ]
  boxTitle (pure TextAlignment_Center) (pure doubleBoxStyle) (current title) $
    grout flex $ do
      dh <- displayHeight
      dw <- displayWidth
      let grad = gradient1D [(0.0, RGB 50 205 50), (0.4, RGB 255 220 0), (0.7, RGB 255 140 0), (1.0, RGB 220 40 40)]
          chartImgs h w r =
            let filledF = fromIntegral (numerator r) * fromIntegral h / fromIntegral (denominator r) :: Double
                filled = min h $ floor filledF
                frac = filledF - fromIntegral filled
                partial = if frac > 0 && filled < h then ceiling (8 * frac) :: Int else 0
                rowColor i = V.withForeColor V.defAttr $ fromRGB $ sampleGradient1D grad (fromIntegral i / fromIntegral (max 1 (h - 1)))
                fullImgs =
                  [ V.translate 0 (h - 1 - i) $ V.charFill (rowColor i) '█' w 1
                  | i <- [0 .. filled - 1]
                  ]
                partialImg =
                  if partial > 0 && filled < h
                    then [V.translate 0 (h - 1 - filled) $ V.text' (rowColor filled) (T.replicate w (T.singleton $ eighthBlocks partial))]
                    else []
            in fullImgs ++ partialImg
      tellImages $ chartImgs <$> current dh <*> current dw <*> current pct

eighthBlocks :: (Eq a, Num a, Ord a) => a -> Char
eighthBlocks n =
  if
    | n <= 0 -> ' '
    | n == 1 -> '▁'
    | n == 2 -> '▂'
    | n == 3 -> '▃'
    | n == 4 -> '▄'
    | n == 5 -> '▅'
    | n == 6 -> '▆'
    | n == 7 -> '▇'
    | otherwise -> '█'

-- | Determine the current percentage usage according to this algorithm:
--
-- PrevIdle = previdle + previowait
-- Idle = idle + iowait
--
-- PrevNonIdle = prevuser + prevnice + prevsystem + previrq + prevsoftirq + prevsteal
-- NonIdle = user + nice + system + irq + softirq + steal
--
-- PrevTotal = PrevIdle + PrevNonIdle
-- Total = Idle + NonIdle
--
-- totald = Total - PrevTotal
-- idled = Idle - PrevIdle
--
-- CPU_Percentage = (totald - idled)/totald
--
-- Source: https://stackoverflow.com/questions/23367857/accurate-calculation-of-cpu-usage-given-in-percentage-in-linux
cpuPercentStep
  :: (Word64, Word64) -- Current active, Current idle
  -> ((Word64, Word64), Ratio Word64) -- (Previous idle, Previous total), previous percent
  -> ((Word64, Word64), Ratio Word64) -- (New idle, new total), percent
cpuPercentStep (nonidle, idle) ((previdle, prevtotal), _) =
  let total = idle + nonidle
      idled = idle - previdle
      totald = total - prevtotal
  in ( (idle, total)
     , (totald - idled) % totald
     )