packages feed

termbox-tea-1.0.0: examples/Demo.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedRecordDot #-}

module Main (main) where

import Control.Concurrent (threadDelay)
import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar)
import Control.Monad (forever, guard)
import Data.Function ((&))
import GHC.Clock (getMonotonicTime)
import Ki qualified
import Termbox.Tea qualified as Termbox
import Text.Printf (printf)

main :: IO ()
main = do
  t0 <- getMonotonicTime
  result <-
    Ki.scoped \scope -> do
      timeVar <- newEmptyMVar
      Ki.fork_ scope do
        forever do
          threadDelay 100_000
          getMonotonicTime >>= putMVar timeVar
      Termbox.run
        Termbox.Program
          { initialize,
            pollEvent = pollEvent t0 timeVar,
            handleEvent,
            render,
            finished
          }
  putStrLn case result of
    Left err -> "Termbox program failed to initialize: " ++ show err
    Right state -> "Final state: " ++ show state

data State = State
  { elapsed :: !Double,
    terminalSize :: !Termbox.Size,
    lastKey :: !(Maybe Termbox.Key),
    lastMouse :: !(Maybe Termbox.Mouse),
    cursor :: !(Maybe (Termbox.Pos, Double)),
    bright :: !Bool
  }
  deriving stock (Show)

initialize :: Termbox.Size -> State
initialize terminalSize =
  State
    { elapsed = 0,
      terminalSize,
      lastKey = Nothing,
      lastMouse = Nothing,
      cursor = Nothing,
      bright = False
    }

pollEvent :: Double -> MVar Double -> Maybe (IO Double)
pollEvent t0 timeVar =
  Just do
    t1 <- takeMVar timeVar
    pure (t1 - t0)

handleEvent :: (Applicative m) => State -> Termbox.Event Double -> m State
handleEvent state = \case
  Termbox.EventKey key ->
    pure
      state
        { lastKey = Just key,
          bright =
            case key of
              Termbox.KeyChar '*' -> not state.bright
              _ -> state.bright
        }
  Termbox.EventMouse mouse ->
    pure
      state
        { lastMouse = Just mouse,
          cursor =
            case mouse.button of
              Termbox.ReleaseClick -> Just (mouse.pos, state.elapsed + 1)
              _ -> state.cursor
        }
  Termbox.EventResize terminalSize -> pure state {terminalSize}
  Termbox.EventUser elapsed ->
    pure
      state
        { elapsed,
          cursor = do
            (pos, erase) <- state.cursor
            guard (erase > elapsed)
            Just (pos, erase)
        }

