packages feed

reflex-vty-0.2.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 Data.Time
import Data.Word
import qualified Data.Text as T
import qualified Graphics.Vty as V
import Text.Printf

import Reflex
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 (Show, Read, Eq, Ord, Enum, Bounded)

-- | 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
     , 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 doubleBoxStyle) (current title) $ col $ do
    grout flex blank
    dh <- displayHeight
    let heights = calcRowHeights <$> dh <*> pct
        quarters = fst <$> heights
        eighths = snd <$> heights
        eighthRow = ffor eighths $ \x -> if x == 0 then 0 else 1
    grout (fixed eighthRow) $ fill' (current $ eighthBlocks <$> eighths) $ current $
      ffor quarters $ \q ->
        if | _quarter_fourth q > 0 -> red
           | _quarter_third q > 0 -> orange
           | _quarter_second q > 0 -> yellow
           | otherwise -> white
    grout (fixed $ _quarter_fourth <$> quarters) $ fill' (pure '█') (pure red)
    grout (fixed $ _quarter_third <$> quarters) $ fill' (pure '█') (pure orange)
    grout (fixed $ _quarter_second <$> quarters) $ fill' (pure '█') (pure yellow)
    grout (fixed $ _quarter_first <$> quarters) $ fill' (pure '█') (pure white)
  where
    -- Calculate number of full rows, height of partial row
    calcRowHeights :: Int -> Ratio Word64 -> (Quarter Int, Int)
    calcRowHeights h r =
      let (full, leftovers) = divMod (numerator r * fromIntegral h) (denominator r)
          partial = ceiling $ 8 * (leftovers % denominator r)
          quarter = ceiling $ fromIntegral h / (4 :: Double)
          n = fromIntegral full
      in if | n <= quarter ->
                (Quarter n 0 0 0, partial)
            | n <= (2 * quarter) ->
                (Quarter quarter (n - quarter) 0 0, partial)
            | n <= (3 * quarter) ->
                (Quarter quarter quarter (n - (2 * quarter)) 0, partial)
            | otherwise ->
                (Quarter quarter quarter quarter (n - (3 * quarter)), partial)
    fill' bc attr = do
      dw <- displayWidth
      dh <- displayHeight
      let fillImg =
            (\w h c a -> [V.charFill a c w h])
            <$> current dw
            <*> current dh
            <*> bc
            <*> attr
      tellImages fillImg
    color :: Int -> Int -> Int -> V.Color
    color = V.rgbColor
    red = V.withForeColor V.defAttr $ color 255 0 0
    orange = V.withForeColor V.defAttr $ color 255 165 0
    yellow = V.withForeColor V.defAttr $ color 255 255 0
    white = V.withForeColor V.defAttr $ color 255 255 255

data Quarter a = Quarter
  { _quarter_first :: a
  , _quarter_second :: a
  , _quarter_third :: a
  , _quarter_fourth :: a
  }

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
     )