dojang-0.2.0: src/Dojang/Commands.hs
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Dojang.Commands
( Admonition (..)
, Color (..)
, codeStyleFor
, colorFor
, die'
, dieWithErrors
, pathStyleFor
, pathStyleFor'
, printStderr
, printStderr'
, printTable
) where
import Control.Monad (forM_)
import Control.Monad.IO.Class (MonadIO (liftIO))
import System.Environment (lookupEnv)
import System.Exit (ExitCode, exitWith)
import System.IO.Extra (Handle, hIsTerminalDevice, stderr, stdout)
import System.IO.Unsafe (unsafePerformIO)
import Prelude hiding (putStr, replicate)
import Data.List (transpose)
import Data.Text (Text, length, pack, replicate)
import Data.Text.IO (hPutStr, hPutStrLn, putStr)
import System.Console.Pretty
( Color (..)
, Pretty
, Style (..)
, bgColor
, style
)
import System.Console.Pretty qualified (color)
import System.OsPath (OsPath, decodeFS)
import TextShow (FromStringShow (FromStringShow), TextShow (showt))
isColorAvailable :: (MonadIO m) => Handle -> m Bool
isColorAvailable handle = liftIO $ do
term <- lookupEnv "TERM"
noColor <- lookupEnv "NO_COLOR"
case (term, noColor) of
(Just "dumb", _) -> return False
(_, Just (_ : _)) -> return False
_ -> hIsTerminalDevice handle
colorFor
:: forall a m. (Pretty a, MonadIO m) => Handle -> m (Color -> Color -> a -> a)
colorFor handle = do
colorAvailable <- isColorAvailable handle
return $ if colorAvailable then color else dumb
where
dumb :: Color -> Color -> a -> a
dumb _ _ = id
color :: Color -> Color -> a -> a
color bg text v = bgColor bg $ System.Console.Pretty.color text v
codeStyleFor :: forall a m. (Pretty a, MonadIO m) => Handle -> m (a -> a)
codeStyleFor handle = do
colorAvailable <- isColorAvailable handle
return $
if colorAvailable
then style Bold
else id
pathStyleFor :: forall m. (MonadIO m) => Handle -> m (OsPath -> Text)
pathStyleFor handle = do
pathStyle <- pathStyleFor' handle
return $ \path -> pathStyle $ decodeFS' path
where
decodeFS' :: OsPath -> Text
decodeFS' = pack . unsafePerformIO . decodeFS
pathStyleFor' :: forall a m. (Pretty a, MonadIO m) => Handle -> m (a -> a)
pathStyleFor' handle = do
colorAvailable <- isColorAvailable handle
return $
if colorAvailable
then style Italic
else id
printStderr :: (MonadIO m) => Text -> m ()
printStderr = liftIO . hPutStrLn stderr
data Admonition = Hint | Note | Warning | Error
deriving (Show, Eq, Ord, Enum, Bounded)
printStderr' :: (MonadIO m) => Admonition -> Text -> m ()
printStderr' admonition message = do
color <- colorFor stderr
printStderr $ color Default color' prefix <> message
where
color' :: Color
color' = case admonition of
Hint -> Cyan
Note -> Yellow
Warning -> Yellow
Error -> Red
prefix :: Text
prefix = showt (FromStringShow admonition) <> ": "
die' :: (MonadIO m) => ExitCode -> Text -> m a
die' exitCode message = do
printStderr' Error message
liftIO $ exitWith exitCode
dieWithErrors :: (MonadIO m) => ExitCode -> [Text] -> m a
dieWithErrors exitCode errors = do
forM_ errors $ printStderr' Error
liftIO $ exitWith exitCode
printTable :: forall m. (MonadIO m) => [Text] -> [[(Color, Text)]] -> m ()
printTable headers rows = do
-- Headers are printed to stderr, so that they can be piped to another
-- program without interfering with the table.
forM_ (zip headers columnWidths) $ \(header, width) -> do
putCol stderr Cyan header width
liftIO $ hPutStr stderr " "
putLnStderr
forM_ columnWidths $ \width -> do
putCol stderr Cyan (replicate width "\x2500") width
liftIO $ hPutStr stderr " "
putLnStderr
forM_ rows $ \row -> do
forM_ (zip3 row columnWidths [1 ..]) $ \((color', value), width, i) -> do
putCol stdout color' value $
if i < Prelude.length row then width else Data.Text.length value
liftIO $ putStr " "
putLn
where
valueWidths :: [[Int]]
valueWidths =
[Data.Text.length c | c <- headers]
: [[Data.Text.length v | (_, v) <- row] | row <- rows]
columnWidths :: [Int]
columnWidths = map maximum $ transpose valueWidths
putCol :: Handle -> Color -> Text -> Int -> m ()
putCol h color' value width = do
color <- colorFor stdout
liftIO $ hPutStr h $ color Default color' value
liftIO $ hPutStr h $ replicate (width - Data.Text.length value) " "
putLn :: m ()
putLn = liftIO $ putStrLn mempty
putLnStderr :: m ()
putLnStderr = liftIO $ hPutStrLn stderr mempty