packages feed

i3blocks-hs-contrib-2.0.0: Bandwidth/Main.hs

{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

module Main where

import           Control.Applicative       (liftA3)
import           Control.Exception         (try)
import           Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)
import           Data.Bool                 (bool)
import           Data.Foldable             (fold)
import           Data.Functor              (($>))
import           Data.List                 (intersperse)
import           Data.Maybe                (maybe)
import           Data.Monoid               ((<>))
import           Data.Text                 (null, pack, strip, unpack)
import           Data.Time.Clock.POSIX     (getPOSIXTime)
import           Prelude                   hiding (FilePath, putStrLn)
import           Text.Printf               (printf)
import           Data.Text.IO              (putStrLn)
import           Turtle                    hiding (empty, fold, printf)
import           Data.Maybe
import           System.Environment (getArgs)

data Report = Report {
    uploadRate   :: UploadRate,
    downloadRate :: DownloadRate
  } deriving Show

newtype DownloadRate = DownloadRate { runDownloadRate :: TransferRate } deriving Show
newtype UploadRate = UploadRate { runUploadRate :: TransferRate } deriving Show

data TransferRate = TransferRate {
    value :: Double,
    unit  :: TransferUnit
  } deriving (Show, Eq)

data TransferUnit = Bs | KBs | MBs deriving (Show, Eq, Enum, Ord)

data Record = Record {
    timestamp     :: Timestamp,
    bytesReceived :: TotalBytesIn,
    bytesSent     :: TotalBytesOut
  } deriving Show

newtype TotalBytesIn = TotalBytesIn { runTotalBytesIn :: Integer } deriving (Show, Read)
newtype TotalBytesOut = TotalBytesOut { runTotalBytesOut :: Integer } deriving (Show, Read)
type Timestamp = Integer
type NetworkInterface = Text

dataPathForInterface :: NetworkInterface -> FilePath
dataPathForInterface interface = dataPath </> fromText interface

dataPath :: FilePath
dataPath = "/dev/shm/i3blocks-bandwidth-module"

main :: IO ()
main = do
  icons <- getArgs
  sh
          $   defaultInterface
          >>= maybe handleNoInterface (runScript icons)
          >>= liftIO
          .   putStrLn
  where
    handleNoInterface = return "No UP interface"

initPath :: NetworkInterface -> Shell FilePath
initPath interface = do
        mktree dataPath
        touch $ dataPathForInterface interface
        return $ dataPathForInterface interface

runScript :: [String] -> NetworkInterface -> Shell Text
runScript icons interface = do
  _ <- initPath interface
  record <- readRecord interface
  maybe handleNoRecordAvailable handleRecord record
    where
      handleRecord oldRecord =
          formatReport icons
          .   applyBestUnit
          .   speedReport oldRecord
          <$> writeRecord interface
      handleNoRecordAvailable = do
        _ <- writeRecord interface
        return $ "No data for " <> (pack $ show interface)

writeRecord :: NetworkInterface -> Shell Record
writeRecord interface =
        liftA3 Record
               (liftIO poxisTimeAsInteger)
               (readBytesReceived interface)
               (readBytesTransfered interface)
                >>= liftIO
                .   writeRecordToFile
    where
        recordToText record =
                pack
                        . fold
                        . intersperse " "
                        $ [ show . timestamp $ record
                          , show . runTotalBytesIn . bytesReceived $ record
                          , show . runTotalBytesOut . bytesSent $ record
                          , "\n"
                          ]
        poxisTimeAsInteger = round <$> getPOSIXTime
        writeRecordToFile record =
                ( writeTextFile (dataPathForInterface interface)
                        . recordToText
                        $ record
                        )
                        $> record

