packages feed

sneakyterm-0.1: src/System/Console/SneakyTerm/MonadTerminal.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TemplateHaskell            #-}
{-|
Module      : System.Console.SneakyTerm.MonadTerminal
Maintainer  : pmidden@secure.mailbox.org
-}
module System.Console.SneakyTerm.MonadTerminal(
    runTerminal
  , tmRender
  , tmCharEvent
  , MonadTerminal
  , MonadTerminalM) where

import           ClassyPrelude              hiding ((\\))
import           Control.Lens               (ix, makeLenses, to, use, view,
                                             (%=), (+=), (.=), (^.), (^?!))
import           Control.Monad.State        (MonadState)
import           Control.Monad.State.Strict (StateT, evalStateT)
import           Data.List                  ((\\))
import           Data.Map.Strict            (insert)
import           Linear.V2
import           System.Console.SneakyTerm.Color
import           System.Console.SneakyTerm.ColorPair
import           System.Console.SneakyTerm.PointInt
import           System.Console.SneakyTerm.Rect
import           System.Console.SneakyTerm.Tile
import qualified UI.NCurses                 as C

-- | A type analogous to 'MonadState' and 'MonadReader'
class Monad m => MonadTerminal m where
  tmRender :: [Tile] -> m () -- ^ Render the given tiles
  tmCharEvent :: m Char -- ^ Wait for a character on standard input (blocking)

-- | Map ColorPairs to ColorIDs (a color cache, so to speak)
type ColorMap = Map ColorPair C.ColorID

data TerminalData = TerminalData {
    _tdWindow      :: C.Window
  , _tdViewport    :: Rect Int
  , _tdPrevTiles   :: [Tile]
  , _tdColors      :: ColorMap
  , _tdNextColorId :: Int
  }

$(makeLenses ''TerminalData)

newtype MonadTerminalM a = MonadTerminalM {
    runMonadTerminal :: StateT TerminalData C.Curses a
  } deriving(Monad,MonadIO,MonadState TerminalData,Applicative,Functor)

instance (Monad m,MonadTerminal m) => MonadTerminal (StateT n m) where
  tmRender ts = lift (tmRender ts)
  tmCharEvent = lift tmCharEvent

liftCurses :: C.Curses a -> MonadTerminalM a
liftCurses  = MonadTerminalM . lift

instance MonadTerminal MonadTerminalM where
  tmRender ts = do
      currentColors <- use tdColors
      let unassignedColors = (view tileColor <$> ts) \\ keys currentColors
      forM_ (toList unassignedColors) $ \c -> do
        nextColor <- use tdNextColorId
        cid <- liftCurses $ C.newColorID (toCurses (c ^. cpForeground)) (toCurses (c ^. cpBackground)) (fromIntegral nextColor)
        tdNextColorId += 1
        tdColors %= insert c cid
      colors <- use tdColors
      prevTiles <- use tdPrevTiles
      window <- use tdWindow
      liftCurses $ C.updateWindow window $ tileDiff colors prevTiles ts
      liftCurses $ C.render
      tdPrevTiles .= ts
  tmCharEvent = do
    window <- use tdWindow
    e <- liftCurses (C.getEvent window Nothing)
    case e of
      Just (C.EventCharacter c) -> return c
      _ -> tmCharEvent

tileDiff :: ColorMap -> [Tile] -> [Tile] -> C.Update ()
tileDiff cs before after = do
  let toClear = before \\ after
      toAdd = after \\ before
  mapM_ clearTile toClear
  mapM_ (drawTile cs) toAdd

moveCursor :: PointInt -> C.Update ()
moveCursor (V2 x y) = C.moveCursor (fromIntegral y) (fromIntegral x)

clearTile :: Tile -> C.Update ()
clearTile t = do
  moveCursor (t ^. tilePosition)
  C.drawString " "

drawTile :: ColorMap -> Tile -> C.Update ()
drawTile colors t = do
  moveCursor (t ^. tilePosition)
  C.setColor (colors ^?! ix (t ^. tileColor))
  C.drawString [t ^. tileCharacter]

runTerminal :: Rect Int -- ^ The terminal viewport
            -> MonadTerminalM () -- ^ The actions to run inside a curses environment
            -> IO ()
runTerminal viewport a = C.runCurses $ do
  C.setEcho False
  w <- C.newWindow (viewport ^. rectHeight . to fromIntegral) (viewport ^. rectWidth . to fromIntegral) 0 0
  _ <- C.setCursorMode C.CursorInvisible
  evalStateT (runMonadTerminal a) (TerminalData w viewport mempty mempty 1)