termplot (empty) → 0.1.0.0
raw patch · 5 files changed
+221/−0 lines, 5 filesdep +basedep +brickdep +data-defaultsetup-changed
Dependencies added: base, brick, data-default, optparse-applicative, process, split, time-units, transformers, unix, vty
Files
- LICENSE +20/−0
- Main.hs +156/−0
- README +14/−0
- Setup.hs +2/−0
- termplot.cabal +29/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2015 Ricardo Catalinas Jiménez++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Main.hs view
@@ -0,0 +1,156 @@+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))
+ README view
@@ -0,0 +1,14 @@+Usage+-----+$ termplot \+ up1:'uptime | awk "{ print \$8 }"' \+ mem:'free | awk "/Mem:/ { print \$3 }"' \+ wlan0:'iw dev wlan0 link | awk "/bitrate/ { print \$3 }"'++up1 0.0 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄+mem 150464.0 ▆▇▇██▇▇▇▆▇▇▆▆▆▅▆▆▅▅▅▅▅▇▇▆▅▅▅▄▃▄▂▂+wlan0 43.3 ▆▆▆▆▆▆▆▆▆▆█▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▄▄ ▄▆▆▄++++ Inspired by: https://github.com/holman/spark++ Powered by the awesome library: https://hackage.haskell.org/package/brick
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ termplot.cabal view
@@ -0,0 +1,29 @@+name: termplot+version: 0.1.0.0+synopsis: Plot time series in your terminal using commands stdout+description: Use unicode characters to plot fancy time series in real-time in your terminal.+homepage: https://github.com/jimenezrick/termplot+license: MIT+license-file: LICENSE+author: Ricardo Catalinas Jiménez <r@untroubled.be>+maintainer: Ricardo Catalinas Jiménez <r@untroubled.be>+copyright: Copyright (c) 2015 Ricardo Catalinas Jiménez+category: Console+build-type: Simple+extra-source-files: README+cabal-version: >=1.10++executable termplot+ default-language: Haskell2010+ ghc-options: -Wall -threaded+ main-is: Main.hs+ build-depends: base >=4.8 && <4.9+ , time-units >=1.0 && <1.1+ , transformers >=0.4 && <0.5+ , data-default >=0.5 && <0.6+ , vty >=5.4 && <5.5+ , brick >=0.2 && <0.3+ , optparse-applicative >=0.12 && <0.13+ , process >=1.2 && <1.3+ , unix >=2.7 && <2.8+ , split >=0.2 && <0.3