readRecord :: NetworkInterface -> Shell (Maybe Record)
readRecord interface = liftIO . runMaybeT $ extractRecord =<< safeReadTextFile
    where
        safeReadTextFile =
                MaybeT
                        $   tryToMaybe
                        <$> try
                                    (strip <$> readTextFile
                                            (dataPathForInterface interface)
                                    )
        extractRecord =
                MaybeT . return . matchToMaybe . match (decimal `sepBy` " ")
        matchToMaybe = \case
                [[time', received, sent]] -> Just $ Record
                        time'
                        (TotalBytesIn received)
                        (TotalBytesOut sent)
                _ -> Nothing
        tryToMaybe :: Either IOError b -> Maybe b
        tryToMaybe (Left  _)  = Nothing
        tryToMaybe (Right x') = Just x'

speedReport :: Record -> Record -> Report
speedReport oldRecord newRecord = Report (UploadRate uploadRate')
                                         (DownloadRate downloadRate')
    where
        timediff = max (timestamp newRecord - timestamp oldRecord) 1
        bytesReceivedDiff =
                runTotalBytesIn (bytesReceived newRecord)
                        - runTotalBytesIn (bytesReceived oldRecord)
        bytesSentDiff =
                runTotalBytesOut (bytesSent newRecord)
                        - runTotalBytesOut (bytesSent oldRecord)
        downloadRate' = TransferRate
                (fromIntegral bytesReceivedDiff / fromIntegral timediff)
                Bs
        uploadRate' = TransferRate
                (fromIntegral bytesSentDiff / fromIntegral timediff)
                Bs

getUploadIcon :: [String] -> Maybe Text
getUploadIcon icons = case icons of
  uploadIcon : _ -> Just $ pack uploadIcon
  _              -> Nothing

getDownloadIcon :: [String] -> Maybe Text
getDownloadIcon icons = case icons of
  _ : downloadIcon : _ -> Just $ pack downloadIcon
  _                    -> Nothing

formatReport :: [String] -> Report -> Text
formatReport icons report =
  let downloadIcon = fromMaybe "Rx " $ getDownloadIcon icons
      uploadIcon   = fromMaybe "Tx " $ getUploadIcon icons
  in  downloadIcon
                <> ( pack
                   . printf "%.1f"
                   . value
                   . runDownloadRate
                   . downloadRate
                   $ report
                   )
                <> (pack . show . unit . runDownloadRate . downloadRate $ report)
                <> "  "
                <> uploadIcon
                <> ( pack
                   . printf "%.1f"
                   . value
                   . runUploadRate
                   . uploadRate
                   $ report
                   )
                <> (pack . show . unit . runUploadRate . uploadRate $ report)

applyBestUnit :: Report -> Report
applyBestUnit = liftA2
        Report
        (UploadRate . applyBestUnit' . runUploadRate . uploadRate)
        (DownloadRate . applyBestUnit' . runDownloadRate . downloadRate)
    where
        applyBestUnit' transferRate | value transferRate < 1024 = transferRate
        applyBestUnit' transferRate = applyBestUnit'
                $ convertRate transferRate (succ . unit $ transferRate)

convertRate :: TransferRate -> TransferUnit -> TransferRate
convertRate rate to | unit rate < to = convertRate (convertRateUp rate) to
    where
        convertRateUp (TransferRate rate' from)
          = TransferRate (rate' / 1024) (succ from)
convertRate rate' to | unit rate' > to = convertRate (convertRateDown rate') to
    where
        convertRateDown (TransferRate rate from)
          = TransferRate (rate * 1024) (pred from)
convertRate rate _ = rate

defaultInterface :: Shell (Maybe NetworkInterface)
defaultInterface =
        textToMaybe . strip
                <$> strict (inshell
                            "ip addr | awk '/state UP/ {print $2}' | sed 's/.$//'"
                            mempty
                    )
        where textToMaybe text' = bool (Just text') Nothing (Data.Text.null text')

readBytesTransfered :: Text -> Shell TotalBytesOut
readBytesTransfered interface =
        TotalBytesOut . read . unpack . strip
                <$> strict (inshell
                            (  "cat /sys/class/net/"
                            <> interface
                            <> "/statistics/tx_bytes"
                            )
                            mempty
                    )

readBytesReceived :: Text -> Shell TotalBytesIn
readBytesReceived interface =
        TotalBytesIn . read . unpack . strip
                <$> strict (inshell
                            (  "cat /sys/class/net/"
                            <> interface
                            <> "/statistics/rx_bytes"
                            )
                            mempty
                    )