packages feed

dr-cabal-0.0.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

Implementation of the @dr-cabal watch@ command.
-}

module DrCabal.Watch
    ( runWatch
    ) where

import Colourista.Short (b)
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (wait, withAsync)
import Data.Aeson.Encode.Pretty (encodePretty)
import GHC.Clock (getMonotonicTimeNSec)
import System.Console.ANSI (clearLine, setCursorColumn)
import System.IO (isEOF)

import DrCabal.Cli (WatchArgs (..))
import DrCabal.Model (Entry (..), Line (..))

import qualified Colourista
import qualified Data.ByteString as ByteString
import qualified Data.Text as Text


runWatch :: WatchArgs -> IO ()
runWatch WatchArgs{..} = do
    watchRef <- newIORef [Start]

    withAsync (watchWorker watchRef) $ \workerAsync -> do
        readFromStdin watchRef watchArgsOutput
        wait workerAsync

readFromStdin :: IORef [WatchAction] -> FilePath -> IO ()
readFromStdin watchRef outputPath = go []
  where
    go :: [Line] -> IO ()
    go cabalOutput = do
        isEndOfInput <- isEOF
        if isEndOfInput
        then do
            pushAction watchRef $ End outputPath cabalOutput
        else do
            time <- getMonotonicTimeNSec
            line <- ByteString.getLine

            -- output line to the watch worker for output redirection
            pushAction watchRef $ Consume line

            go $ Line time line : cabalOutput

linesToEntries :: [Line] -> [Entry]
linesToEntries = mapMaybe parseLine . reverse

parseLine :: Line -> Maybe Entry
parseLine Line{..} = do
    let txtLine = decodeUtf8 lineLine
    txtStatus : library : _ <- Just $ words txtLine

    -- parse status string to the 'Status' type
    status <- readMaybe $ toString txtStatus

    -- check if this line is a library: '-' separates library name and its version
    guard $ Text.elem '-' library

    pure $ Entry
        { entryStatus  = status
        , entryStart   = lineTime
        , entryLibrary = library
        }

data WatchAction
    = Start
    | Consume ByteString
    | End FilePath [Line]

-- | Add 'WatchAction' to end of the list
pushAction :: IORef [WatchAction] -> WatchAction -> IO ()
pushAction watchRef action =
    atomicModifyIORef' watchRef $ \actions -> (actions ++ [action], ())

data WorkerCommand
    = Greeting
    | WriteLine ByteString
    | Wait
    | Finish FilePath [Line]

watchWorker :: IORef [WatchAction] -> IO ()
watchWorker watchRef = go "Watching build output" (cycle spinnerFrames)
  where
    spinnerFrames :: [Text]
    spinnerFrames =
        [ "⠋"
        , "⠙"
        , "⠹"
        , "⠸"
        , "⠼"
        , "⠴"
        , "⠦"
        , "⠧"
        , "⠇"
        , "⠏"
        ]

    go :: Text -> [Text] -> IO ()
    go _ [] = do
        Colourista.errorMessage $
            "Panic! At the 'dr-cabal'! Impossible happened: list of frames is empty"
        exitFailure
    go prevLine (frame : frames) = do
        command <- atomicModifyIORef' watchRef popAction
        case command of
            Greeting -> do
                Colourista.formattedMessage
                    [Colourista.blue, Colourista.bold]
                    "Watching cabal output..."

                go prevLine (frame : frames)
            WriteLine line -> do
                resetLine
                let l = decodeUtf8 line
                putText $ frame <> " " <> l
                hFlush stdout
                threadDelay 80_000  -- wait 80 ms to update spinner
                go l frames
            Wait -> do
                resetLine
                putText $ frame <> " " <> prevLine
                hFlush stdout
                threadDelay 80_000  -- wait 80 ms to update spinner
                go prevLine frames
            Finish outputPath lns -> do
                writeFileLBS outputPath $ encodePretty $ linesToEntries lns
                resetLine
                putTextLn $ unlines
                    [ b "Build finished successfully!"
                    , ""
                    , "To see the profiling output, run the following command:"
                    , ""
                    , "    dr-cabal profile --input=" <> toText outputPath
                    ]

    popAction :: [WatchAction] -> ([WatchAction], WorkerCommand)
    popAction [] = ([], Wait)
    popAction (x : xs) = case x of
        Start        -> (xs, Greeting)
        Consume l    -> (xs, WriteLine l)
        End path lns -> ([], Finish path lns)

    resetLine :: IO ()
    resetLine = do
        clearLine
        setCursorColumn 0