dr-cabal-0.2.0.0: src/DrCabal/Watch.hs
{-# LANGUAGE NumericUnderscores #-}
{- |
Module : DrCabal.Watch
Copyright : (c) 2019 Alexander Gugel
(c) 2022 Dmitrii Kovanikov
SPDX-License-Identifier : MPL-2.0
Maintainer : Dmitrii Kovanikov <kovanikov@gmail.com>
Stability : Experimental
Portability : Portable
Watch the output of the @cabal build@ command and update the profile
chart interactively.
-}
module DrCabal.Watch
( watchBuild
) where
import Colourista.Short (b)
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently)
import GHC.Clock (getMonotonicTimeNSec)
import System.IO (isEOF)
import DrCabal.Model (Entry (..), Line (..), parseLine)
import DrCabal.Terminal (clearScreen)
import qualified Colourista
import qualified Data.ByteString as ByteString
{- | Watch build entries from @stdin@ and interactively update the
chart and current status.
-}
watchBuild
:: ([Entry] -> Text)
-- ^ A function to draw chart
-> IO [Entry]
-- ^ Returns the final list of entries
watchBuild drawChart = do
inputActionRef <- newIORef [Start]
(entries, _) <- concurrently
(interactiveWorker inputActionRef drawChart)
(stdinReaderWorker inputActionRef)
pure entries
stdinReaderWorker :: IORef [InputAction] -> IO ()
stdinReaderWorker inputActionRef = go
where
go :: IO ()
go = do
isEndOfInput <- isEOF
if isEndOfInput
then do
pushAction inputActionRef End
else do
time <- getMonotonicTimeNSec
line <- ByteString.getLine
let ln = Line time line
-- output line to the watch worker for output redirection
pushAction inputActionRef $ Consume ln
go
-- | Action returned by the @stdinReaderWorker@.
data InputAction
-- | Produce the initial message
= Start
-- | Line content read from @stdin@ with timestamp
| Consume Line
-- | EOF reached for @stdin@
| End
-- | Add 'InputAction' to end of the queue in the given 'IORef'.
pushAction :: IORef [InputAction] -> InputAction -> IO ()
pushAction inputActionRef action =
atomicModifyIORef' inputActionRef $ \actions -> (actions ++ [action], ())
data InteractiveCommand
-- | Initial message
= Greeting
-- | New line received from the @stdinReaderWorker@. Update the chart.
| UpdateChart Line
-- | No new lines from @stdin@. Simply wait and update the spinner.
| Wait
-- | Finished reading lines from @stdin@
| Finish
{- | Produce the next 'InteractiveCommand' by reading the current
'InputAction' and removing it from the queue.
-}
nextCommand :: IORef [InputAction] -> IO InteractiveCommand
nextCommand inputActionRef = atomicModifyIORef' inputActionRef popAction
where
popAction :: [InputAction] -> ([InputAction], InteractiveCommand)
popAction [] = ([], Wait)
popAction (x : xs) = case x of
Start -> (xs, Greeting)
Consume l -> (xs, UpdateChart l)
End -> ([], Finish)
-- | A data type
data Output = Output
{ outputCabalLog :: Text
, outputEntries :: [Entry]
}
interactiveWorker
:: IORef [InputAction]
-- ^ Mutable reference to the queue of input actions
-> ([Entry] -> Text)
-- ^ A function to draw the chart
-> IO [Entry]
interactiveWorker inputActionRef drawChart =
go (Output "Profiling 'cabal build' interactively..." []) (cycle spinnerFrames)
where
spinnerFrames :: [Text]
spinnerFrames =
[ "⠋"
, "⠙"
, "⠹"
, "⠸"
, "⠼"
, "⠴"
, "⠦"
, "⠧"
, "⠇"
, "⠏"
]
go :: Output -> [Text] -> IO [Entry]
go _ [] = do
Colourista.errorMessage $
"Panic! At the 'dr-cabal'! Impossible happened: list of frames is empty"
exitFailure
go prevOutput (frame : frames) = do
command <- nextCommand inputActionRef
case command of
Greeting -> do
printOutput frame prevOutput prevOutput
go prevOutput (frame : frames)
UpdateChart line@Line{..} -> case parseLine line of
Nothing -> do
let newOutput = prevOutput { outputCabalLog = decodeUtf8 lineLine }
printOutput frame prevOutput newOutput
go newOutput frames
Just entry -> do
let newOutput = Output
{ outputCabalLog = decodeUtf8 lineLine
, outputEntries = outputEntries prevOutput ++ [entry]
}
printOutput frame prevOutput newOutput
go newOutput frames
Wait -> do
printOutput frame prevOutput prevOutput
go prevOutput frames
Finish -> do
putTextLn $ b "Build finished successfully!"
pure $ outputEntries prevOutput
printOutput :: Text -> Output -> Output -> IO ()
printOutput frame oldOutput newOutput = do
clearPreviousOutput oldOutput
putText $ fmtOutput frame newOutput
hFlush stdout
threadDelay 80_000 -- wait 80 ms to update spinner
clearPreviousOutput :: Output -> IO ()
clearPreviousOutput output = do
let fakeFrame = ""
let screenHeight = length $ lines $ fmtOutput fakeFrame output
clearScreen screenHeight
fmtOutput :: Text -> Output -> Text
fmtOutput frame Output{..} =
chart <> log
where
chart :: Text
chart = case outputEntries of
[] -> ""
_ -> drawChart outputEntries
log :: Text
log = frame <> " " <> outputCabalLog