From 22c9d47dfc9eb6113620258400424bc777cb3236 Mon Sep 17 00:00:00 2001
From: Jan Beinke <git@janbeinke.com>
Date: Fri, 14 Feb 2020 11:36:10 +0100
Subject: [PATCH] Add a native cpu-usage block

---
 src/QBar/Blocks.hs          |   2 +
 src/QBar/Blocks/CpuUsage.hs | 152 ++++++++++++++++++++++++++++++++++++
 src/QBar/DefaultConfig.hs   |   4 +-
 3 files changed, 156 insertions(+), 2 deletions(-)
 create mode 100644 src/QBar/Blocks/CpuUsage.hs

diff --git a/src/QBar/Blocks.hs b/src/QBar/Blocks.hs
index cedacdb..1b15b76 100644
--- a/src/QBar/Blocks.hs
+++ b/src/QBar/Blocks.hs
@@ -1,8 +1,10 @@
 module QBar.Blocks
   ( QBar.Blocks.Battery.batteryBlock,
+    QBar.Blocks.CpuUsage.cpuUsageBlock,
     QBar.Blocks.Date.dateBlock,
   )
 where
 
 import qualified QBar.Blocks.Battery
+import qualified QBar.Blocks.CpuUsage
 import qualified QBar.Blocks.Date
