@@ -1,4 +1,5 @@
11{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE LambdaCase #-}
23{-# LANGUAGE MultiParamTypeClasses #-}
34{-# LANGUAGE OverloadedStrings #-}
45-- | Provides a simple CLI spinner that interoperates cleanly with the rest of the logging output.
@@ -7,27 +8,43 @@ module Obelisk.CLI.Spinner (withSpinner') where
78import Control.Concurrent (killThread, threadDelay)
89import Control.Monad (forM_, (>=>))
910import Control.Monad.Catch (MonadMask, mask, onException)
10−import Control.Monad.IO.Class (liftIO)
1111import Control.Monad.Log (MonadLog, logMessage)
1212import Control.Monad.Reader (MonadIO)
13+import Control.Monad.IO.Class
14+import Data.IORef
1315import Data.Text (Text)
1416import qualified Data.Text as T
1517import System.Console.ANSI (Color (Cyan, Green, Red), ColorIntensity (Vivid), ConsoleLayer (Foreground),
1618 SGR (Reset, SetColor), setSGRCode)
19+import Data.List (delete)
1720
18−import Obelisk.CLI.Logging (LoggingConfig, Output (..), allowUserToMakeLoggingVerbose, forkML)
21+import Obelisk.CLI.Logging (LoggingConfig(..), Output (..), allowUserToMakeLoggingVerbose, forkML)
1922
2023-- | Run an action with a CLI spinner.
2124withSpinner'
2225 :: (MonadIO m, MonadMask m, MonadLog Output m)
2326 => LoggingConfig -> Text -> m a -> m a
24−withSpinner' conf s = bracket' (sequence [run, forkML conf $ allowUserToMakeLoggingVerbose conf enquiryCode]) cleanup . const
27+withSpinner' conf s = bracket' start cleanup . const
2528 where
26− run = forkML conf $ do
27− runSpinner spinner $ \c -> do
28− logMessage $ Output_Overwrite [c, " ", T.unpack s]
29+ start = do
30+ tids <- run
31+ tid <- forkML conf $ allowUserToMakeLoggingVerbose conf enquiryCode
32+ pure $ tid : tids
33+ stack = _loggingConfig_stack conf
34+ run = do
35+ -- Add this log to the spinner stack
36+ wasEmpty <- liftIO $ atomicModifyIORef' stack $ \old -> (s : old, null old)
37+ if wasEmpty
38+ then do -- Fork a thread to manage output of anything on the stack
39+ tid <- forkML conf $ runSpinner spinner $ \c -> do
40+ logs <- liftIO $ concatStack <$> readIORef stack
41+ logMessage $ Output_Overwrite [c, " ", T.unpack logs]
42+ pure [tid]
43+ else pure []
2944 cleanup failed tids = do
3045 liftIO $ mapM_ killThread tids
46+ -- Delete this log from the spinner stack
47+ liftIO $ modifyIORef' stack $ delete s
3148 logMessage Output_ClearLine
3249 let mark = if failed
3350 then withColor Red "✖"
@@ -35,6 +52,10 @@ withSpinner' conf s = bracket' (sequence [run, forkML conf $ allowUserToMakeLogg
3552 logMessage $ Output_Overwrite [mark, " ", T.unpack s, "\n"]
3653 spinner = coloredSpinner defaultSpinnerTheme
3754
55+-- | How nested spinner logs should be displayed
56+concatStack :: [Text] -> Text
57+concatStack = T.intercalate " |> " . reverse
58+
3859-- | A spinner is simply an infinite list of strings that supplant each other in a delayed loop, creating the
3960-- animation of a "spinner".
4061type Spinner = [String]