packages feed

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

------------------------------------------------------------------------------
-- |
-- Module      : Redact.Monad.Handle
-- Description : handle I/O
-- Copyright   : Copyright (c) 2020-2022 Travis Cardwell
-- License     : MIT
------------------------------------------------------------------------------

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

module Redact.Monad.Handle
  ( -- * MonadHandle
    MonadHandle(..)
    -- * Internal
  , handleToTerminal
  , handleToTerminal'
  , fileToTerminal
  , fileToTerminal'
  ) where

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

-- https://hackage.haskell.org/package/base
import Control.Monad (join)
import Data.Bifunctor (first)
import Data.Bool (bool)
import Data.Typeable (Typeable)
import qualified System.IO as IO
import System.IO (Handle, IOMode(ReadMode))
import System.IO.Error (tryIOError)

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

-- (redact)
import Redact.Monad.Terminal (MonadTerminal, initialize, putLine)
import Redact.Types (Error(IOError, RedactError), Line)

------------------------------------------------------------------------------
-- $MonadHandle

-- | Handle I/O
--
-- @since 0.4.0.0
class Monad m => MonadHandle m where
  -- | Read a single line from a handle
  hGetLine :: Handle -> m Text

  -- | Check if a handle has more content
  hIsEOF :: Handle -> m Bool

  -- | Open a file and perform an action on its handle
  withFile
    :: Typeable r
    => FilePath
    -> IOMode
    -> (Handle -> m r)
    -> m (Either IOError r)

instance MonadHandle IO where
  hGetLine = TIO.hGetLine
  {-# INLINE hGetLine #-}

  hIsEOF = IO.hIsEOF
  {-# INLINE hIsEOF #-}

  withFile path mode = tryIOError . IO.withFile path mode
  {-# INLINE withFile #-}

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

-- | Redact text from a 'Handle' strictly, putting it to the terminal
handleToTerminal
  :: forall m s. (MonadHandle m, MonadTerminal m)
  => (s -> Text -> Either String (Line, s))  -- ^ step function
  -> (s -> Maybe String)                     -- ^ end function
  -> s                                       -- ^ initial state
  -> [Term.SGR]  -- ^ 'Term.SGR's for redacted text
  -> Handle
  -> m (Either Error ())
handleToTerminal step end initialState sgrs handle =
    begin =<< maybeGetLine initialState
  where
    maybeGetLine :: s -> m (Either String (Maybe Line, s))
    maybeGetLine state
      = bool
          (fmap (first Just) . step state <$> hGetLine handle)
          (pure $ Right (Nothing, state))
      =<< hIsEOF handle

    begin :: Either String (Maybe Line, s) -> m (Either Error ())
    begin = \case
      Right (Just line, state) -> do
        initialize sgrs line
        loop line =<< maybeGetLine state
      Right (Nothing, _state) -> pure $ Right ()
      Left err -> pure . Left $ RedactError err

    loop :: Line -> Either String (Maybe Line, s) -> m (Either Error ())
    loop line = \case
      Right (Just nextLine, state) -> do
        putLine sgrs line $ Just nextLine
        loop nextLine =<< maybeGetLine state
      Right (Nothing, state) -> do
        putLine sgrs line Nothing
        case end state of
          Nothing -> pure $ Right ()
          Just err -> pure . Left $ RedactError err
      Left err -> do
        putLine sgrs line Nothing
        pure . Left $ RedactError err

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

-- | Redact text from a 'Handle' leniently, putting it to the terminal
handleToTerminal'
  :: forall m s. (MonadHandle m, MonadTerminal m)
  => (s -> Text -> (Line, s))  -- ^ step function
  -> s                         -- ^ initial state
  -> [Term.SGR]                -- ^ 'Term.SGR's for redacted text
  -> Handle
  -> m ()
handleToTerminal' step initialState sgrs handle =
    begin =<< maybeGetLine initialState
  where
    maybeGetLine :: s -> m (Maybe Line, s)
    maybeGetLine state
      = bool
          (first Just . step state <$> hGetLine handle)
          (pure (Nothing, state))
      =<< hIsEOF handle

    begin :: (Maybe Line, s) -> m ()
    begin = \case
      (Just line, state) -> do
        initialize sgrs line
        loop line =<< maybeGetLine state
      (Nothing, _state) -> pure ()

    loop :: Line -> (Maybe Line, s) -> m ()
    loop line (mNextLine, state) = do
      putLine sgrs line mNextLine
      case mNextLine of
        Just nextLine -> loop nextLine =<< maybeGetLine state
        Nothing -> pure ()

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

-- | Redact text from a file strictly, putting it to the terminal
fileToTerminal
  :: (MonadHandle m, MonadTerminal m)
  => (s -> Text -> Either String (Line, s))  -- ^ step function
  -> (s -> Maybe String)                     -- ^ end function
  -> s                                       -- ^ initial state
  -> [Term.SGR]  -- ^ 'Term.SGR's for redacted text
  -> FilePath
  -> m (Either Error ())
fileToTerminal step end initialState sgrs path
    = fmap (join . first IOError)
    . withFile path ReadMode
    $ handleToTerminal step end initialState sgrs

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

-- | Redact text from a file leniently, putting it to the terminal
fileToTerminal'
  :: (MonadHandle m, MonadTerminal m)
  => (s -> Text -> (Line, s))  -- ^ step function
  -> s                         -- ^ initial state
  -> [Term.SGR]                -- ^ 'Term.SGR's for redacted text
  -> FilePath
  -> m (Either Error ())
fileToTerminal' step initialState sgrs path
    = fmap (first IOError)
    . withFile path ReadMode
    $ handleToTerminal' step initialState sgrs