packages feed

rhine-terminal-0.8.1: src/FRP/Rhine/Terminal.hs

{- | Wrapper to write @terminal@ applications in Rhine, using concurrency.
-}

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RecordWildCards #-}
module FRP.Rhine.Terminal
  ( TerminalEventClock (..)
  , flowTerminal
  , terminalConcurrently
  ) where

-- base
import Prelude hiding (putChar)
import Unsafe.Coerce (unsafeCoerce)

-- exceptions
import Control.Monad.Catch (MonadMask)

-- time
import Data.Time.Clock ( getCurrentTime )

-- terminal
import System.Terminal ( awaitEvent, runTerminalT, Event, Interrupt, TerminalT, MonadInput )
import System.Terminal.Internal ( Terminal )

-- transformers
import Control.Monad.Trans.Reader
import Control.Monad.Trans.Class (lift)

-- rhine
import FRP.Rhine

-- | A clock that ticks whenever events or interrupts on the terminal arrive.
data TerminalEventClock = TerminalEventClock

instance (MonadInput m, MonadIO m) => Clock m TerminalEventClock
  where
    type Time TerminalEventClock = UTCTime
    type Tag  TerminalEventClock = Either Interrupt Event

    initClock TerminalEventClock = do
      initialTime <- liftIO getCurrentTime
      return
        ( constM $ do
            event <- awaitEvent
            time <- liftIO getCurrentTime
            return (time, event)
        , initialTime
        )

instance GetClockProxy TerminalEventClock

instance Semigroup TerminalEventClock where
  t <> _ = t

-- | A function wrapping `flow` to use at the top level
-- in order to run a `Rhine (TerminalT t m) cl ()`
--
-- Example:
--
-- @
-- mainRhine :: MonadIO m => Rhine (TerminalT LocalTerminal m) TerminalEventClock () ()
-- mainRhine = tagS >-> arrMCl (liftIO . print) @@ TerminalEventClock
--
-- main :: IO ()
-- main = withTerminal $ \term -> `flowTerminal` term mainRhine
-- @

flowTerminal
  :: ( MonadIO m
     , MonadMask m
     , Terminal t
     , Clock (TerminalT t m) cl
     , GetClockProxy cl
     , Time cl ~ Time (In  cl)
     , Time cl ~ Time (Out cl)
     )
  => t
  -> Rhine (TerminalT t m) cl () ()
  -> m ()
flowTerminal term clsf = flip runTerminalT term $ flow clsf

-- | A schedule in the 'TerminalT LocalTerminal' transformer,
--   supplying the same backend connection to its scheduled clocks.
terminalConcurrently
  :: forall t cl1 cl2. (
       Terminal t
     , Clock (TerminalT t IO) cl1
     , Clock (TerminalT t IO) cl2
     , Time cl1 ~ Time cl2
     )
  => Schedule (TerminalT t IO) cl1 cl2
terminalConcurrently
  = Schedule $ \cl1 cl2 -> do
      term <- terminalT ask
      lift $ first liftTransS <$>
        initSchedule concurrently (runTerminalClock term cl1) (runTerminalClock term cl2)

-- Workaround TerminalT constructor not being exported. Should be safe in practice.
-- See PR upstream https://github.com/lpeterse/haskell-terminal/pull/18
terminalT :: ReaderT t m a -> TerminalT t m a
terminalT = unsafeCoerce

type RunTerminalClock m t cl = HoistClock (TerminalT t m) m cl

runTerminalClock
  :: Terminal t
  => t
  -> cl
  -> RunTerminalClock IO t cl
runTerminalClock term unhoistedClock = HoistClock
  { monadMorphism = flip runTerminalT term
  , ..
  }