brick-0.30: src/Brick/Main.hs
module Brick.Main
( App(..)
, defaultMain
, customMain
, simpleMain
, resizeOrQuit
, simpleApp
-- * Event handler functions
, continue
, halt
, suspendAndResume
, lookupViewport
, lookupExtent
, findClickedExtents
, clickedExtent
, getVtyHandle
-- ** Viewport scrolling
, viewportScroll
, ViewportScroll
, vScrollBy
, vScrollPage
, vScrollToBeginning
, vScrollToEnd
, hScrollBy
, hScrollPage
, hScrollToBeginning
, hScrollToEnd
, setTop
, setLeft
-- * Cursor management functions
, neverShowCursor
, showFirstCursor
, showCursorNamed
-- * Rendering cache management
, invalidateCacheEntry
, invalidateCache
)
where
import Control.Exception (finally)
import Lens.Micro ((^.), (&), (.~))
import Control.Monad (forever)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State
import Control.Monad.Trans.Reader
import Control.Concurrent (forkIO, killThread)
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
import Data.Monoid (mempty)
#endif
import Data.Maybe (listToMaybe)
import qualified Data.Map as M
import qualified Data.Set as S
import Graphics.Vty
( Vty
, Picture(..)
, Cursor(..)
, Event(..)
, update
, outputIface
, displayBounds
, shutdown
, nextEvent
, mkVty
, defaultConfig
)
import Graphics.Vty.Attributes (defAttr)
import Brick.BChan (BChan, newBChan, readBChan, readBChan2, writeBChan)
import Brick.Types (Widget, EventM(..))
import Brick.Types.Internal
import Brick.Widgets.Internal
import Brick.AttrMap
import qualified Brick.Widgets.Core as C
-- | The library application abstraction. Your application's operations
-- are represented here and passed to one of the various main functions
-- in this module. An application is in terms of an application state
-- type 's', an application event type 'e', and a resource name type
-- 'n'. In the simplest case 'e' is unused (left polymorphic or set to
-- '()'), but you may define your own event type and use 'customMain'
-- to provide custom events. The state type is the type of application
-- state to be provided by you and iteratively modified by event
-- handlers. The resource name type is the type of names you can assign
-- to rendering resources such as viewports and cursor locations.
data App s e n =
App { appDraw :: s -> [Widget n]
-- ^ This function turns your application state into a list of
-- widget layers. The layers are listed topmost first.
, appChooseCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n)
-- ^ This function chooses which of the zero or more cursor
-- locations reported by the rendering process should be
-- selected as the one to use to place the cursor. If this
-- returns 'Nothing', no cursor is placed. The rationale here
-- is that many widgets may request a cursor placement but your
-- application state is what you probably want to use to decide
-- which one wins.
, appHandleEvent :: s -> BrickEvent n e -> EventM n (Next s)
-- ^ This function takes the current application state and an
-- event and returns an action to be taken and a corresponding
-- transformed application state. Possible options are
-- 'continue', 'suspendAndResume', and 'halt'.
, appStartEvent :: s -> EventM n s
-- ^ This function gets called once just prior to the first
-- drawing of your application. Here is where you can make
-- initial scrolling requests, for example.
, appAttrMap :: s -> AttrMap
-- ^ The attribute map that should be used during rendering.
}
-- | The default main entry point which takes an application and an
-- initial state and returns the final state returned by a 'halt'
-- operation.
defaultMain :: (Ord n)
=> App s e n
-- ^ The application.
-> s
-- ^ The initial application state.
-> IO s
defaultMain app st = do
customMain (mkVty defaultConfig) Nothing app st
-- | A simple main entry point which takes a widget and renders it. This
-- event loop terminates when the user presses any key, but terminal
-- resize events cause redraws.
simpleMain :: (Ord n)
=> Widget n
-- ^ The widget to draw.
-> IO ()
simpleMain w = defaultMain (simpleApp w) ()
-- | A simple application with reasonable defaults to be overridden as
-- desired:
--
-- * Draws only the specified widget
-- * Quits on any event other than resizes
-- * Has no start event handler
-- * Provides no attribute map
-- * Never shows any cursors
simpleApp :: Widget n -> App s e n
simpleApp w =
App { appDraw = const [w]
, appHandleEvent = resizeOrQuit
, appStartEvent = return
, appAttrMap = const $ attrMap defAttr []
, appChooseCursor = neverShowCursor
}
-- | An event-handling function which continues execution of the event
-- loop only when resize events occur; all other types of events trigger
-- a halt. This is a convenience function useful as an 'appHandleEvent'
-- value for simple applications using the 'Event' type that do not need
-- to get more sophisticated user input.
resizeOrQuit :: s -> BrickEvent n e -> EventM n (Next s)
resizeOrQuit s (VtyEvent (EvResize _ _)) = continue s
resizeOrQuit s _ = halt s
data InternalNext n a = InternalSuspendAndResume (RenderState n) (IO a)
| InternalHalt a
readBrickEvent :: BChan (BrickEvent n e) -> BChan e -> IO (BrickEvent n e)
readBrickEvent brickChan userChan = either id AppEvent <$> readBChan2 brickChan userChan
runWithNewVty :: (Ord n)
=> Vty
-> BChan (BrickEvent n e)
-> Maybe (BChan e)
-> App s e n
-> RenderState n
-> s
-> IO (InternalNext n s)
runWithNewVty buildVty brickChan mUserChan app initialRS initialSt =
withVty buildVty $ \vty -> do
pid <- forkIO $ supplyVtyEvents vty brickChan
let readEvent = case mUserChan of
Nothing -> readBChan brickChan
Just uc -> readBrickEvent brickChan uc
runInner rs st = do
(result, newRS) <- runVty vty readEvent app st (rs & observedNamesL .~ S.empty
& clickableNamesL .~ mempty)
case result of
SuspendAndResume act -> do
killThread pid
return $ InternalSuspendAndResume newRS act
Halt s -> do
killThread pid
return $ InternalHalt s
Continue s -> runInner newRS s
runInner initialRS initialSt
-- | The custom event loop entry point to use when the simpler ones
-- don't permit enough control.
customMain :: (Ord n)
=> IO Vty
-- ^ An IO action to build a Vty handle. This is used to
-- build a Vty handle whenever the event loop begins or is
-- resumed after suspension.
-> Maybe (BChan e)
-- ^ An event channel for sending custom events to the event
-- loop (you write to this channel, the event loop reads from
-- it). Provide 'Nothing' if you don't plan on sending custom
-- events.
-> App s e n
-- ^ The application.
-> s
-- ^ The initial application state.
-> IO s
customMain buildVty mUserChan app initialAppState = do
initialVty <- buildVty
let run vty rs st brickChan = do
result <- runWithNewVty vty brickChan mUserChan app rs st
case result of
InternalHalt s -> return s
InternalSuspendAndResume newRS action -> do
newAppState <- action
newVty <- buildVty
run newVty newRS newAppState brickChan
emptyES = ES [] []
eventRO = EventRO M.empty initialVty mempty
(st, eState) <- runStateT (runReaderT (runEventM (appStartEvent app initialAppState)) eventRO) emptyES
let initialRS = RS M.empty (esScrollRequests eState) S.empty mempty []
brickChan <- newBChan 20
run initialVty initialRS st brickChan
supplyVtyEvents :: Vty -> BChan (BrickEvent n e) -> IO ()
supplyVtyEvents vty chan =
forever $ do
e <- nextEvent vty
writeBChan chan $ VtyEvent e
runVty :: (Ord n)
=> Vty
-> IO (BrickEvent n e)
-> App s e n
-> s
-> RenderState n
-> IO (Next s, RenderState n)
runVty vty readEvent app appState rs = do
(firstRS, exts) <- renderApp vty app appState rs
e <- readEvent
(e', nextRS, nextExts) <- case e of
-- If the event was a resize, redraw the UI to update the
-- viewport states before we invoke the event handler since we
-- want the event handler to have access to accurate viewport
-- information.
VtyEvent (EvResize _ _) -> do
(rs', exts') <- renderApp vty app appState $ firstRS & observedNamesL .~ S.empty
return (e, rs', exts')
VtyEvent (EvMouseDown c r button mods) -> do
let matching = findClickedExtents_ (c, r) exts
case matching of
(Extent n (Location (ec, er)) _ (Location (oC, oR)):_) ->
-- If the clicked extent was registered as
-- clickable, send a click event. Otherwise, just
-- send the raw mouse event
case n `elem` firstRS^.clickableNamesL of
True -> do
let localCoords = Location (lc, lr)
lc = c - ec + oC
lr = r - er + oR
return (MouseDown n button mods localCoords, firstRS, exts)
False -> return (e, firstRS, exts)
_ -> return (e, firstRS, exts)
VtyEvent (EvMouseUp c r button) -> do
let matching = findClickedExtents_ (c, r) exts
case matching of
(Extent n (Location (ec, er)) _ (Location (oC, oR)):_) ->
-- If the clicked extent was registered as
-- clickable, send a click event. Otherwise, just
-- send the raw mouse event
case n `elem` firstRS^.clickableNamesL of
True -> do
let localCoords = Location (lc, lr)
lc = c - ec + oC
lr = r - er + oR
return (MouseUp n button localCoords, firstRS, exts)
False -> return (e, firstRS, exts)
_ -> return (e, firstRS, exts)
_ -> return (e, firstRS, exts)
let emptyES = ES [] []
eventRO = EventRO (viewportMap nextRS) vty nextExts
(next, eState) <- runStateT (runReaderT (runEventM (appHandleEvent app appState e'))
eventRO) emptyES
return (next, nextRS { rsScrollRequests = esScrollRequests eState
, renderCache = applyInvalidations (cacheInvalidateRequests eState) $
renderCache nextRS
})
applyInvalidations :: (Ord n) => [CacheInvalidateRequest n] -> M.Map n v -> M.Map n v
applyInvalidations ns cache = foldr (.) id (mkFunc <$> ns) cache
where
mkFunc InvalidateEntire = const mempty
mkFunc (InvalidateSingle n) = M.delete n
-- | Given a viewport name, get the viewport's size and offset
-- information from the most recent rendering. Returns 'Nothing' if
-- no such state could be found, either because the name was invalid
-- or because no rendering has occurred (e.g. in an 'appStartEvent'
-- handler).
lookupViewport :: (Ord n) => n -> EventM n (Maybe Viewport)
lookupViewport n = EventM $ asks (M.lookup n . eventViewportMap)
-- | Did the specified mouse coordinates (column, row) intersect the
-- specified extent?
clickedExtent :: (Int, Int) -> Extent n -> Bool
clickedExtent (c, r) (Extent _ (Location (lc, lr)) (w, h) _) =
c >= lc && c < (lc + w) &&
r >= lr && r < (lr + h)
-- | Given a resource name, get the most recent rendering extent for the
-- name (if any).
lookupExtent :: (Eq n) => n -> EventM n (Maybe (Extent n))
lookupExtent n = EventM $ asks (listToMaybe . filter f . latestExtents)
where
f (Extent n' _ _ _) = n == n'
-- | Given a mouse click location, return the extents intersected by the
-- click. The returned extents are sorted such that the first extent in
-- the list is the most specific extent and the last extent is the most
-- generic (top-level). So if two extents A and B both intersected the
-- mouse click but A contains B, then they would be returned [B, A].
findClickedExtents :: (Int, Int) -> EventM n [Extent n]
findClickedExtents pos = EventM $ asks (findClickedExtents_ pos . latestExtents)
findClickedExtents_ :: (Int, Int) -> [Extent n] -> [Extent n]
findClickedExtents_ pos = reverse . filter (clickedExtent pos)
-- | Get the Vty handle currently in use.
getVtyHandle :: EventM n Vty
getVtyHandle = EventM $ asks eventVtyHandle
-- | Invalidate the rendering cache entry with the specified resource
-- name.
invalidateCacheEntry :: n -> EventM n ()
invalidateCacheEntry n = EventM $ do
lift $ modify (\s -> s { cacheInvalidateRequests = InvalidateSingle n : cacheInvalidateRequests s })
-- | Invalidate the entire rendering cache.
invalidateCache :: EventM n ()
invalidateCache = EventM $ do
lift $ modify (\s -> s { cacheInvalidateRequests = InvalidateEntire : cacheInvalidateRequests s })
withVty :: Vty -> (Vty -> IO a) -> IO a
withVty vty useVty = do
useVty vty `finally` shutdown vty
renderApp :: Vty -> App s e n -> s -> RenderState n -> IO (RenderState n, [Extent n])
renderApp vty app appState rs = do
sz <- displayBounds $ outputIface vty
let (newRS, pic, theCursor, exts) = renderFinal (appAttrMap app appState)
(appDraw app appState)
sz
(appChooseCursor app appState)
rs
picWithCursor = case theCursor of
Nothing -> pic { picCursor = NoCursor }
Just cloc -> pic { picCursor = AbsoluteCursor (cloc^.locationColumnL)
(cloc^.locationRowL)
}
update vty picWithCursor
return (newRS, exts)
-- | Ignore all requested cursor positions returned by the rendering
-- process. This is a convenience function useful as an
-- 'appChooseCursor' value when a simple application has no need to
-- position the cursor.
neverShowCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n)
neverShowCursor = const $ const Nothing
-- | Always show the first cursor, if any, returned by the rendering
-- process. This is a convenience function useful as an
-- 'appChooseCursor' value when a simple program has zero or more
-- widgets that advertise a cursor position.
showFirstCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n)
showFirstCursor = const listToMaybe
-- | Show the cursor with the specified resource name, if such a cursor
-- location has been reported.
showCursorNamed :: (Eq n) => n -> [CursorLocation n] -> Maybe (CursorLocation n)
showCursorNamed name locs =
let matches l = l^.cursorLocationNameL == Just name
in listToMaybe $ filter matches locs
-- | A viewport scrolling handle for managing the scroll state of
-- viewports.
data ViewportScroll n =
ViewportScroll { viewportName :: n
-- ^ The name of the viewport to be controlled by
-- this scrolling handle.
, hScrollPage :: Direction -> EventM n ()
-- ^ Scroll the viewport horizontally by one page in
-- the specified direction.
, hScrollBy :: Int -> EventM n ()
-- ^ Scroll the viewport horizontally by the
-- specified number of rows or columns depending on
-- the orientation of the viewport.
, hScrollToBeginning :: EventM n ()
-- ^ Scroll horizontally to the beginning of the
-- viewport.
, hScrollToEnd :: EventM n ()
-- ^ Scroll horizontally to the end of the viewport.
, vScrollPage :: Direction -> EventM n ()
-- ^ Scroll the viewport vertically by one page in
-- the specified direction.
, vScrollBy :: Int -> EventM n ()
-- ^ Scroll the viewport vertically by the specified
-- number of rows or columns depending on the
-- orientation of the viewport.
, vScrollToBeginning :: EventM n ()
-- ^ Scroll vertically to the beginning of the viewport.
, vScrollToEnd :: EventM n ()
-- ^ Scroll vertically to the end of the viewport.
, setTop :: Int -> EventM n ()
-- ^ Set the top row offset of the viewport.
, setLeft :: Int -> EventM n ()
-- ^ Set the left column offset of the viewport.
}
addScrollRequest :: (n, ScrollRequest) -> EventM n ()
addScrollRequest req = EventM $ do
lift $ modify (\s -> s { esScrollRequests = req : esScrollRequests s })
-- | Build a viewport scroller for the viewport with the specified name.
viewportScroll :: n -> ViewportScroll n
viewportScroll n =
ViewportScroll { viewportName = n
, hScrollPage = \dir -> addScrollRequest (n, HScrollPage dir)
, hScrollBy = \i -> addScrollRequest (n, HScrollBy i)
, hScrollToBeginning = addScrollRequest (n, HScrollToBeginning)
, hScrollToEnd = addScrollRequest (n, HScrollToEnd)
, vScrollPage = \dir -> addScrollRequest (n, VScrollPage dir)
, vScrollBy = \i -> addScrollRequest (n, VScrollBy i)
, vScrollToBeginning = addScrollRequest (n, VScrollToBeginning)
, vScrollToEnd = addScrollRequest (n, VScrollToEnd)
, setTop = \i -> addScrollRequest (n, SetTop i)
, setLeft = \i -> addScrollRequest (n, SetLeft i)
}
-- | Continue running the event loop with the specified application
-- state.
continue :: s -> EventM n (Next s)
continue = return . Continue
-- | Halt the event loop and return the specified application state as
-- the final state value.
halt :: s -> EventM n (Next s)
halt = return . Halt
-- | Suspend the event loop, save the terminal state, and run the
-- specified action. When it returns an application state value, restore
-- the terminal state, redraw the application from the new state, and
-- resume the event loop.
suspendAndResume :: IO s -> EventM n (Next s)
suspendAndResume = return . SuspendAndResume