packages feed

redact-0.4.0.0: src/Redact/Monad/Terminal.hs

------------------------------------------------------------------------------
-- |
-- Module      : Redact.Monad.Terminal
-- Description : terminal output
-- Copyright   : Copyright (c) 2020-2022 Travis Cardwell
-- License     : MIT
------------------------------------------------------------------------------

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Redact.Monad.Terminal
  ( -- * MonadTerminal
    MonadTerminal(..)
    -- * API
  , redactSGRs
  , resetSGRs
  , reset
  , putLines
    -- * Internal
  , initialize
  , putLine
  ) where

-- https://hackage.haskell.org/package/ansi-terminal
import qualified System.Console.ANSI as Term

-- https://hackage.haskell.org/package/base
import Control.Monad (unless, when)
import Data.Maybe (listToMaybe)
import Prelude hiding (lines, putStr, putStrLn)

-- https://hackage.haskell.org/package/text
import Data.Text (Text)
import qualified Data.Text.IO as TIO

-- (redact)
import Redact.Types (Line(NormalLine, RedactLine), Part(Redact, Stet))

------------------------------------------------------------------------------
-- $MonadTerminal

-- | Terminal output
--
-- @since 0.4.0.0
class Monad m => MonadTerminal m where
  -- | Write a string to the terminal
  putStr :: Text -> m ()

  -- | Write a string to the terminal, appending a newline
  putStrLn :: Text -> m ()

  -- | Set Select Graphic Rendition mode
  setSGR :: [Term.SGR] -> m ()

instance MonadTerminal IO where
  putStr = TIO.putStr
  {-# INLINE putStr #-}

  putStrLn =  TIO.putStrLn
  {-# INLINE putStrLn #-}

  setSGR = Term.setSGR
  {-# INLINE setSGR #-}

------------------------------------------------------------------------------
-- $API

-- | Construct 'Term.SGR's for redacted text
--
-- @since 0.4.0.0
redactSGRs :: Term.Color -> Term.ColorIntensity -> [Term.SGR]
redactSGRs color intensity =
    [ Term.SetColor Term.Foreground intensity color
    , Term.SetColor Term.Background intensity color
    ]

------------------------------------------------------------------------------

-- | 'Term.SGR's for resetting to normal mode
--
-- @since 0.4.0.0
resetSGRs :: [Term.SGR]
resetSGRs = [Term.Reset]

------------------------------------------------------------------------------

-- | Reset the terminal color mode and go to the next line
--
-- @since 0.4.0.0
reset :: MonadTerminal m => m ()
reset = setSGR resetSGRs >> putStrLn ""

------------------------------------------------------------------------------

-- | Put redacted text to the terminal
--
-- It is assumed that the terminal is set to display normal colors when this
-- function is called.  If the first 'Line' is a 'RedactLine', then an extra
-- blank line is first output in order to set the colors.  The terminal is set
-- to display normal colors when this function exits.
--
-- @since 0.4.0.0
putLines
  :: forall m. MonadTerminal m
  => [Term.SGR]
  -> [Line]
  -> m ()
putLines sgrs lines = do
    maybe (pure ()) (initialize sgrs) . listToMaybe $ take 1 lines
    mapM_ (uncurry $ putLine sgrs) $
      zip lines ((Just <$> drop 1 lines) ++ [Nothing])

------------------------------------------------------------------------------
-- $Internal

-- | Initialize the terminal colors
--
-- When the first line is a 'RedactLine', an extra blank line is output in
-- order to set the terminal colors.
initialize
  :: MonadTerminal m
  => [Term.SGR]  -- ^ 'Term.SGR's for redacted text
  -> Line        -- ^ first line
  -> m ()
initialize sgrs = \case
    NormalLine{} -> pure ()
    RedactLine{} -> setSGR sgrs >> putStrLn ""

------------------------------------------------------------------------------

-- | Put a line of redacted text to the terminal
--
-- The colors for the line is assumed to be already set.  Be sure to call
-- 'initialize' before putting the first line.
putLine
  :: forall m. MonadTerminal m
  => [Term.SGR]  -- ^ 'Term.SGR's for redacted text
  -> Line        -- ^ line to put
  -> Maybe Line  -- ^ 'Just' next line or 'Nothing' if end
  -> m ()
putLine sgrs line mNextLine = case line of
    NormalLine parts -> go False parts
    RedactLine t
      | isNextRedact -> putStrLn t
      | otherwise    -> putStr t >> setSGR resetSGRs >> putStrLn ""
  where
    isNextRedact :: Bool
    isNextRedact = case mNextLine of
      Just RedactLine{} -> True
      _normalLineOrEnd  -> False

    go :: Bool -> [Part] -> m ()
    go isRedact [part] = case part of
      Stet t -> do
        when isRedact $ setSGR resetSGRs
        if isNextRedact
          then putStr t >> setSGR sgrs >> putStrLn ""
          else putStrLn t
      Redact t -> do
        unless isRedact $ setSGR sgrs
        if isNextRedact
          then putStrLn t
          else putStr t >> setSGR resetSGRs >> putStrLn ""
    go isRedact (part:parts) = case (isRedact, part) of
      (False, Stet t)   ->                     putStr t >> go False parts
      (True,  Stet t)   -> setSGR resetSGRs >> putStr t >> go False parts
      (False, Redact t) -> setSGR sgrs      >> putStr t >> go True  parts
      (True,  Redact t) ->                     putStr t >> go True  parts
    go _isRedact [] = putStrLn ""