1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE LambdaCase #-}
3{-# LANGUAGE MultiParamTypeClasses #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE TupleSections #-}
7-- | Provides a simple CLI spinner that interoperates cleanly with the rest of the logging output.
8module Obelisk.CliApp.Spinner
14import Control.Concurrent (killThread, threadDelay)
15import Control.Monad (forM_, (>=>))
16import Control.Monad.Catch (MonadMask, mask, onException)
17import Control.Monad.IO.Class
18import Control.Monad.Log (Severity (..), logMessage)
20import qualified Data.List as L
21import Data.Maybe (isNothing)
22import Data.Text (Text)
23import System.Console.ANSI (Color (Blue, Cyan, Green, Red))
25import Obelisk.CliApp.Logging (allowUserToMakeLoggingVerbose, fork, putLog)
26import Obelisk.CliApp.TerminalString (TerminalString (..), enquiryCode)
27import Obelisk.CliApp.Theme
28import Obelisk.CliApp.Types (CliLog, CliConfig (..), HasCliConfig, Output (..), getCliConfig)
30-- | Run an action with a CLI spinner.
32 :: (MonadIO m, MonadMask m, CliLog m, HasCliConfig e m)
34withSpinner s = withSpinner' s $ Just $ const s
36-- | A spinner that leaves no trail after a successful run.
38-- Use if you wish the spinner to be ephemerally visible to the user.
40-- The 'no trail' property automatically carries over to sub-spinners (in that
41-- they won't leave a trail either).
43 :: (MonadIO m, MonadMask m, CliLog m, HasCliConfig e m)
45withSpinnerNoTrail s = withSpinner' s Nothing
47-- | Advanced version that controls the display and content of the trail message.
49 :: (MonadIO m, MonadMask m, CliLog m, HasCliConfig e m)
51 -> Maybe (a -> Text) -- ^ Leave an optional trail with the given message creator
54withSpinner' msg mkTrail action = do
55 cliConf <- getCliConfig
56 let noSpinner = _cliConfig_noSpinner cliConf
58 then putLog Notice msg >> action
59 else bracket' run cleanup $ const action
62 -- Add this log to the spinner stack, and start a spinner if it is top-level.
63 modifyStack pushSpinner >>= \case
64 True -> do -- Top-level spinner; fork a thread to manage output of anything on the stack
65 ctrleThread <- fork $ allowUserToMakeLoggingVerbose enquiryCode
66 cliConf <- getCliConfig
67 let theme = _cliConfig_theme cliConf
68 spinner = coloredSpinner $ _cliTheme_spinner theme
69 spinnerThread <- fork $ runSpinner spinner $ \c -> do
70 logs <- renderSpinnerStack theme c . snd <$> readStack
71 logMessage $ Output_Overwrite logs
72 pure [ctrleThread, spinnerThread]
73 False -> -- Sub-spinner; nothing to do.
75 cleanup tids resultM = do
76 liftIO $ mapM_ killThread tids
77 logMessage Output_ClearLine
78 cliConf <- getCliConfig
79 let theme = _cliConfig_theme cliConf
80 logsM <- modifyStack $ (popSpinner theme) $ case resultM of
82 ( TerminalString_Colorized Red $ _cliTheme_failed $ _cliConfig_theme cliConf
83 , Just msg -- Always display final message if there was an exception.
86 ( TerminalString_Colorized Green $ _cliTheme_done $ _cliConfig_theme cliConf
87 , mkTrail <*> pure result
89 -- Last message, finish off with newline.
90 forM_ logsM $ logMessage . Output_Write
91 pushSpinner (flag, old) =
92 ( (isTemporary : flag, TerminalString_Normal msg : old)
93 , null old -- Is empty?
96 isTemporary = isNothing mkTrail
97 popSpinner theme (mark, trailMsgM) (flag, old) =
99 -- With final trail spinner message to render
100 , renderSpinnerStack theme mark . (: new) . TerminalString_Normal <$> (
101 if inTemporarySpinner then Nothing else trailMsgM
105 inTemporarySpinner = or newFlag -- One of our parent spinners is temporary
106 newFlag = drop 1 flag
107 new = L.delete (TerminalString_Normal msg) old
108 readStack = liftIO . readIORef
109 =<< fmap _cliConfig_spinnerStack getCliConfig
110 modifyStack f = liftIO . flip atomicModifyIORef' f
111 =<< fmap _cliConfig_spinnerStack getCliConfig
113-- | How nested spinner logs should be displayed
116 -> TerminalString -- ^ That which comes before the final element in stack
117 -> [TerminalString] -- ^ Spinner elements in reverse order
119renderSpinnerStack theme mark = L.intersperse space . go . L.reverse
122 go (x:[]) = mark : [x]
123 go (x:xs) = arrow : x : go xs
124 arrow = TerminalString_Colorized Blue $ _cliTheme_arrow theme
125 space = TerminalString_Normal " "
127-- | A spinner is simply an infinite list of strings that supplant each other in a delayed loop, creating the
128-- animation of a "spinner".
129type Spinner = [TerminalString]
131coloredSpinner :: SpinnerTheme -> Spinner
132coloredSpinner = cycle . fmap (TerminalString_Colorized Cyan)
134-- | Run a spinner with a monadic function that defines how to represent the individual spinner characters.
135runSpinner :: MonadIO m => Spinner -> (TerminalString -> m ()) -> m ()
136runSpinner spinner f = forM_ spinner $ f >=> const delay
138 delay = liftIO $ threadDelay 100000 -- A shorter delay ensures that we update promptly.
140-- | Like `bracket` but the `release` function can know whether an exception was raised
141bracket' :: MonadMask m => m a -> (a -> Maybe c -> m b) -> (a -> m c) -> m c
142bracket' acquire release use = mask $ \unmasked -> do
144 result <- unmasked (use resource) `onException` release resource Nothing
145 _ <- release resource $ Just result
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list