packages feed

fourmolu-0.16.0.0: src/Ormolu/Terminal.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

-- | An abstraction for colorful output in terminal.
module Ormolu.Terminal
  ( -- * The 'Term' abstraction
    Term,
    ColorMode (..),
    runTerm,
    runTermPure,

    -- * Styling
    bold,
    cyan,
    green,
    red,

    -- * Printing
    put,
    putShow,
    putOutputable,
    newline,
  )
where

import Control.Applicative (Const (..))
import Control.Monad (forM_)
import Data.Foldable (toList)
import Data.Sequence (Seq)
import Data.Sequence qualified as Seq
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO.Utf8 qualified as T.Utf8
import GHC.Utils.Outputable (Outputable)
import Ormolu.Utils (showOutputable)
import System.Console.ANSI
import System.IO (Handle, hFlush)

----------------------------------------------------------------------------
-- The 'Term' abstraction

type Term = TermOutput ()

newtype TermOutput a = TermOutput (Const (Seq TermOutputNode) a)
  deriving (Semigroup, Monoid, Functor, Applicative)

data TermOutputNode
  = OutputText Text
  | WithColor Color Term
  | WithBold Term

singleTerm :: TermOutputNode -> Term
singleTerm = TermOutput . Const . Seq.singleton

-- | Whether to use colors and other features of ANSI terminals.
data ColorMode = Never | Always | Auto
  deriving (Eq, Show, Enum, Bounded)

-- | Run 'Term' monad.
runTerm ::
  Term ->
  -- | Color mode
  ColorMode ->
  -- | Handle to print to
  Handle ->
  IO ()
runTerm term0 colorMode handle = do
  useSGR <- case colorMode of
    Never -> return False
    Always -> return True
    Auto -> hSupportsANSI handle
  runTerm' useSGR term0
  hFlush handle
  where
    runTerm' useSGR = go
      where
        go (TermOutput (Const nodes)) =
          forM_ nodes $ \case
            OutputText s -> T.Utf8.hPutStr handle s
            WithColor color term -> withSGR [SetColor Foreground Dull color] (go term)
            WithBold term -> withSGR [SetConsoleIntensity BoldIntensity] (go term)

        withSGR sgrs m
          | useSGR = hSetSGR handle sgrs >> m >> hSetSGR handle [Reset]
          | otherwise = m

runTermPure :: Term -> Text
runTermPure (TermOutput (Const nodes)) =
  T.concat . toList . flip fmap nodes $ \case
    OutputText s -> s
    WithColor _ term -> runTermPure term
    WithBold term -> runTermPure term

----------------------------------------------------------------------------
-- Styling

-- | Make the output bold text.
bold :: Term -> Term
bold = singleTerm . WithBold

-- | Make the output cyan text.
cyan :: Term -> Term
cyan = singleTerm . WithColor Cyan

-- | Make the output green text.
green :: Term -> Term
green = singleTerm . WithColor Green

-- | Make the output red text.
red :: Term -> Term
red = singleTerm . WithColor Red

----------------------------------------------------------------------------
-- Printing

-- | Output 'Text'.
put :: Text -> Term
put = singleTerm . OutputText

-- | Output a 'Show' value.
putShow :: (Show a) => a -> Term
putShow = put . T.pack . show

-- | Output an 'Outputable' value.
putOutputable :: (Outputable a) => a -> Term
putOutputable = put . T.pack . showOutputable

-- | Output a newline.
newline :: Term
newline = put "\n"