termbox-banana-0.2.0: src/Termbox/Banana.hs
{-# language DerivingStrategies #-}
{-# language GeneralizedNewtypeDeriving #-}
{-# language LambdaCase #-}
{-# language ScopedTypeVariables #-}
{-# language TypeFamilies #-}
module Termbox.Banana
( -- $intro
TermboxEvent
, run
, run_
, set
, Scene(..)
, Cells
, Cursor(..)
-- * Re-exports
, Termbox.black
, Termbox.red
, Termbox.green
, Termbox.yellow
, Termbox.blue
, Termbox.magenta
, Termbox.cyan
, Termbox.white
, Termbox.bold
, Termbox.underline
, Termbox.reverse
, Termbox.Attr
, Termbox.Cell(..)
, Termbox.Event(..)
, Termbox.InitError(..)
, Termbox.InputMode(..)
, Termbox.Key(..)
, Termbox.Mouse(..)
, Termbox.MouseMode(..)
, Termbox.OutputMode(..)
-- * Example
-- $example
) where
import Control.Concurrent.MVar
import Control.Exception (throwIO)
import Data.Function (fix)
import Reactive.Banana
import Reactive.Banana.Frameworks
import qualified Termbox
-- $intro
-- See the bottom of this module for a simple, runnable example to get started.
--
-- Here's how to run the examples with @cabal@:
--
-- @
-- cabal v2-run --constraint "termbox-banana +build-examples" termbox-banana-example-echo
-- cabal v2-run --constraint "termbox-banana +build-examples" termbox-banana-example-hoogle
-- @
--
-- This module is intended to be imported qualified.
--
-- @
-- import qualified Termbox.Banana as Termbox
-- @
-- | A @termbox@ event. This type alias exists only for Haddock readability;
-- in code, you are encouraged to use
--
-- * @Event@ for @reactive-banana@ events
-- * @Termbox.Event@ for @termbox@ events
--
-- @since 0.1.0
type TermboxEvent
= Termbox.Event
-- | A scene to render; a grid of cells and a cursor.
--
-- @since 0.1.0
data Scene
= Scene !Cells !Cursor
-- | A grid of cells.
--
-- Create a 'Cells' with 'set' or 'mempty' and combine them with ('<>').
--
-- @since 0.1.0
newtype Cells
= Cells (IO ())
deriving newtype (Monoid, Semigroup)
-- | A cursor.
--
-- @since 0.1.0
data Cursor
= Cursor !Int !Int -- ^ Column, then row
| NoCursor
-- | Set a single cell's value (column, then row).
--
-- @since 0.1.0
set :: Int -> Int -> Termbox.Cell -> Cells
set x y z =
Cells (Termbox.set x y z)
type EventSource a
= (AddHandler a, a -> IO ())
-- | Run a @termbox@ program with the specified input and output modes.
--
-- Given
--
-- * the terminal event stream
-- * the time-varying terminal size (width, then height)
--
-- return
--
-- * a time-varying scene to render
-- * an event stream of arbitrary values, only the first of which is relevant,
-- which ends the @termbox@ program and returns from the @main@ action.
--
-- @since 0.2.0
run
:: Termbox.InputMode -- ^
-> Termbox.OutputMode -- ^
-> ( Event TermboxEvent
-> Behavior (Int, Int)
-> MomentIO (Behavior Scene, Event a))
-> IO (Either Termbox.InitError a)
run imode omode program =
Termbox.run $ do
Termbox.setInputMode imode
Termbox.setOutputMode omode
doneVar :: MVar a <-
newEmptyMVar
(eventAddHandler, fireEvent) :: EventSource TermboxEvent <-
newAddHandler
network :: EventNetwork <-
compile $ do
eEvent :: Event TermboxEvent <-
fromAddHandler eventAddHandler
let
eResize :: Event (Int, Int)
eResize =
filterJust
((\case
Termbox.EventResize w h -> Just (w, h)
_ -> Nothing)
<$> eEvent)
bSize :: Behavior (Int, Int) <-
flip stepper eResize =<<
liftIO Termbox.getSize
moment program eEvent bSize (putMVar doneVar)
actuate network
fix $ \loop -> do
Termbox.poll >>= fireEvent
tryReadMVar doneVar >>=
maybe loop pure
-- | Like 'run', but throws 'Termbox.InitError's as @IO@ exceptions.
--
-- @since 0.2.0
run_
:: Termbox.InputMode -- ^
-> Termbox.OutputMode -- ^
-> ( Event TermboxEvent
-> Behavior (Int, Int)
-> MomentIO (Behavior Scene, Event a))
-> IO a
run_ imode omode program =
run imode omode program >>= either throwIO pure
moment
:: ( Event TermboxEvent
-> Behavior (Int, Int)
-> MomentIO (Behavior Scene, Event a))
-> Event TermboxEvent
-> Behavior (Int, Int)
-> (a -> IO ())
-> MomentIO ()
moment program eEvent bSize abort = do
(bScene, eDone) :: (Behavior Scene, Event a) <-
program eEvent bSize
eScene :: Event (Future Scene) <-
changes bScene
let
render :: Scene -> IO ()
render (Scene (Cells cells) cursor) = do
Termbox.clear mempty mempty
cells
case cursor of
Cursor c r -> Termbox.setCursor c r
NoCursor -> Termbox.hideCursor
Termbox.flush
liftIO . render =<< valueB bScene
reactimate (abort <$> eDone)
reactimate' ((fmap.fmap) render eScene)
-- $example
--
-- Below is a sample program that simply displays the last key pressed, and
-- quits on @Esc@:
--
-- @
-- {-\# LANGUAGE LambdaCase \#-}
-- {-\# LANGUAGE ScopedTypeVariables \#-}
--
-- module Main where
--
-- import Reactive.Banana
-- import Reactive.Banana.Frameworks
--
-- import qualified Termbox.Banana as Termbox
--
-- main :: IO ()
-- main =
-- Termbox.'run_'
-- (Termbox.'Termbox.InputModeEsc' Termbox.'Termbox.MouseModeNo')
-- Termbox.'Termbox.OutputModeNormal'
-- moment
--
-- moment
-- :: Event Termbox.'Termbox.Event'
-- -> Behavior (Int, Int)
-- -> MomentIO (Behavior Termbox.'Termbox.Scene', Event ())
-- moment eEvent _bSize = do
-- let
-- eQuit :: Event ()
-- eQuit =
-- () <$ filterE isKeyEsc eEvent
--
-- bLatestEvent :: Behavior (Maybe Termbox.'Termbox.Event') <-
-- stepper
-- Nothing
-- (Just \<$\> eEvent)
--
-- let
-- bCells :: Behavior Termbox.'Termbox.Cells'
-- bCells =
-- maybe mempty renderEvent \<$\> bLatestEvent
--
-- let
-- bScene :: Behavior Termbox.'Termbox.Scene'
-- bScene =
-- Termbox.'Termbox.Scene'
-- \<$\> bCells
-- \<*\> pure Termbox.'Termbox.NoCursor'
--
-- pure (bScene, eQuit)
--
-- renderEvent :: Termbox.'Termbox.Event' -> Termbox.'Termbox.Cells'
-- renderEvent =
-- foldMap (\\(i, c) -> Termbox.set i 0 (Termbox.'Termbox.Cell' c mempty mempty))
-- . zip [0..]
-- . show
--
-- isKeyEsc :: Termbox.'Termbox.Event' -> Bool
-- isKeyEsc = \\case
-- Termbox.'Termbox.EventKey' Termbox.'Termbox.KeyEsc' _ -> True
-- _ -> False
-- @