render :: State -> Termbox.Scene
render state =
  vcat
    [ string "Welcome to Termbox. Try typing, clicking, and resizing the terminal!",
      string " ",
      hcat
        [ string "Elapsed time: ",
          string (printf "%.1fs" state.elapsed)
        ],
      hcat [string "Terminal size: ", string (show state.terminalSize)],
      hcat
        [ string "Latest key press: ",
          case state.lastKey of
            Nothing -> emptyBox
            Just event -> string (show event) & boldBox
        ],
      hcat
        [ string "Latest mouse click: ",
          case state.lastMouse of
            Nothing -> emptyBox
            Just event -> string (show event) & boldBox
        ],
      string " ",
      hcat
        [ let selected = bgBox (Termbox.gray 20) . fgBox (Termbox.gray 0)
           in vcat
                if state.bright
                  then [string " ", string "normal", selected (string "bright")]
                  else [string " ", selected (string "normal"), string "bright"],
          string " ",
          vcat
            [ string "default red green yellow blue magenta cyan white",
              hcat
                [ rect Termbox.Size {width = 7, height = 2} (brighten Termbox.defaultColor),
                  string " ",
                  rect Termbox.Size {width = 3, height = 2} (brighten Termbox.red),
                  string " ",
                  rect Termbox.Size {width = 5, height = 2} (brighten Termbox.green),
                  string " ",
                  rect Termbox.Size {width = 6, height = 2} (brighten Termbox.yellow),
                  string " ",
                  rect Termbox.Size {width = 4, height = 2} (brighten Termbox.blue),
                  string " ",
                  rect Termbox.Size {width = 7, height = 2} (brighten Termbox.magenta),
                  string " ",
                  rect Termbox.Size {width = 4, height = 2} (brighten Termbox.cyan),
                  string " ",
                  rect Termbox.Size {width = 5, height = 2} (brighten Termbox.white),
                  string " ",
                  hcat [string "Press ", boldBox (string "*"), string " to toggle brightness."]
                ]
            ]
        ],
      string " ",
      string "color 0 .. color 215",
      vcat $
        chunksOf 24 [0 .. 215] & map \is ->
          hcat $
            is & map \i ->
              acat
                [ rect Termbox.Size {width = 4, height = 2} (Termbox.color i),
                  string (show i) & bgBox (Termbox.color i)
                ],
      string " ",
      string "gray 0 .. gray 23",
      hcat $
        [0 .. 23] & map \i ->
          acat
            [ rect (Termbox.Size {width = 4, height = 2}) (Termbox.gray i),
              string (show i) & bgBox (Termbox.gray i)
            ],
      string " ",
      hcat
        [ string "This text is bold." & boldBox,
          string " ",
          string "This text is underlined." & underlineBox,
          string " ",
          string "This text is blinking (maybe)." & blinkBox
        ],
      string " ",
      hcat
        [ string "Press ",
          string "Esc" & boldBox,
          string " to quit!"
        ]
    ]
    & boxImage
    & Termbox.at Termbox.Pos {row = 1, col = 2}
    & Termbox.image
    & case state.cursor of
      Nothing -> id
      Just (pos, _) -> Termbox.cursor pos
    & Termbox.fill (Termbox.gray 4)
  where
    brighten :: Termbox.Color -> Termbox.Color
    brighten =
      if state.bright
        then Termbox.bright
        else id

finished :: State -> Bool
finished State {lastKey} =
  case lastKey of
    Just Termbox.KeyEsc -> True
    _ -> False

data Box
  = Box !Termbox.Size !Termbox.Image

emptyBox :: Box
emptyBox =
  Box (Termbox.Size 0 0) mempty

boxImage :: Box -> Termbox.Image
boxImage (Box _size image) =
  image

fgBox :: Termbox.Color -> Box -> Box
fgBox color (Box size image) =
  Box size (Termbox.fg color image)

bgBox :: Termbox.Color -> Box -> Box
bgBox color (Box size image) =
  Box size (Termbox.bg color image)

boldBox :: Box -> Box
boldBox (Box size image) =
  Box size (Termbox.bold image)

underlineBox :: Box -> Box
underlineBox (Box size image) =
  Box size (Termbox.underline image)

blinkBox :: Box -> Box
blinkBox (Box size image) =
  Box size (Termbox.blink image)

one :: Char -> Box
one char =
  Box (Termbox.Size 1 1) (Termbox.char char)

acat :: [Box] -> Box
acat =
  foldr f emptyBox
  where
    f :: Box -> Box -> Box
    f (Box (Termbox.Size w1 h1) i1) (Box (Termbox.Size w2 h2) i2) =
      Box (Termbox.Size (max w1 w2) (max h1 h2)) (i1 <> i2)

hcat :: [Box] -> Box
hcat =
  foldr f emptyBox
  where
    f :: Box -> Box -> Box
    f (Box (Termbox.Size w1 h1) i1) (Box (Termbox.Size w2 h2) i2) =
      Box (Termbox.Size (w1 + w2) (max h1 h2)) (i1 <> Termbox.atCol w1 i2)

vcat :: [Box] -> Box
vcat =
  foldr f emptyBox
  where
    f :: Box -> Box -> Box
    f (Box (Termbox.Size w1 h1) i1) (Box (Termbox.Size w2 h2) i2) =
      Box (Termbox.Size (max w1 w2) (h1 + h2)) (i1 <> Termbox.atRow h1 i2)

string :: [Char] -> Box
string =
  hcat . map one

rect :: Termbox.Size -> Termbox.Color -> Box
rect Termbox.Size {width, height} color =
  bgBox color (vcat (replicate height (string (replicate width ' '))))

chunksOf :: Int -> [a] -> [[a]]
chunksOf n = \case
  [] -> []
  xs ->
    let (ys, zs) = splitAt n xs
     in ys : chunksOf n zs