packages feed

freestyle-0.1.0.0: lib/Freestyle.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Create and display pretty terminal graphics
--
-- Construct a new handle and configuration:
--
--   * a way to read state
--   * a function to draw the current state
--   * a function to layout the current doc
--   * a function to render the 'SimpleDocStream' to 'Text'
--
-- Run the configuration using 'runFreestyle'.
-- Note, it has an 'Eq' constraint. Freestyle re-renders
-- the terminal if the 'SimpleDocStream' changes.
-- This way, apps may make state changes without causing
-- terminal re-rendering.
--
-- The t'Freestyle' handle supports on-the-fly
-- state, draw, layout, and rendering changes. Use dynamic
-- layout, rendering algorithms based on environment
-- such as window size.
--
-- @
-- import Freestyle
--
-- main :: IO ()
-- main = do
--   s <- newTVarIO \"FREESTYLE!!!FREESTYLE!!!FREESTYLE!!!\"
--   f <- newFreestyleIO
--   concurrently_ (runFreestyle f $ cfg s) $
--     forever $ do
--       atomically $ modifyTVar' s rote
--       threadDelay 200000
--   where
--     rote s = last s : init s
--
-- cfg :: TVar String -> FreestyleCfg String AnsiStyle
-- cfg s = FreestyleCfg
--   { readState = readTVar s
--   , drawState = \\s' -> return $
--     applyWhen (take 1 s' == \"E\") (annotate $ color Blue) $ pretty s'
--   , layoutDoc = return . layoutPretty defaultLayoutOptions
--   , renderDoc = return . renderLazy
--   }
-- @
module Freestyle
  ( Freestyle
  , newFreestyle
  , newFreestyleIO
  , FreestyleCfg(..)
  , runFreestyle
  , setLayout
  , setRender
  , setState
  , setDraw
  ) where

import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as T
import Data.Text.Lazy.Builder
import qualified Data.Text.Lazy.IO as TIO
import Prettyprinter
import System.IO

-- | TUI handle
data Freestyle s ann = Freestyle
  { lay      :: TMVar (Doc ann -> STM (SimpleDocStream ann))
  , ren      :: TMVar (SimpleDocStream ann -> STM Text)
  , stateVar :: TMVar (STM s)              -- current state
  , drawVar  :: TMVar (s -> STM (Doc ann)) -- draw state
  }

-- | Construct a new STM TUI
newFreestyle :: STM (Freestyle s ann)
newFreestyle =
  Freestyle <$> newEmptyTMVar <*> newEmptyTMVar <*> newEmptyTMVar <*> newEmptyTMVar

-- | Construct a new IO TUI
newFreestyleIO :: IO (Freestyle s ann)
newFreestyleIO = atomically newFreestyle

-- | User init configuration.
-- The TUI updates when 'SimpleDocStream' changes.
data FreestyleCfg s ann = FreestyleCfg
  { readState :: STM s                                -- ^ read the current state
  , drawState :: s -> STM (Doc ann)                   -- ^ draw the current state
  , layoutDoc :: Doc ann -> STM (SimpleDocStream ann) -- ^ layout the doc
  , renderDoc :: SimpleDocStream ann -> STM Text      -- ^ render the doc
  }

-- | Run the TUI with the configuration
runFreestyle :: Eq ann => Freestyle s ann -> FreestyleCfg s ann -> IO a
runFreestyle f cfg = do
  atomically $ do
    setLayout f $ layoutDoc cfg
    setRender f $ renderDoc cfg
    setState  f $ readState cfg
    setDraw   f $ drawState cfg
  -- without echo
  bracket_ (hSetEcho stdin False) (hSetEcho stdin True) $ do
    hSetBuffering stdout $ BlockBuffering Nothing
    -- without cursor
    bracket_ (output "\x1b[?25l") (output "\x1b[?25h") $
      loop Nothing `finally` output clearScreen
  where
    loop prevM = join $ atomically $ do
      s <- join $ readTMVar $ stateVar f
      draw <- readTMVar $ drawVar f
      layo <- readTMVar $ lay f
      rend <- readTMVar $ ren f
      sds' <- layo =<< draw s
      case prevM of
        Nothing -> do
          t <- rend sds'
          return $ do
            output $ clearScreen <> t
            loop $ Just (sds', t)
        Just (sds, p) -> do
          check $ sds /= sds'
          t <- rend sds'
          return $ do
            output $ composite t p
            loop $ Just (sds', t)

-- | Change the layout algorithm
setLayout :: Freestyle s ann -> (Doc ann -> STM (SimpleDocStream ann)) -> STM ()
setLayout f = writeTMVar $ lay f

-- | Change the rendering algorithm
setRender :: Freestyle s ann -> (SimpleDocStream ann -> STM Text) -> STM ()
setRender f = writeTMVar $ ren f

-- | Change the state reader
setState :: Freestyle s ann -> STM s -> STM ()
setState f = writeTMVar $ stateVar f

-- | Change the drawing algorithm
setDraw :: Freestyle s ann -> (s -> STM (Doc ann)) -> STM ()
setDraw f = writeTMVar $ drawVar f

-- | write stdout and flush
output :: Text -> IO ()
output t = TIO.putStr t >> hFlush stdout

-- | erase screen and goto home
clearScreen :: Text
clearScreen = "\x1b[2J\x1b[H"

-- | construct line diff rewrites
composite :: Text -> Text -> Text
composite new old = toLazyText $ go 0 (T.lines new) (T.lines old)
  where
    go _ ns [] = fromLazyText $ T.unlines ns
    go _ [] (_:_) = "\x1b[J"
    go r (n:ns) (o:os) = mconcat
      [ if n /= o then movRow <> fromLazyText n else mempty
      , if T.length o > T.length n then "\x1b[0K" else mempty -- erase line
      , go (r + 1) ns os
      ]
      where
        movRow = "\x1b[" <> fromString (show (r + 1 :: Int)) <> "H"