packages feed

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

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

-- | Wrapper to write @terminal@ applications in Rhine, using concurrency.
module FRP.Rhine.Terminal (
  TerminalEventClock (..),
  flowTerminal,
  terminalConcurrently,
) where

-- base

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

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

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

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

-- transformers

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

-- 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
    , ..
    }