packages feed

brick 0.24 → 0.24.1

raw patch · 6 files changed

+650/−4 lines, 6 filesdep ~vtynew-component:exe:brick-testingbinary-addedPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: vty

API changes (from Hackage documentation)

- Brick.Widgets.Dialog: dialogButtonsL :: forall a_a14wH a_a14xh. Lens (Dialog a_a14wH) (Dialog a_a14xh) [(String, a_a14wH)] [(String, a_a14xh)]
+ Brick.Widgets.Dialog: dialogButtonsL :: forall a_a14zk a_a14zU. Lens (Dialog a_a14zk) (Dialog a_a14zU) [(String, a_a14zk)] [(String, a_a14zU)]
- Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_a14wH. Lens' (Dialog a_a14wH) (Maybe Int)
+ Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_a14zk. Lens' (Dialog a_a14zk) (Maybe Int)
- Brick.Widgets.Dialog: dialogTitleL :: forall a_a14wH. Lens' (Dialog a_a14wH) (Maybe String)
+ Brick.Widgets.Dialog: dialogTitleL :: forall a_a14zk. Lens' (Dialog a_a14zk) (Maybe String)
- Brick.Widgets.Dialog: dialogWidthL :: forall a_a14wH. Lens' (Dialog a_a14wH) Int
+ Brick.Widgets.Dialog: dialogWidthL :: forall a_a14zk. Lens' (Dialog a_a14zk) Int
- Brick.Widgets.Edit: editContentsL :: forall t_a16hR n_a16hS t_a16it. Lens (Editor t_a16hR n_a16hS) (Editor t_a16it n_a16hS) (TextZipper t_a16hR) (TextZipper t_a16it)
+ Brick.Widgets.Edit: editContentsL :: forall t_a16ku n_a16kv t_a16l6. Lens (Editor t_a16ku n_a16kv) (Editor t_a16l6 n_a16kv) (TextZipper t_a16ku) (TextZipper t_a16l6)
- Brick.Widgets.List: listElementsL :: forall n_a185N e_a185O e_a18g2. Lens (List n_a185N e_a185O) (List n_a185N e_a18g2) (Vector e_a185O) (Vector e_a18g2)
+ Brick.Widgets.List: listElementsL :: forall n_a188q e_a188r e_a18iF. Lens (List n_a188q e_a188r) (List n_a188q e_a18iF) (Vector e_a188r) (Vector e_a18iF)
- Brick.Widgets.List: listItemHeightL :: forall n_a185N e_a185O. Lens' (List n_a185N e_a185O) Int
+ Brick.Widgets.List: listItemHeightL :: forall n_a188q e_a188r. Lens' (List n_a188q e_a188r) Int
- Brick.Widgets.List: listNameL :: forall n_a185N e_a185O n_a18g3. Lens (List n_a185N e_a185O) (List n_a18g3 e_a185O) n_a185N n_a18g3
+ Brick.Widgets.List: listNameL :: forall n_a188q e_a188r n_a18iG. Lens (List n_a188q e_a188r) (List n_a18iG e_a188r) n_a188q n_a18iG
- Brick.Widgets.List: listSelectedL :: forall n_a185N e_a185O. Lens' (List n_a185N e_a185O) (Maybe Int)
+ Brick.Widgets.List: listSelectedL :: forall n_a188q e_a188r. Lens' (List n_a188q e_a188r) (Maybe Int)

Files

