{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Cli.Extras.Spinner
( withSpinner
, withSpinnerNoTrail
, withSpinner'
) where
import Control.Concurrent (forkIO, killThread, threadDelay)
import Control.Monad (forM_, (>=>))
import Control.Monad.Catch (MonadMask, mask, onException)
import Control.Monad.IO.Class
import Control.Monad.Log (Severity (..), logMessage)
import Data.IORef
import qualified Data.List as L
import Data.Maybe (isNothing)
import Data.Text (Text)
import System.Console.ANSI (Color (Blue, Cyan, Green, Red))
import Cli.Extras.Logging (allowUserToMakeLoggingVerbose, putLog, handleLog)
import Cli.Extras.TerminalString (TerminalString (..), enquiryCode)
import Cli.Extras.Theme
import Cli.Extras.Types (CliLog, CliConfig (..), HasCliConfig, Output (..), getCliConfig)
withSpinner
:: (MonadIO m, MonadMask m, CliLog m, HasCliConfig m)
=> Text -> m a -> m a
withSpinner s = withSpinner' s $ Just $ const s
withSpinnerNoTrail
:: (MonadIO m, MonadMask m, CliLog m, HasCliConfig m)
=> Text -> m a -> m a
withSpinnerNoTrail s = withSpinner' s Nothing
withSpinner'
:: (MonadIO m, MonadMask m, CliLog m, HasCliConfig m)
=> Text
-> Maybe (a -> Text)
-> m a
-> m a
withSpinner' msg mkTrail action = do
cliConf <- getCliConfig
let noSpinner = _cliConfig_noSpinner cliConf
if noSpinner
then putLog Notice msg >> action
else bracket' run cleanup $ const action
where
run = do
cliConf <- getCliConfig
modifyStack pushSpinner >>= \case
True -> do
ctrleThread <- liftIO $ forkIO $ allowUserToMakeLoggingVerbose cliConf enquiryCode
let theme = _cliConfig_theme cliConf
spinner = coloredSpinner $ _cliTheme_spinner theme
spinnerThread <- liftIO $ forkIO $ runSpinner spinner $ \c -> do
logs <- renderSpinnerStack theme c . snd <$> readIORef (_cliConfig_spinnerStack cliConf)
handleLog cliConf $ Output_Overwrite logs
pure [ctrleThread, spinnerThread]
False ->
pure []
cleanup tids resultM = do
liftIO $ mapM_ killThread tids
logMessage Output_ClearLine
cliConf <- getCliConfig
let theme = _cliConfig_theme cliConf
logsM <- modifyStack $ (popSpinner theme) $ case resultM of
Nothing ->
( TerminalString_Colorized Red $ _cliTheme_failed $ _cliConfig_theme cliConf
, Just msg
)
Just result ->
( TerminalString_Colorized Green $ _cliTheme_done $ _cliConfig_theme cliConf
, mkTrail <*> pure result
)
forM_ logsM $ logMessage . Output_Write
pushSpinner (flag, old) =
( (isTemporary : flag, TerminalString_Normal msg : old)
, null old
)
where
isTemporary = isNothing mkTrail
popSpinner theme (mark, trailMsgM) (flag, old) =
( (newFlag, new)
, renderSpinnerStack theme mark . (: new) . TerminalString_Normal <$> (
if inTemporarySpinner then Nothing else trailMsgM
)
)
where
inTemporarySpinner = or newFlag
newFlag = drop 1 flag
new = L.delete (TerminalString_Normal msg) old
modifyStack f = liftIO . flip atomicModifyIORef' f
=<< fmap _cliConfig_spinnerStack getCliConfig
renderSpinnerStack
:: CliTheme
-> TerminalString
-> [TerminalString]
-> [TerminalString]
renderSpinnerStack theme mark = L.intersperse space . go . L.reverse
where
go [] = []
go (x:[]) = mark : [x]
go (x:xs) = arrow : x : go xs
arrow = TerminalString_Colorized Blue $ _cliTheme_arrow theme
space = TerminalString_Normal " "
type Spinner = [TerminalString]
coloredSpinner :: SpinnerTheme -> Spinner
coloredSpinner = cycle . fmap (TerminalString_Colorized Cyan)
runSpinner :: MonadIO m => Spinner -> (TerminalString -> m ()) -> m ()
runSpinner spinner f = forM_ spinner $ f >=> const delay
where
delay = liftIO $ threadDelay 100000
bracket' :: MonadMask m => m a -> (a -> Maybe c -> m b) -> (a -> m c) -> m c
bracket' acquire release use = mask $ \unmasked -> do
resource <- acquire
result <- unmasked (use resource) `onException` release resource Nothing
_ <- release resource $ Just result
return result