diff --git a/src/QBar/Blocks/CpuUsage.hs b/src/QBar/Blocks/CpuUsage.hs
new file mode 100644
index 0000000..12fc773
--- /dev/null
+++ b/src/QBar/Blocks/CpuUsage.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module QBar.Blocks.CpuUsage where
+
+import Control.Applicative ((<|>))
+import Control.Lens
+import Control.Monad.State
+import qualified Data.Attoparsec.Text.Lazy as AT
+import qualified Data.Text.Lazy as T
+import QBar.BlockOutput
+import QBar.Blocks.Utils
+import QBar.Core
+
+{-
+  For time accounting the guest fields need to be ignored according to the kernel source code
+  (https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/kernel/sched/cputime.c#n150)
+  the accounting also counts the guest time to user or nice respectively so applications
+  that are not aware of the new fields do not loose time.
+-}
+data CpuStat
+  = CpuStat
+      { userTime :: Int,
+        niceTime :: Int,
+        systemTime :: Int,
+        idleTime :: Int,
+        iowaitTime :: Int,
+        irqTime :: Int,
+        softirqTime :: Int,
+        stealTime :: Int
+      }
+  deriving (Show)
+
+getCpuStat :: IO (Maybe CpuStat)
+getCpuStat = parseFile "/proc/stat" cpuStat
+  where
+    skipLine :: AT.Parser ()
+    skipLine = AT.skipWhile (not . AT.isEndOfLine) *> AT.skipWhile AT.isEndOfLine
+    cpuStat :: AT.Parser CpuStat
+    cpuStat = cpuLine <|> (skipLine *> cpuStat)
+    cpuLine :: AT.Parser CpuStat
+    cpuLine = AT.string "cpu " *> do
+      userTime' <- AT.skipSpace *> AT.decimal
+      niceTime' <- AT.skipSpace *> AT.decimal
+      systemTime' <- AT.skipSpace *> AT.decimal
+      idleTime' <- AT.skipSpace *> AT.decimal
+      iowaitTime' <- AT.skipSpace *> AT.decimal
+      irqTime' <- AT.skipSpace *> AT.decimal
+      softirqTime' <- AT.skipSpace *> AT.decimal
+      stealTime' <- AT.skipSpace *> AT.decimal
+      return $
+        CpuStat
+          { userTime = userTime',
+            niceTime = niceTime',
+            systemTime = systemTime',
+            idleTime = idleTime',
+            iowaitTime = iowaitTime',
+            irqTime = irqTime',
+            softirqTime = softirqTime',
+            stealTime = stealTime'
+          }
+
+differenceCpuStat :: CpuStat -> CpuStat -> CpuStat
+differenceCpuStat a b =
+  CpuStat
+    { userTime = userTime a - userTime b,
+      niceTime = niceTime a - niceTime b,
+      systemTime = systemTime a - systemTime b,
+      idleTime = idleTime a - idleTime b,
+      iowaitTime = iowaitTime a - iowaitTime b,
+      irqTime = irqTime a - irqTime b,
+      softirqTime = softirqTime a - softirqTime b,
+      stealTime = stealTime a - stealTime b
+    }
+
+cpuTotalTime :: Num a => CpuStat -> a
+cpuTotalTime
+  CpuStat
+    { userTime,
+      niceTime,
+      systemTime,
+      idleTime,
+      iowaitTime,
+      irqTime,
+      softirqTime,
+      stealTime
+    } =
+    fromIntegral . sum $
+      [ userTime,
+        niceTime,
+        systemTime,
+        idleTime,
+        iowaitTime,
+        irqTime,
+        softirqTime,
+        stealTime
+      ]
+
+cpuUsage :: CpuStat -> Float
+cpuUsage stat@CpuStat {idleTime, iowaitTime} = 1 - (totalIdleTime / totalTime)
+  where
+    totalTime :: Num a => a
+    totalTime = cpuTotalTime stat
+    totalIdleTime :: Num a => a
+    totalIdleTime = fromIntegral $ idleTime + iowaitTime
+
+data CpuBlockState
+  = CpuBlockState
+      { _lastCpuStat :: CpuStat,
+        _lastCpuUsage :: Float
+      }
+  deriving (Show)
+
+makeLenses ''CpuBlockState
+
+cpuUsageBlock :: Int -> PullBlock
+cpuUsageBlock decimalPlaces = evalStateT cpuUsageBlock' createState
+  where
+    cpuUsageBlock' :: StateT CpuBlockState Block PullMode
+    cpuUsageBlock' = do
+      updateState
+      importance <- cpuUsageImportance
+      updateBlock . mkBlockOutput . importantText importance =<< cpuUsageText
+      cpuUsageBlock'
+    createState :: CpuBlockState
+    createState =
+      CpuBlockState
+        { _lastCpuStat = CpuStat 0 0 0 0 0 0 0 0,
+          _lastCpuUsage = 0
+        }
+    cpuUsageImportance :: Monad m => StateT CpuBlockState m Importance
+    cpuUsageImportance = toImportance (100, 90, 80, 60, 50, 0) <$> use lastCpuUsage
+    cpuUsageTextWidth :: Num a => a
+    cpuUsageTextWidth
+      | decimalPlaces == 0 = 3
+      | otherwise = fromIntegral $ 4 + decimalPlaces
+    cpuUsageText :: Monad m => StateT CpuBlockState m Text
+    cpuUsageText = do
+      cpuUsage' <- use lastCpuUsage
+      let cpuUsageText' = formatFloatN decimalPlaces cpuUsage' <> "%"
+      if T.length cpuUsageText' <= cpuUsageTextWidth
+        then return $ T.justifyRight cpuUsageTextWidth ' ' cpuUsageText'
+        else return $ "99" <> (if decimalPlaces == 0 then "" else "." <> T.replicate (fromIntegral decimalPlaces) "9") <> "%"
+    updateState :: MonadBarIO m => StateT CpuBlockState m ()
+    updateState = do
+      oldCpuStat <- use lastCpuStat
+      newCpuStat' <- liftIO getCpuStat
+      case newCpuStat' of
+        Nothing -> return ()
+        (Just newCpuStat) -> do
+          let cpuStatDiff = differenceCpuStat newCpuStat oldCpuStat
+          lastCpuUsage .= 100 * cpuUsage cpuStatDiff
+          lastCpuStat .= newCpuStat
diff --git a/src/QBar/DefaultConfig.hs b/src/QBar/DefaultConfig.hs
index 8118633..1d09d4a 100644
--- a/src/QBar/DefaultConfig.hs
+++ b/src/QBar/DefaultConfig.hs
@@ -18,18 +18,18 @@ generateDefaultBarConfig = do
   let todo = systemInfoInterval (blockScript $ blockLocation "todo")
   let wifi = systemInfoInterval (blockScript $ blockLocation "wifi2") >-> modify (addIcon "📡\xFE0E")
   let networkEnvironment = systemInfoInterval (blockScript $ blockLocation "network-environment")
-  let cpu = systemInfoInterval (blockScript $ blockLocation "cpu_usage") >-> modify ((blockName ?~ "cpu") . addIcon "💻\xFE0E") >-> autoPadding
   let ram = systemInfoInterval (blockScript $ blockLocation "memory") >-> modify (addIcon "🐏\xFE0E") >-> autoPadding
   let temperature = systemInfoInterval (blockScript $ blockLocation "temperature") >-> autoPadding
   let volumeBlock = persistentBlockScript $ blockLocation "volume-pulseaudio -S -F3"
   let battery = systemInfoInterval $ batteryBlock >-> modify (blockName ?~ "battery")
+  let cpuUsage = systemInfoInterval $ cpuUsageBlock 1 >-> modify ((blockName ?~ "cpuUsage") . addIcon "💻\xFE0E")
 
   addBlock dateBlock
   addBlock battery
   addBlock volumeBlock
   addBlock temperature
   addBlock ram
-  addBlock cpu
+  addBlock cpuUsage
   addBlock networkEnvironment
   addBlock wifi
   addBlock todo
-- 
GitLab