cachix-1.7: src/Cachix/Client/Daemon/Progress.hs
module Cachix.Client.Daemon.Progress
( UploadProgress,
new,
complete,
fail,
tick,
update,
)
where
import Cachix.Client.Daemon.Types (PushRetryStatus (..))
import Cachix.Client.HumanSize (humanSize)
import qualified Control.Concurrent.Async as Async
import Control.Concurrent.MVar
import Data.String (String)
import qualified Data.Text as T
import Protolude
import qualified System.Console.AsciiProgress as Ascii
import qualified System.Console.AsciiProgress.Internal as Ascii.Internal
import System.Console.Pretty
import System.IO (hIsTerminalDevice)
data UploadProgress
= ProgressBar
{ path :: String,
size :: Int64,
progressBar :: Ascii.ProgressBar
}
| FallbackText
{ path :: String,
size :: Int64
}
new :: Handle -> String -> Int64 -> PushRetryStatus -> IO UploadProgress
new hdl path size retryStatus = do
isTerminal <- liftIO $ hIsTerminalDevice hdl
if isTerminal
then do
progressBar <- newProgressBar path size retryStatus
return $ ProgressBar {..}
else do
hPutStr stderr $ uploadStartFallback path size retryStatus
return $ FallbackText {..}
complete :: UploadProgress -> IO ()
complete ProgressBar {..} = Ascii.complete progressBar
complete _ = pure ()
tick :: UploadProgress -> Int64 -> IO ()
tick ProgressBar {..} deltaBytes = Ascii.tickN progressBar (fromIntegral deltaBytes)
tick _ _ = pure ()
fail :: UploadProgress -> IO ()
fail ProgressBar {..} =
clearAsciiWith progressBar (uploadFailed path)
fail FallbackText {path} =
hPutStr stderr $ uploadFailed path
update :: UploadProgress -> PushRetryStatus -> IO UploadProgress
update pg@ProgressBar {..} retryStatus = do
let opts = newProgressBarOptions path size retryStatus
pg' <- newAsciiProgressBarInPlace progressBar opts
return $ pg {progressBar = pg'}
update fbt _ = return fbt
newProgressBar :: String -> Int64 -> PushRetryStatus -> IO Ascii.ProgressBar
newProgressBar path size retryStatus =
newAsciiProgressBar $ newProgressBarOptions path size retryStatus
newProgressBarOptions :: String -> Int64 -> PushRetryStatus -> Ascii.Options
newProgressBarOptions path size retryStatus = do
let hSize = toS $ humanSize $ fromIntegral size
let barLength =
uploadTickBar path hSize retryStatus
& toS
& T.replace "[:bar]" ""
& T.replace ":percent" " 0%"
& T.length
Ascii.def
{ Ascii.pgTotal = fromIntegral size,
-- https://github.com/yamadapc/haskell-ascii-progress/issues/24
Ascii.pgWidth = 20 + barLength,
Ascii.pgOnCompletion = Just $ uploadComplete path size,
Ascii.pgFormat = uploadTickBar path hSize retryStatus
}
retryText :: PushRetryStatus -> String
retryText PushRetryStatus {retryCount} =
if retryCount == 0
then ""
else color Yellow $ "retry #" <> show retryCount <> " "
uploadComplete :: String -> Int64 -> String
uploadComplete path size =
let hSize = toS $ humanSize $ fromIntegral size
in color Green "✓ " <> path <> " (" <> hSize <> ")"
uploadFailed :: String -> String
uploadFailed path =
color Red "✗ " <> path
uploadTickBar :: String -> String -> PushRetryStatus -> String
uploadTickBar path hSize retryStatus =
color Blue "[:bar] " <> retryText retryStatus <> toS path <> " (:percent of " <> hSize <> ")"
uploadStartFallback :: String -> Int64 -> PushRetryStatus -> String
uploadStartFallback path size retryStatus =
let hSize = toS $ humanSize $ fromIntegral size
in retryText retryStatus <> "Pushing " <> path <> " (" <> hSize <> ")\n"
-- Internal
newAsciiProgressBar :: Ascii.Options -> IO Ascii.ProgressBar
newAsciiProgressBar opts = do
region <- Ascii.openConsoleRegion Ascii.Linear
newAsciiProgressBarWithRegion opts region
-- | Create a new progress bar in place of an existing one, reusing the console region.
newAsciiProgressBarInPlace :: Ascii.ProgressBar -> Ascii.Options -> IO Ascii.ProgressBar
newAsciiProgressBarInPlace pg opts = do
cancelUpdates pg
newAsciiProgressBarWithRegion opts (Ascii.pgRegion pg)
-- | Create a new progress bar using the provided console region.
newAsciiProgressBarWithRegion :: Ascii.Options -> Ascii.ConsoleRegion -> IO Ascii.ProgressBar
newAsciiProgressBarWithRegion opts region = do
info <- Ascii.Internal.newProgressBarInfo opts
-- Display initial progress-bar
pgStr <- Ascii.pgGetProgressStr opts opts <$> Ascii.Internal.getInfoStats info
Ascii.setConsoleRegion region pgStr
future <- Async.async $ start info
return $ Ascii.ProgressBar info future region
where
start info@Ascii.Internal.ProgressBarInfo {..} = do
c <- readMVar pgCompleted
unlessDone c $ do
n <- readChan pgChannel
_ <- handleMessage info n
unlessDone (c + n) $ start info
where
unlessDone c action | c < Ascii.pgTotal opts = action
unlessDone _ _ = do
let fmt = fromMaybe (Ascii.pgFormat opts) (Ascii.pgOnCompletion opts)
onCompletion <- Ascii.pgGetProgressStr opts opts {Ascii.pgFormat = fmt} <$> Ascii.Internal.getInfoStats info
Ascii.setConsoleRegion region onCompletion
handleMessage info n = do
-- Update the completed tick count
modifyMVar_ (Ascii.Internal.pgCompleted info) (\c -> return (c + n))
-- Find and update the current and first tick times:
stats <- Ascii.Internal.getInfoStats info
let progressStr = Ascii.Internal.pgGetProgressStr opts opts stats
Ascii.setConsoleRegion region progressStr
-- | Cancel async updates.
cancelUpdates :: Ascii.ProgressBar -> IO ()
cancelUpdates (Ascii.ProgressBar _ future _) = Async.cancel future
-- | Cancel async updates and clear the console region with the given string.
clearAsciiWith :: Ascii.ProgressBar -> String -> IO ()
clearAsciiWith pg@(Ascii.ProgressBar _ _ region) str = do
cancelUpdates pg
Ascii.setConsoleRegion region str