CHANGELOG.md view
@@ -2,13 +2,24 @@ Brick changelog --------------- +0.24.1+------++Bug fixes:+ * vBox/hBox: when there is leftover space and all elements are greedy,+   spread it amongst the elements as evenly as possible instead of+   assigning it all to the first element (fixes #133)++Package changes:+ * Include Sam Tay's brick tutorial files in extra-doc-files+ 0.24 ----  API changes:  * Added Brick.Widgets.Core.setAvailableSize to control rendering    context size in cases where the screen size is too constraining (e.g.-   for a floating layer that might be bigger than the screen)          .+   for a floating layer that might be bigger than the screen).  Documentation changes:  * Samuel Tay has contributed his wonderful Brick tutorial to this
brick.cabal view
@@ -1,5 +1,5 @@ name:                brick-version:             0.24+version:             0.24.1 synopsis:            A declarative terminal user interface library description:   Write terminal applications painlessly with 'brick'! You write an@@ -40,6 +40,8 @@  extra-doc-files:     README.md,                      docs/guide.rst,+                     docs/samtay-tutorial.md,+                     docs/snake-demo.gif,                      CHANGELOG.md  Source-Repository head@@ -95,6 +97,19 @@                        template-haskell,                        deepseq >= 1.3 && < 1.5,                        word-wrap >= 0.2++executable brick-testing+  if !flag(demos)+    Buildable: False+  hs-source-dirs:      programs+  ghc-options:         -threaded -Wall -fno-warn-unused-do-bind -O3+  default-language:    Haskell2010+  default-extensions:  CPP+  main-is:             Testing.hs+  build-depends:       base,+                       brick,+                       vty,+                       text  executable brick-readme-demo   if !flag(demos)
+ docs/samtay-tutorial.md view
@@ -0,0 +1,541 @@++# Brick Tutorial by Samuel Tay++This tutorial was written by Samuel Tay, Copyright 2017+(https://github.com/samtay, https://samtay.github.io/). It is provided+as part of the brick distribution with permission.++## Introduction++I'm going to give a short introduction to+[brick](https://hackage.haskell.org/package/brick), a Haskell library+for building terminal user interfaces. So far I've used `brick` to+implement [Conway's Game of Life](https://github.com/samtay/conway) and+a [Tetris clone](https://github.com/samtay/tetris). I'll explain the+basics, walk through an example [snake](https://github.com/samtay/snake)+application, and then explain some more complicated scenarios.++The first thing I'll say is that this package has some of the most+impressive documentation and resources, which makes it easy to figure+out pretty much anything you need to do. I'll try to make this useful,+but I imagine if you're reading this then it is mostly being used as a+reference in addition to the existing resources:++1. [Demo programs](https://github.com/jtdaugherty/brick/tree/master/programs)+(clone down to explore the code and run them locally)+2. [User guide](https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst)+3. [Haddock docs](https://hackage.haskell.org/package/brick-0.18)+4. [Google group](https://groups.google.com/forum/#!forum/brick-users)++### The basic idea++`brick` is very declarative. Once your base application logic is in+place, the interface is generally built by two functions: drawing and+handling events. The drawing function++```haskell+appDraw :: s -> [Widget n]+```++takes your app state `s` and produces the visuals `[Widget n]`. The+handler++```haskell+appHandleEvent :: s -> BrickEvent n e -> EventM n (Next s)+```++takes your app state, an event (e.g. user presses the `'m'` key), and+produces the resulting app state. *That's pretty much it.*++## `snake`++We're going to build the [classic+snake](https://en.wikipedia.org/wiki/Snake_(video_game)) game that you+might recall from arcades or the first cell phones. The full source code+is [here](https://github.com/samtay/snake). This is the end product:++![](snake-demo.gif)++### Structure of the app++The library makes it easy to separate the concerns of your application+and the interface; I like to have a module with all of the core business+logic that exports the core state of the app and functions for modifying+it, and then have an interface module that just handles the setup,+drawing, and handling events. So let's just use the `simple` stack+template and add two modules++```+├── LICENSE+├── README.md+├── Setup.hs+├── snake.cabal+├── src+│   ├── Main.hs+│   ├── Snake.hs+│   └── UI.hs+└── stack.yaml+```++and our dependencies to `test.cabal`++```yaml+executable snake+  hs-source-dirs:      src+  main-is:             Main.hs+  exposed-modules:     Snake+                     , UI+  default-language:    Haskell2010+  build-depends:       base >= 4.7 && < 5+                     , brick+                     , containers+                     , linear+                     , microlens+                     , microlens-th+                     , random+```++### `Snake`++Since this tutorial is about `brick`, I'll elide most of the+implementation details of the actual game, but here are some of the key+types and scaffolding:++```haskell+{-# LANGUAGE TemplateHaskell, FlexibleContexts #-}+module Snake where++import Control.Applicative ((<|>))+import Control.Monad (guard)+import Data.Maybe (fromMaybe)++import Data.Sequence (Seq, ViewL(..), ViewR(..), (<|))+import qualified Data.Sequence as S+import Lens.Micro.TH (makeLenses)+import Lens.Micro ((&), (.~), (%~), (^.))+import Linear.V2 (V2(..), _x, _y)+import System.Random (Random(..), newStdGen)++-- Types++data Game = Game+  { _snake  :: Snake        -- ^ snake as a sequence of points in R2+  , _dir    :: Direction    -- ^ direction+  , _food   :: Coord        -- ^ location of the food+  , _foods  :: Stream Coord -- ^ infinite list of random food locations+  , _dead   :: Bool         -- ^ game over flag+  , _paused :: Bool         -- ^ paused flag+  , _score  :: Int          -- ^ score+  , _frozen :: Bool         -- ^ freeze to disallow duplicate turns+  } deriving (Show)++type Coord = V2 Int+type Snake = Seq Coord++data Stream a = a :| Stream a+  deriving (Show)++data Direction+  = North+  | South+  | East+  | West+  deriving (Eq, Show)+```++All of this is pretty self-explanatory, with the possible exception+of lenses if you haven't seen them. At first glance they may seem+complicated (and the underlying theory arguably is), but using them as+getters and setters is very straightforward. So, if you are following+along because you are writing a terminal app like this, I'd recommend+using them, but they are not required to use `brick`.++Here are the core functions for playing the game:++```haskell+-- | Step forward in time+step :: Game -> Game+step g = fromMaybe g $ do+  guard (not $ g ^. paused || g ^. dead)+  let g' = g & frozen .~ False+  return . fromMaybe (move g') $ die g' <|> eatFood g'++-- | Possibly die if next head position is disallowed+die :: Game -> Maybe Game++-- | Possibly eat food if next head position is food+eatFood :: Game -> Maybe Game++-- | Move snake along in a marquee fashion+move :: Game -> Game++-- | Turn game direction (only turns orthogonally)+--+-- Implicitly unpauses yet freezes game+turn :: Direction -> Game -> Game++-- | Initialize a paused game with random food location+initGame :: IO Game+```++### `UI`++To start, we need to determine what our `App s e n` type parameters are.+This will completely describe the interface application and be passed+to one of the library's `main` style functions for execution. Note that+`s` is the app state, `e` is an event type, and `n` is a resource name.+The `e` is abstracted so that we can provide custom events. The `n`+is usually a custom sum type called `Name` which allows us to *name*+particular viewports. This is important so that we can keep track of+where the user currently has *focus*, such as typing in one of two+textboxes; however, for this simple snake game we don't need to worry+about that.++In simpler cases, the state `s` can directly coincide with a core+datatype such as our `Snake.Game`. In many cases however, it will be+necessary to wrap the core state within the ui state `s` to keep track+of things that are interface specific (more on this later).++Let's write out our app definition and leave some undefined functions:++```haskell+{-# LANGUAGE OverloadedStrings #-}+module UI where++import Control.Monad (forever, void)+import Control.Monad.IO.Class (liftIO)+import Control.Concurrent (threadDelay, forkIO)+import Data.Maybe (fromMaybe)++import Snake++import Brick+  ( App(..), AttrMap, BrickEvent(..), EventM, Next, Widget+  , customMain, neverShowCursor+  , continue, halt+  , hLimit, vLimit, vBox, hBox+  , padRight, padLeft, padTop, padAll, Padding(..)+  , withBorderStyle+  , str+  , attrMap, withAttr, emptyWidget, AttrName, on, fg+  , (<+>)+  )+import Brick.BChan (newBChan, writeBChan)+import qualified Brick.Widgets.Border as B+import qualified Brick.Widgets.Border.Style as BS+import qualified Brick.Widgets.Center as C+import qualified Graphics.Vty as V+import Data.Sequence (Seq)+import qualified Data.Sequence as S+import Linear.V2 (V2(..))+import Lens.Micro ((^.))++-- Types++-- | Ticks mark passing of time+--+-- This is our custom event that will be constantly fed into the app.+data Tick = Tick++-- | Named resources+--+-- Not currently used, but will be easier to refactor+-- if we call this "Name" now.+type Name = ()++data Cell = Snake | Food | Empty++-- App definition++app :: App Game Tick Name+app = App { appDraw = drawUI+          , appChooseCursor = neverShowCursor+          , appHandleEvent = handleEvent+          , appStartEvent = return+          , appAttrMap = const theMap+          }++main :: IO ()+main = undefined++-- Handling events++handleEvent :: Game -> BrickEvent Name Tick -> EventM Name (Next Game)+handleEvent = undefined++-- Drawing++drawUI :: Game -> [Widget Name]+drawUI = undefined++theMap :: AttrMap+theMap = undefined+```++#### Custom Events++So far I've only used `brick` to make games which need to be redrawn+as time passes, with or without user input. This requires using+`Brick.customMain` with that `Tick` event type, and opening a forked+process to `forever` feed that event type into the channel. Since this+is a common scenario, there is a `Brick.BChan` module that makes this+pretty quick:++```haskell+main :: IO ()+main = do+  chan <- newBChan 10+  forkIO $ forever $ do+    writeBChan chan Tick+    threadDelay 100000 -- decides how fast your game moves+  g <- initGame+  void $ customMain (V.mkVty V.defaultConfig) (Just chan) app g+```++We do need to import `Vty.Graphics` since `customMain` allows us+to specify a custom `IO Vty.Graphics.Vty` handle, but we're only+customizing the existence of the event channel `BChan Tick`. The app+is now bootstrapped, and all we need to do is implement `handleEvent`,+`drawUI`, and `theMap` (handles styling).++#### Handling events++Handling events is largely straightforward, and can be very clean when+your underlying application logic is taken care of in a core module. All+we do is essentially map events to the proper state modifiers.++```haskell+handleEvent :: Game -> BrickEvent Name Tick -> EventM Name (Next Game)+handleEvent g (AppEvent Tick)                       = continue $ step g+handleEvent g (VtyEvent (V.EvKey V.KUp []))         = continue $ turn North g+handleEvent g (VtyEvent (V.EvKey V.KDown []))       = continue $ turn South g+handleEvent g (VtyEvent (V.EvKey V.KRight []))      = continue $ turn East g+handleEvent g (VtyEvent (V.EvKey V.KLeft []))       = continue $ turn West g+handleEvent g (VtyEvent (V.EvKey (V.KChar 'k') [])) = continue $ turn North g+handleEvent g (VtyEvent (V.EvKey (V.KChar 'j') [])) = continue $ turn South g+handleEvent g (VtyEvent (V.EvKey (V.KChar 'l') [])) = continue $ turn East g+handleEvent g (VtyEvent (V.EvKey (V.KChar 'h') [])) = continue $ turn West g+handleEvent g (VtyEvent (V.EvKey (V.KChar 'r') [])) = liftIO (initGame) >>= continue+handleEvent g (VtyEvent (V.EvKey (V.KChar 'q') [])) = halt g+handleEvent g (VtyEvent (V.EvKey V.KEsc []))        = halt g+handleEvent g _                                     = continue g+```++It's probably obvious, but `continue` will continue execution with+the supplied state value, which is then drawn. We can also `halt` to+stop execution, which will essentially finish the evaluation of our+`customMain` and result in `IO Game`, where the resulting game is the+last value that we supplied to `halt`.++#### Drawing++Drawing is fairly simple as well but can require a good amount of code+to position things how you want them. I like to break up the visual+space into regions with drawing functions for each one.++```haskell+drawUI :: Game -> [Widget Name]+drawUI g =+  [ C.center $ padRight (Pad 2) (drawStats g) <+> drawGrid g ]++drawStats :: Game -> Widget Name+drawStats = undefined++drawGrid :: Game -> Widget Name+drawGrid = undefined+```++This will center the overall interface (`C.center`), put the stats and+grid widgets horizontally side by side (`<+>`), and separate them by a+2-character width (`padRight (Pad 2)`).++Let's move forward with the stats column:++```haskell+drawStats :: Game -> Widget Name+drawStats g = hLimit 11+  $ vBox [ drawScore (g ^. score)+         , padTop (Pad 2) $ drawGameOver (g ^. dead)+         ]++drawScore :: Int -> Widget Name+drawScore n = withBorderStyle BS.unicodeBold+  $ B.borderWithLabel (str "Score")+  $ C.hCenter+  $ padAll 1+  $ str $ show n++drawGameOver :: Bool -> Widget Name+drawGameOver dead =+  if dead+     then withAttr gameOverAttr $ C.hCenter $ str "GAME OVER"+     else emptyWidget++gameOverAttr :: AttrName+gameOverAttr = "gameOver"+```++I'm throwing in that `hLimit 11` to prevent the widget greediness caused+by the outer `C.center`. I'm also using `vBox` to show some other+options of aligning widgets; `vBox` and `hBox` align a list of widgets+vertically and horizontally, respectfully. They can be thought of as+folds over the binary `<=>` and `<+>` operations.++The score is straightforward, but it is the first border in+this tutorial. Borders are well documented in the [border+demo](https://github.com/jtdaugherty/brick/blob/master/programs/BorderDe+mo.hs) and the Haddocks for that matter.++We also only show the "game over" widget if the game is actually over.+In that case, we are rendering the string widget with the `gameOverAttr`+attribute name. Attribute names are basically type safe *names* that+we can assign to widgets to apply predetermined styles, similar to+assigning a class name to a div in HTML and defining the CSS styles for+that class elsewhere.++Attribute names implement `IsString`, so they are easy to construct with+the `OverloadedStrings` pragma.++Now for the main event:++```haskell+drawGrid :: Game -> Widget Name+drawGrid g = withBorderStyle BS.unicodeBold+  $ B.borderWithLabel (str "Snake")+  $ vBox rows+  where+    rows         = [hBox $ cellsInRow r | r <- [height-1,height-2..0]]+    cellsInRow y = [drawCoord (V2 x y) | x <- [0..width-1]]+    drawCoord    = drawCell . cellAt+    cellAt c+      | c `elem` g ^. snake = Snake+      | c == g ^. food      = Food+      | otherwise           = Empty++drawCell :: Cell -> Widget Name+drawCell Snake = withAttr snakeAttr cw+drawCell Food  = withAttr foodAttr cw+drawCell Empty = withAttr emptyAttr cw++cw :: Widget Name+cw = str "  "++snakeAttr, foodAttr, emptyAttr :: AttrName+snakeAttr = "snakeAttr"+foodAttr = "foodAttr"+emptyAttr = "emptyAttr"++```++There's actually nothing new here! We've already covered all the+`brick` functions necessary to draw the grid. My approach to grids is+to render a square cell widget `cw` with different colors depending+on the cell state. The easiest way to draw a colored square is to+stick two characters side by side. If we assign an attribute with a+matching foreground and background, then it doesn't matter what the two+characters are (provided that they aren't some crazy Unicode characters+that might render to an unexpected size). However, if we want empty+cells to render with the same color as the user's default background+color, then spaces are a good choice.++Finally, we'll define the attribute map:++```haskell+theMap :: AttrMap+theMap = attrMap V.defAttr+  [ (snakeAttr, V.blue `on` V.blue)+  , (foodAttr, V.red `on` V.red)+  , (gameOverAttr, fg V.red `V.withStyle` V.bold)+  ]+```++Again, styles aren't terribly complicated, but it+will be one area where you might have to look in the+[vty](http://hackage.haskell.org/package/vty) package (specifically+[Graphics.Vty.Attributes](http://hackage.haskell.org/package/vty-5.15.1/docs/Graphics-Vty-Attributes.html)) to find what you need.++Another thing to mention is that the attributes form a hierarchy and+can be combined in a parent-child relationship via `mappend`. I haven't+actually used this feature, but it does sound quite handy. For a more+detailed discussion see the+[Brick.AttrMap](https://hackage.haskell.org/package/brick-0.18/docs/Brick-AttrMap.html) haddocks.++## Variable speed++One difficult problem I encountered was implementing a variable speed in+the GoL. I could have just used the same approach above with the minimum+thread delay (corresponding to the maximum speed) and counted `Tick`+events, only issuing an actual `step` in the game when the modular count+of `Tick`s reached an amount corresponding to the current game speed,+but that's kind of an ugly approach.++Instead, I reached out to the author and he advised me to use a `TVar`+within the app state.  I had never used `TVar`, but it's pretty easy!++```haskell+main :: IO ()+main = do+  chan <- newBChan 10+  tv   <- atomically $ newTVar (spToInt initialSpeed)+  forkIO $ forever $ do+    writeBChan chan Tick+    int <- atomically $ readTVar tv+    threadDelay int+  customMain (V.mkVty V.defaultConfig) (Just chan) app (initialGame tv)+    >>= printResult+```++The `tv <- atomically $ newTVar (value :: a)` creates a new mutable+reference to a value of type `a`, i.e. `TVar a`, and returns it in `IO`.+In this case `value` is an `Int` which represents the delay between game+steps. Then in the forked process, we read the delay from the `TVar`+reference and use that to space out the calls to `writeBChan chan Tick`.++I store that same `tv :: TVar Int` in the brick app state, so that the+user can change the speed:++```haskell+handleEvent :: Game -> BrickEvent Name Tick -> EventM Name (Next Game)+handleEvent g (VtyEvent (V.EvKey V.KRight [V.MCtrl])) = handleSpeed g (+)+handleEvent g (VtyEvent (V.EvKey V.KLeft [V.MCtrl]))  = handleSpeed g (-)++handleSpeed :: Game -> (Float -> Float -> Float) -> EventM n (Next Game)+handleSpeed g (+/-) = do+  let newSp = validS $ (g ^. speed) +/- speedInc+  liftIO $ atomically $ writeTVar (g ^. interval) (spToInt newSp)+  continue $ g & speed .~ newSp++-- where++-- | Speed increments = 0.01 gives 100 discrete speed settings+speedInc :: Float+speedInc = 0.01++-- | Game state+data Game = Game+  { _board    :: Board -- ^ Board state+  , _time     :: Int   -- ^ Time elapsed+  , _paused   :: Bool  -- ^ Playing vs. paused+  , _speed    :: Float -- ^ Speed in [0..1]+  , _interval :: TVar Int -- ^ Interval kept in TVar+  , _focus    :: F.FocusRing Name -- ^ Keeps track of grid focus+  , _selected :: Cell -- ^ Keeps track of cell focus+  }+```++## Conclusion++`brick` let's you build TUIs very quickly. I was able to write `snake`+along with this tutorial within a few hours. More complicated interfaces+can be tougher, but if you can successfully separate the interface and+core functionality, you'll have an easier time tacking on the frontend.++Lastly, let me remind you to look in the+[demo programs](https://github.com/jtdaugherty/brick/tree/master/programs)+to figure stuff out, as *many* scenarios are covered throughout them.++## Links+* [brick](https://hackage.haskell.org/package/brick)+* [snake](https://github.com/samtay/snake)+* [tetris](https://github.com/samtay/tetris)+* [conway](https://github.com/samtay/conway)
+ docs/snake-demo.gif view

binary file changed (absent → 406517 bytes)

+ programs/Testing.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ViewPatterns #-}++module Main where++import Control.Monad (void)+import Data.Function ((&))+import Data.Function (on)+import Data.List     (transpose)++import qualified Brick                as B+import qualified Brick.Widgets.Core   as B+import qualified Brick.Widgets.Border as B+import qualified Brick.Widgets.Center as B+import qualified Graphics.Vty         as V++type Name = ()+data Tick+data Game++app :: B.App Game Tick Name+app = B.App+    { B.appDraw         = drawUI+    , B.appChooseCursor = B.neverShowCursor+    , B.appHandleEvent  = handleEvent+    , B.appStartEvent   = pure+    , B.appAttrMap      = const theMap+    }++theMap :: B.AttrMap+theMap = B.attrMap V.defAttr+    [ (blackSquare, go V.withBackColor V.black)+    , (redSquare, go V.withBackColor V.red)+    ]+  where+    go a b = a V.defAttr b++blackSquare, redSquare :: B.AttrName+blackSquare = "blackSquare"+redSquare = "redSquare"++vkey :: B.BrickEvent Name Tick -> Maybe V.Key+vkey (B.VtyEvent (V.EvKey y [])) = Just y+vkey _ = Nothing++handleEvent :: Game -> B.BrickEvent Name Tick -> B.EventM Name (B.Next Game)+handleEvent g (vkey -> Just (V.KChar 'q')) = g & B.halt+handleEvent g (vkey -> Just (V.KEsc))      = g & B.halt+handleEvent g (vkey -> Nothing)            = g & B.continue+handleEvent g _                            = g & B.continue++drawUI :: Game -> [B.Widget Name]+drawUI = pure . drawBoard++boardNumbers :: Game -> [[B.Widget Name]]+boardNumbers _ =+    board+  where+    (.:) = (.) . (.)++    ap = take 8 .: zipC `on` repeat++    board    = ap oddRows evenRows+    oddRows  = ap reds    blacks+    evenRows = ap blacks  reds++    zipC = (concat . transpose) .: mappend `on` pure++    pattern p = B.withAttr p . B.center . B.str $ " "++    reds   = pattern redSquare+    blacks = pattern blackSquare++drawBoard :: Game -> B.Widget Name+drawBoard = B.border . B.vBox . fmap B.hBox . boardNumbers++main :: IO ()+main = void $ B.defaultMain app undefined
src/Brick/Widgets/Core.hs view
@@ -501,9 +501,10 @@               let remainingPrimary = c^.(contextPrimary br) -                                      (sum $ (^._2.imageL.(to $ imagePrimary br)) <$> renderedHis)                   primaryPerLow = remainingPrimary `div` length ls-                  padFirst = remainingPrimary - (primaryPerLow * length ls)+                  rest = remainingPrimary - (primaryPerLow * length ls)                   secondaryPerLow = c^.(contextSecondary br)-                  primaries = replicate (length ls) primaryPerLow & ix 0 %~ (+ padFirst)+                  primaries = replicate rest (primaryPerLow + 1) <>+                              replicate (length ls - rest) primaryPerLow                let renderLow ((i, prim), pri) =                       (i,) <$> (render $ limitPrimary br pri