1{-# LANGUAGE ConstraintKinds #-}
2{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE LambdaCase #-}
5{-# LANGUAGE MultiParamTypeClasses #-}
6{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE RankNTypes #-}
8{-# LANGUAGE ScopedTypeVariables #-}
9-- | Provides a logging handler that facilitates safe ouputting to terminal using MVar based locking.
10-- | Spinner.hs and Process.hs work on this guarantee.
11module Obelisk.CliApp.Logging
12 ( AsUnstructuredError (..)
26 , allowUserToMakeLoggingVerbose
31import Control.Concurrent (ThreadId, forkIO, killThread, threadDelay)
32import Control.Concurrent.MVar (modifyMVar_, newMVar)
33import Control.Lens (Prism', review)
34import Control.Monad (unless, void, when)
35import Control.Monad.Catch (MonadCatch, MonadMask, bracket, catch, throwM)
36import Control.Monad.Except (throwError)
37import Control.Monad.IO.Class (liftIO)
38import Control.Monad.Log (Severity (..), WithSeverity (..), logMessage, runLoggingT)
39import Control.Monad.Loops (iterateUntil)
40import Control.Monad.Reader (MonadIO, ReaderT (..))
41import Data.IORef (atomicModifyIORef', newIORef, readIORef, writeIORef)
43import Data.Semigroup ((<>))
44import Data.Text (Text)
45import qualified Data.Text as T
46import qualified Data.Text.IO as T
47import GHC.IO.Encoding.Types
48import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
49 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
50 SGR (SetColor, SetConsoleIntensity), clearLine)
51import System.Exit (ExitCode (..))
54import qualified Obelisk.CliApp.TerminalString as TS
55import Obelisk.CliApp.Theme
56import Obelisk.CliApp.Types
64newCliConfig sev noColor noSpinner errorLogExitCode = do
67 tipDisplayed <- newIORef False
68 stack <- newIORef ([], [])
69 textEncoding <- hGetEncoding stdout
70 let theme = if fromMaybe False $ supportsUnicode <$> textEncoding
73 return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode theme
75runCli :: MonadIO m => CliConfig e -> CliT e m a -> m a
77 flip runLoggingT (handleLog c)
78 . flip runReaderT (_cliConfig_errorLogExitCode c)
83verboseLogLevel :: Severity
84verboseLogLevel = Debug
86isOverwrite :: Output -> Bool
88 Output_Overwrite _ -> True
91getSeverity :: Output -> Maybe Severity
93 Output_Log (WithSeverity sev _) -> Just sev
94 Output_LogRaw (WithSeverity sev _) -> Just sev
97getLogLevel :: (MonadIO m, HasCliConfig e m) => m Severity
98getLogLevel = getLogLevel' =<< getCliConfig
100getLogLevel' :: MonadIO m => CliConfig e -> m Severity
101getLogLevel' = liftIO . readIORef . _cliConfig_logLevel
103setLogLevel :: (MonadIO m, HasCliConfig e m) => Severity -> m ()
105 l <- _cliConfig_logLevel <$> getCliConfig
106 liftIO $ writeIORef l sev
108handleLog :: MonadIO m => CliConfig e -> Output -> m ()
109handleLog conf output = do
110 level <- getLogLevel' conf
111 liftIO $ modifyMVar_ (_cliConfig_lock conf) $ \wasOverwriting -> do
112 let noColor = _cliConfig_noColor conf
113 case getSeverity output of
114 Nothing -> handleLog' noColor output
115 Just sev -> if sev > level
116 then return wasOverwriting -- Discard if sev is above configured log level
118 -- If the last output was an overwrite (with cursor on same line), ...
119 when wasOverwriting $
120 void $ handleLog' noColor Output_ClearLine -- first clear it,
121 handleLog' noColor output -- then, actually write the msg.
123handleLog' :: MonadIO m => Bool -> Output -> m Bool
124handleLog' noColor output = do
126 Output_Log m -> liftIO $ do
127 writeLog True noColor m
128 Output_LogRaw m -> liftIO $ do
129 writeLog False noColor m
130 hFlush stdout -- Explicitly flush, as there is no newline
131 Output_Write ts -> liftIO $ do
132 T.putStrLn $ TS.render (not noColor) Nothing ts
134 Output_Overwrite ts -> liftIO $ do
135 width <- TS.getTerminalWidth
136 T.putStr $ "\r" <> (TS.render (not noColor) width ts)
138 Output_ClearLine -> liftIO $ do
139 -- Go to the first column and clear the whole line
143 return $ isOverwrite output
145-- | Like `putLog` but without the implicit newline added.
146putLogRaw :: CliLog m => Severity -> Text -> m ()
147putLogRaw sev = logMessage . Output_LogRaw . WithSeverity sev
149-- | Indicates unstructured errors form one variant (or conceptual projection)
152-- Shouldn't really use this, but who has time to clean up that much!
153class AsUnstructuredError e where
154 asUnstructuredError :: Prism' e Text
156instance AsUnstructuredError Text where
157 asUnstructuredError = id
159-- | Like `putLog Alert` but also abrupts the program.
160failWith :: (CliThrow e m, AsUnstructuredError e) => Text -> m a
161failWith = throwError . review asUnstructuredError
164 :: (HasCliConfig e m, CliThrow e m, CliLog m)
168 putLog Warning $ fst $ _cliConfig_errorLogExitCode c e
170-- | Intercept ExitFailure exceptions and log the given alert before exiting.
172-- This is useful when you want to provide contextual information to a deeper failure.
173withExitFailMessage :: (CliLog m, MonadCatch m) => Text -> m a -> m a
174withExitFailMessage msg f = f `catch` \(e :: ExitCode) -> do
176 ExitFailure _ -> putLog Alert msg
177 ExitSuccess -> pure ()
180-- | Write log to stdout, with colors (unless `noColor`)
181writeLog :: (MonadIO m, MonadMask m) => Bool -> Bool -> WithSeverity Text -> m ()
182writeLog withNewLine noColor (WithSeverity severity s)
183 | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
184 | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
185 | not noColor && severity <= Warning = TS.putStrWithSGR warningColors h withNewLine s
186 | not noColor && severity >= Debug = TS.putStrWithSGR debugColors h withNewLine s
187 | otherwise = liftIO $ putFn s
189 putFn = if withNewLine then (T.hPutStrLn h) else (T.hPutStr h)
190 h = if severity <= Error then stderr else stdout
191 errorColors = [SetColor Foreground Vivid Red]
192 warningColors = [SetColor Foreground Vivid Yellow]
193 debugColors = [SetConsoleIntensity FaintIntensity]
195-- | Allow the user to immediately switch to verbose logging upon pressing a particular key.
197-- Call this function in a thread, and kill it to turn off keystroke monitoring.
198allowUserToMakeLoggingVerbose
199 :: (MonadIO m, MonadMask m, CliLog m, HasCliConfig e m)
200 => String -- ^ The key to press in order to make logging verbose
202allowUserToMakeLoggingVerbose keyCode = bracket showTip (liftIO . killThread) $ \_ -> do
204 liftIO $ hSetBuffering stdin NoBuffering
205 _ <- iterateUntil (== keyCode) $ liftIO getChars
206 putLog Warning "Ctrl+e pressed; making output verbose (-v)"
207 setLogLevel verboseLogLevel
209 showTip = fork $ unlessVerbose $ do
211 liftIO $ threadDelay $ 10*1000000 -- Only show tip for actions taking too long (10 seconds or more)
212 tipDisplayed <- liftIO $ atomicModifyIORef' (_cliConfig_tipDisplayed conf) $ (,) True
213 unless tipDisplayed $ unlessVerbose $ do -- Check again in case the user had pressed Ctrl+e recently
214 putLog Notice "Tip: Press Ctrl+e to display full output"
217 unless (l == verboseLogLevel) f
219-- | Like `getChar` but also retrieves the subsequently pressed keys.
221-- Allowing, for example, the ↑ key, which consists of the three characters
222-- ['\ESC','[','A'] to be distinguished from an actual \ESC character input.
224getChars = reverse <$> f mempty
228 hReady stdin >>= \case
230 False -> return (x:xs)
232fork :: (HasCliConfig e m, MonadIO m) => CliT e IO () -> m ThreadId
235 liftIO $ forkIO $ runCli c f
237-- | Conservatively determines whether the encoding supports Unicode.
239-- Currently this uses a whitelist of known-to-work encodings. In principle it
240-- could test dynamically by opening a file with this encoding, but it doesn't
241-- look like base exposes any way to determine this in a pure fashion.
242supportsUnicode :: TextEncoding -> Bool
243supportsUnicode enc = any ((textEncodingName enc ==) . textEncodingName)
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