termbox-banana-0.4.0: src/Termbox/Banana.hs
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Termbox.Banana
( -- * Introduction
-- $intro
-- * Core API
Event (..),
Program,
run,
-- * Re-exports from @termbox@
Termbox.black,
Termbox.blue,
Termbox.bold,
Termbox.cyan,
Termbox.green,
Termbox.magenta,
Termbox.red,
Termbox.reverse,
Termbox.underline,
Termbox.white,
Termbox.yellow,
Termbox.set,
Termbox.Attr,
Termbox.Cell (..),
Termbox.Cells,
Termbox.Cursor (..),
Termbox.InitError (..),
Termbox.Key (..),
pattern Termbox.KeyCtrl2,
pattern Termbox.KeyCtrl3,
pattern Termbox.KeyCtrl4,
pattern Termbox.KeyCtrl5,
pattern Termbox.KeyCtrl7,
pattern Termbox.KeyCtrlH,
pattern Termbox.KeyCtrlI,
pattern Termbox.KeyCtrlLsqBracket,
pattern Termbox.KeyCtrlM,
pattern Termbox.KeyCtrlUnderscore,
Termbox.Mouse (..),
Termbox.PollError (..),
)
where
import Control.Concurrent.MVar
import Control.Monad.IO.Class (liftIO)
import Data.Function (fix)
import qualified Reactive.Banana as Banana
import qualified Reactive.Banana.Frameworks as Banana
import qualified Termbox
-- $intro
--
-- This module is intended to be imported qualified:
--
-- @
-- import qualified Termbox.Banana as Termbox
-- @
--
-- ==== __👉 Quick start example__
--
-- This is a program that displays the last key pressed, and quits on @Esc@:
--
-- @
-- {-\# LANGUAGE LambdaCase \#-}
--
-- module Main where
--
-- import Data.Void (Void)
-- import Reactive.Banana
-- import Reactive.Banana.Frameworks
-- import qualified Termbox.Banana as Termbox
--
-- main :: IO ()
-- main =
-- Termbox.'run' moment
--
-- moment
-- :: (Void -> IO ())
-- -> Event (Termbox.'Event' Void)
-- -> Behavior (Int, Int)
-- -> MomentIO (Behavior (Termbox.'Termbox.Cells', Termbox.'Termbox.Cursor'), Event ())
-- moment _fireUserEvent eEvent _bSize = do
-- let eQuit = () <$ filterE isKeyEsc eEvent
-- bLatestEvent <- stepper Nothing (Just \<$\> eEvent)
-- let bCells = maybe mempty renderEvent \<$\> bLatestEvent
-- let bScene = (,) \<$\> bCells \<*\> pure Termbox.'Termbox.NoCursor'
-- pure (bScene, eQuit)
--
-- renderEvent :: Show a => Termbox.'Event' a -> Termbox.'Termbox.Cells'
-- renderEvent =
-- foldMap (\\(i, c) -> Termbox.set i 0 (Termbox.'Termbox.Cell' c mempty mempty))
-- . zip [0..]
-- . show
--
-- isKeyEsc :: Termbox.'Event' a -> Bool
-- isKeyEsc = \\case
-- Termbox.'EventKey' Termbox.'Termbox.KeyEsc' -> True
-- _ -> False
-- @
-- | A key press, terminal resize, mouse click, or a user event.
data Event a
= EventKey !Termbox.Key
| EventResize !Int !Int
| EventMouse !Termbox.Mouse !Int !Int
| EventUser a
deriving stock (Eq, Ord, Show)
type Program a b =
-- | Callback that produces a user event.
(a -> IO ()) ->
-- | Event stream.
Banana.Event (Event a) ->
-- | Time-varying terminal size (width, then height).
Banana.Behavior (Int, Int) ->
-- | The time-varying scene to render, and an event stream of arbitrary values, only the first of which is relevant,
-- which ends the @termbox@ program and returns from 'run'.
Banana.MomentIO (Banana.Behavior (Termbox.Cells, Termbox.Cursor), Banana.Event b)
-- | Run a @termbox@ program.
run :: Program a b -> IO b
run program =
Termbox.run $ \initialWidth initialHeight render poll -> do
doneVar <- newEmptyMVar
(eventAddHandler, fireEvent) <- Banana.newAddHandler
(userEventAddHandler, fireUserEvent) <- Banana.newAddHandler
network <-
Banana.compile $ do
eEvent <- Banana.fromAddHandler eventAddHandler
eUserEvent <- Banana.fromAddHandler userEventAddHandler
let eTermboxEvent =
Banana.unionWith
const
( ( \case
Termbox.EventKey key -> EventKey key
Termbox.EventResize width height -> EventResize width height
Termbox.EventMouse mouse col row -> EventMouse mouse col row
)
<$> eEvent
)
(EventUser <$> eUserEvent)
let eResize :: Banana.Event (Int, Int)
eResize =
Banana.filterJust
( ( \case
Termbox.EventResize w h -> Just (w, h)
_ -> Nothing
)
<$> eEvent
)
bSize <- Banana.stepper (initialWidth, initialHeight) eResize
(bScene, eDone) <- program fireUserEvent eTermboxEvent bSize
let bRender = (\(cells, cursor) -> render cells cursor) <$> bScene
eRender <- Banana.changes bRender
do
action <- Banana.valueB bRender
liftIO action
Banana.reactimate (putMVar doneVar <$> eDone)
Banana.reactimate' eRender
Banana.actuate network
fix $ \loop -> do
poll >>= fireEvent
tryReadMVar doneVar >>= maybe loop pure