on master help sign in
Allow user to switch to verbose mode in runtime

When the spinner is running, hit `Ctrl+e` to immediately switch to verbose mode. This has the desired effect of displaying process output (eg: output of ongoing `nix-build` that is hitherto hidden) without having to re-run `ob` (to pass the `-v` flag).

commit46fd0b3c241095a0e987b451dcad3d95b0bf0b58 authorSridhar Ratnakumar <srid@srid.ca> dateTue 15 May 2018, 17:12 (8 years ago) parent57da4e4
4 files changed+71−19
lib/command/obelisk-command.cabal+1 lib/command/src/Obelisk/CLI/Logging.hs+46−5 lib/command/src/Obelisk/CLI/Spinner.hs+18−7 lib/command/src/Obelisk/Command.hs+6−7
lib/command/obelisk-command.cabal+1
@@ -24,6 +24,7 @@ library
2424 , io-streams
2525 , exceptions
2626 , logging-effect
27+ , monad-loops
2728 , mtl
2829 , network
2930 , network-uri
lib/command/src/Obelisk/CLI/Logging.hs+46−5
@@ -12,13 +12,17 @@ module Obelisk.CLI.Logging
1212 , failWith
1313 , putLog
1414 , putLogRaw
15+ , setLogLevelIfKeyPressed
1516 , handleLog
17+ , getLogLevel
18+ , newLoggingConfig
1619 ) where
1720
18−import Control.Concurrent.MVar (MVar, modifyMVar_)
19−import Control.Monad (void, when)
21+import Control.Concurrent.MVar (MVar, modifyMVar_, newMVar)
22+import Control.Monad (unless, void, when)
2023import Control.Monad.Catch (MonadMask, bracket_)
2124import Control.Monad.IO.Class (liftIO)
25+import Control.Monad.Loops (iterateUntil)
2226import Control.Monad.Reader (MonadIO)
2327import Data.Text (Text)
2428import qualified Data.Text.IO as T
@@ -26,16 +30,23 @@ import System.Console.ANSI (Color (Red, White, Yellow), ColorIntensity (Vivid),
2630 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
2731 SGR (Reset, SetColor, SetConsoleIntensity), clearLine, setSGR)
2832import System.Exit (ExitCode (ExitFailure), exitWith)
29−import System.IO (hFlush, stdout)
33+import System.IO (BufferMode (NoBuffering), hFlush, hReady, hSetBuffering, stdin, stdout)
3034
3135import Control.Monad.Log (MonadLog, Severity (..), WithSeverity (..), logMessage)
36+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
3237
3338data LoggingConfig = LoggingConfig
34− { _loggingConfig_level :: Severity
39+ { _loggingConfig_level :: IORef Severity -- We are capable of changing the log level at runtime
3540 , _loggingConfig_noColor :: Bool -- Disallow coloured output
3641 , _loggingConfig_lock :: MVar Bool -- Whether the last message was an Overwrite output
3742 }
3843
44+newLoggingConfig :: Severity -> Bool -> IO LoggingConfig
45+newLoggingConfig sev noColor = do
46+ level <- newIORef sev
47+ lock <- newMVar False
48+ return $ LoggingConfig level noColor lock
49+
3950data Output
4051 = Output_Log (WithSeverity Text) -- Regular logging message (with colors and newlines)
4152 | Output_LogRaw (WithSeverity Text) -- Like `Output_Log` but without the implicit newline added.
@@ -54,8 +65,24 @@ getSeverity = \case
5465 Output_LogRaw (WithSeverity sev _) -> Just sev
5566 _ -> Nothing
5667
68+setLogLevel :: MonadIO m => LoggingConfig -> Severity -> m ()
69+setLogLevel (LoggingConfig levelRef _ _) = liftIO . writeIORef levelRef
70+
71+getLogLevel :: MonadIO m => LoggingConfig -> m Severity
72+getLogLevel (LoggingConfig levelRef _ _) = liftIO $ readIORef levelRef
73+
74+setLogLevelIfKeyPressed :: (MonadIO m, MonadLog Output m) => LoggingConfig -> String -> Severity -> m () -> m ()
75+setLogLevelIfKeyPressed conf keyCode sev f = do
76+ logLevel <- getLogLevel conf
77+ unless (logLevel == sev) $ do
78+ liftIO $ hSetBuffering stdin NoBuffering
79+ _ <- iterateUntil (== keyCode) $ liftIO getChars
80+ f
81+ setLogLevel conf sev
82+
5783handleLog :: MonadIO m => LoggingConfig -> Output -> m ()
58−handleLog (LoggingConfig level noColor lock) output = do
84+handleLog (LoggingConfig levelRef noColor lock) output = do
85+ level <- liftIO $ readIORef levelRef
5986 liftIO $ modifyMVar_ lock $ \wasOverwriting -> do
6087 case getSeverity output of
6188 Nothing -> handleLog' noColor output
@@ -113,3 +140,17 @@ writeLogWith f noColor (WithSeverity severity s) = case sevColor severity of
113140 | sev <= Warning -> Just $ liftIO $ setSGR [SetColor Foreground Vivid Yellow]
114141 | sev >= Debug -> Just $ liftIO $ setSGR [SetColor Foreground Vivid White, SetConsoleIntensity FaintIntensity]
115142 | otherwise -> Nothing
143+
144+-- | Like `getChar` but also retrieves the subsequently pressed keys.
145+--
146+-- Allowing, for example, the ↑ key, which consists of the three characters
147+-- ['\ESC','[','A'] to be distinguished from an actual \ESC character input.
148+getChars :: IO String
149+getChars = reverse <$> f mempty
150+ where
151+ f xs = do
152+ x <- getChar
153+ hReady stdin >>= \case
154+ True -> f (x:xs )
155+ False -> return (x:xs)
156+
lib/command/src/Obelisk/CLI/Spinner.hs+18−7
@@ -4,6 +4,7 @@
44-- | Provides a simple CLI spinner that interoperates cleanly with the rest of the logging output.
55module Obelisk.CLI.Spinner (withSpinner') where
66
7+import Control.Applicative (liftA2)
78import Control.Concurrent (forkIO, killThread, threadDelay)
89import Control.Monad (forM_, (>=>))
910import Control.Monad.Catch (MonadMask, mask, onException)
@@ -15,25 +16,32 @@ import qualified Data.Text as T
1516import System.Console.ANSI (Color (Cyan, Green, Red), ColorIntensity (Vivid), ConsoleLayer (Foreground),
1617 SGR (Reset, SetColor), setSGRCode)
1718
18−import Obelisk.CLI.Logging (LoggingConfig, Output (..), handleLog)
19+import Obelisk.CLI.Logging (LoggingConfig, Output (..), Severity (Debug, Warning), handleLog, putLog,
20+ setLogLevelIfKeyPressed)
1921
2022-- | Run an action with a CLI spinner.
2123withSpinner'
2224 :: (MonadIO m, MonadMask m, MonadLog Output m)
2325 => LoggingConfig -> Text -> m a -> m a
24−withSpinner' conf s = bracket' run cleanup . const
26+withSpinner' conf s = bracket' (liftA2 (,) run makeVerbose) cleanup . const
2527 where
26− -- TODO: Can we obviate passing LoggingConfig just to use forkIO?
27− run = liftIO $ forkIO $ flip runLoggingT (handleLog conf) $ do
28− runSpinner (coloredSpinner defaultSpinnerTheme) $ \c -> do
28+ run = fork $ do
29+ runSpinner spinner $ \c -> do
2930 logMessage $ Output_Overwrite [c, " ", T.unpack s]
30− cleanup failed tid = do
31− liftIO $ killThread tid
31+ makeVerbose = fork $ do
32+ setLogLevelIfKeyPressed conf enquiryCode Debug $ do
33+ putLog Warning $ "Ctrl+e pressed; making output verbose (-v)"
34+ cleanup failed (runThread, toggleThread) = do
35+ liftIO $ mapM_ killThread [runThread, toggleThread]
3236 logMessage Output_ClearLine
3337 let mark = if failed
3438 then withColor Red "✖"
3539 else withColor Green "✔"
3640 logMessage $ Output_Overwrite [mark, " ", T.unpack s, "\n"]
41+ spinner = coloredSpinner defaultSpinnerTheme
42+ -- TODO: Can we obviate passing LoggingConfig just to use forkIO?
43+ fork = liftIO . forkIO . flip runLoggingT (handleLog conf)
44+
3745
3846-- | A spinner is simply an infinite list of strings that supplant each other in a delayed loop, creating the
3947-- animation of a "spinner".
@@ -75,3 +83,6 @@ withColor color s = mconcat
7583 , setSGRCode [Reset]
7684 ]
7785
86+-- | Code for https://en.wikipedia.org/wiki/Enquiry_character
87+enquiryCode :: String
88+enquiryCode = "\ENQ"
lib/command/src/Obelisk/Command.hs+6−7
@@ -7,7 +7,6 @@
77{-# LANGUAGE TypeApplications #-}
88module Obelisk.Command where
99
10−import Control.Concurrent (newMVar)
1110import Control.Monad
1211import Control.Monad.IO.Class (liftIO)
1312import Control.Monad.Reader (ask)
@@ -29,8 +28,7 @@ import System.IO (hIsTerminalDevice, stdout)
2928import System.Posix.Process (executeFile)
3029
3130import Obelisk.App
32−import Obelisk.CLI (LoggingConfig (LoggingConfig, _loggingConfig_level), Severity (..), cliDemo, failWith,
33− putLog)
31+import Obelisk.CLI (Severity (..), cliDemo, failWith, getLogLevel, newLoggingConfig, putLog)
3432import Obelisk.Command.Deploy
3533import Obelisk.Command.Project
3634import Obelisk.Command.Repl
@@ -209,12 +207,12 @@ isInteractiveTerm = do
209207
210208mkObeliskConfig :: IO Obelisk
211209mkObeliskConfig = do
212− logLevel <- getLogLevel <$> getObArgs
213210 notInteractive <- not <$> isInteractiveTerm
214− loggingConfig <- LoggingConfig logLevel notInteractive <$> newMVar False
211+ logLevel <- toLogLevel <$> getObArgs
212+ loggingConfig <- newLoggingConfig logLevel notInteractive
215213 return $ Obelisk notInteractive loggingConfig
216214 where
217− getLogLevel = bool Notice Debug . _args_verbose
215+ toLogLevel = bool Notice Debug . _args_verbose
218216
219217getObArgs :: IO Args
220218getObArgs = getArgs >>= handleParseResult . execParserPure parserPrefs argsInfo
@@ -227,11 +225,12 @@ main' = do
227225 c <- ask
228226 obPath <- liftIO getExecutablePath
229227 obArgs <- liftIO getObArgs
228+ logLevel <- liftIO $ getLogLevel $ _obelisk_logging c
230229 putLog Debug $ T.pack $ unwords
231230 [ "Starting Obelisk <" <> obPath <> ">"
232231 , "args=" <> show obArgs
233232 , "noSpinner=" <> show (_obelisk_noSpinner c)
234− , "logging-level=" <> show (_loggingConfig_level $ _obelisk_logging c)
233+ , "logging-level=" <> show logLevel
235234 ]
236235
237236 --TODO: We'd like to actually use the parser to determine whether to hand off,