termplot-0.1.0.0: Main.hs
import Brick
import Control.Concurrent
import Control.Monad
import Control.Monad.IO.Class
import Data.Default
import Data.List.Split
import Data.Time.Units
import Graphics.Vty hiding ((<|>))
import Options.Applicative hiding (str)
import System.Exit
import System.Posix.IO
import System.Posix.Terminal
import System.Process
import Text.Printf
data AppEvent = Tick
| Terminal Event
data AppState = AppState {
appCmds :: [String]
, appCmdOuts :: [[Double]]
, appCmdNames :: [String]
, appRefreshRate :: Second
, appLogScale :: Bool
, appSerieSize :: Int
} deriving Show
main :: IO ()
main = execParser opts >>= runApp
where parser = AppState <$> many (strArgument (
metavar "CMD"
<> help "Command output to plot (<name>:<cmd>)"))
<*> pure []
<*> pure []
<*> ((toPosNum <$> strOption (
short 'i'
<> long "interval"
<> metavar "INTERVAL"
<> help "Updates interval time (secs)"))
<|> pure 1)
<*> switch (
short 'l'
<> long "log"
<> help "Use logarithmic scale")
<*> ((toPosNum <$> strOption (
short 's'
<> long "size"
<> metavar "SIZE"
<> help "Size of the time series buffer"))
<|> pure 1024)
opts = info (helper <*> parser) mempty
nonInteractivePlot :: Bool -> IO ()
nonInteractivePlot useLog = do
series <- map (map toPosNum . concatMap (splitOn ",") . splitOn " ") . lines <$> getContents
if all ((== 1) . length) series
then putStrLn $ getBars' $ concat series
else mapM_ (putStrLn . getBars') series
where getBars' | useLog = getBars . logScale
| otherwise = getBars
toPosNum :: (Ord a, Num a, Read a) => String -> a
toPosNum s = case reads s of
[(v, _)] | v >= 0 -> v
| otherwise -> error $ printf "negative numeric value: %v" s
_ -> error $ printf "invalid numeric value: %v" s
runCmd :: String -> IO String
runCmd cmd = do
(exit, stdout, _) <- readCreateProcessWithExitCode (shell cmd) mempty
case exit of
ExitFailure _ -> error $ "Command failed: " ++ cmd
ExitSuccess -> return stdout
runApp :: AppState -> IO ()
runApp app = do
let termApp = App {
appDraw = \a -> [renderApp a]
, appHandleEvent = loopApp
, appStartEvent = return
, appAttrMap = def
, appLiftVtyEvent = Terminal
, appChooseCursor = neverShowCursor
}
isatty <- queryTerminal stdInput
if not isatty
then nonInteractivePlot $ appLogScale app
else
if null $ appCmds app
then error "no command to execute specified nor input from the stdin"
else do
ticker <- runTicker $ appRefreshRate app
void $ customMain (mkVty def) ticker termApp (initApp app)
renderApp :: AppState -> Widget
renderApp app = seriesNames <+> lastValues <+> vBox (map (str . getBars') $ appCmdOuts app)
where seriesNames = pad1 $ vBox (map str (appCmdNames app))
lastValues = pad1 $ vBox (map (str . showSeries) (appCmdOuts app))
pad1 = padRight $ Pad 1
showSeries [] = ""
showSeries s = show $ head s
getBars' | appLogScale app = getBars . logScale
| otherwise = getBars
loopApp :: AppState -> AppEvent -> EventM (Next AppState)
loopApp app (Terminal (EvKey (KChar 'c') [MCtrl])) = halt app
loopApp app (Terminal (EvKey KEsc _)) = halt app
loopApp app _ = do
newOuts <- liftIO $ zipWithM (runAppCmd maxLen) (appCmds app) (appCmdOuts app)
continue $ app { appCmdOuts = newOuts }
where maxLen = appSerieSize app
runAppCmd :: Int -> String -> [Double] -> IO [Double]
runAppCmd maxLen cmd vals = do
cmdOut <- runCmd cmd
case reads cmdOut of
[(v, _)] | v >= 0 -> return $ take maxLen $ v:vals
| otherwise -> error $ printf "negative numeric value: %v (from: %v)" cmdOut cmd
_ -> error $ printf "invalid numeric value: %v (from: %v)" cmdOut cmd
initApp :: AppState -> AppState
initApp app =
let rawCmds = appCmds app
(names, cmds) = unzip $ map splitNameCmd rawCmds
emptySeries = replicate (length rawCmds) []
in app { appCmdNames = names, appCmds = cmds, appCmdOuts = emptySeries}
splitNameCmd :: String -> (String, String)
splitNameCmd s = case span (/= ':') s of
(name, cmd) | null name || null cmd -> error $ printf "missing name for command: %v" s
| length cmd < 2 -> error $ printf "missing command: %v" s
| otherwise -> (name, tail cmd)
runTicker :: Second -> IO (Chan AppEvent)
runTicker rate = do
ch <- newChan
void $ forkIO $ forever $ do
writeChan ch Tick
threadDelay $ fromInteger $ toMicroseconds rate
return ch
barChars :: String
barChars = " ▁▂▃▄▅▆▇█"
getBar :: Double -> Double -> Double -> Char
getBar min' max' n | min' == max' = barChars !! round (fromIntegral (length barChars) / 2 :: Double)
| otherwise =
let len = fromIntegral $ length barChars
wid = (max' - min') / (len - 1)
idx = round $ (n - min') / wid
in barChars !! idx
getBars :: [Double] -> String
getBars l = map (getBar (minimum l) (maximum l)) l
logScale :: [Double] -> [Double]
logScale = map (logBase 10 . (+1))