packages feed

swarm-0.5.0.0: src/Swarm/App.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Main entry point for the Swarm application.
module Swarm.App where

import Brick
import Brick.BChan
import Control.Carrier.Lift (runM)
import Control.Carrier.Throw.Either (runThrow)
import Control.Concurrent (forkIO, threadDelay)
import Control.Lens (view, (%~), (&), (?~))
import Control.Monad (forever, void, when)
import Control.Monad.IO.Class (liftIO)
import Data.IORef (newIORef, writeIORef)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Graphics.Vty qualified as V
import Swarm.Game.Failure (SystemFailure)
import Swarm.Language.Pretty (prettyText)
import Swarm.Log (LogSource (SystemLog), Severity (..))
import Swarm.ReadableIORef (mkReadonly)
import Swarm.TUI.Controller
import Swarm.TUI.Model
import Swarm.TUI.Model.StateUpdate
import Swarm.TUI.Model.UI (uiAttrMap)
import Swarm.TUI.View
import Swarm.Version (getNewerReleaseVersion)
import Swarm.Web
import System.IO (stderr)

type EventHandler = BrickEvent Name AppEvent -> EventM Name AppState ()

-- | The configuration of the Swarm app which we pass to the @brick@
--   library.
app :: EventHandler -> App AppState AppEvent Name
app eventHandler =
  App
    { appDraw = drawUI
    , appChooseCursor = chooseCursor
    , appHandleEvent = eventHandler
    , appStartEvent = enablePasteMode
    , appAttrMap = view $ uiState . uiAttrMap
    }

-- | The main @IO@ computation which initializes the state, sets up
--   some communication channels, and runs the UI.
appMain :: AppOpts -> IO ()
appMain opts = do
  res <- runM . runThrow $ initAppState opts
  case res of
    Left err -> T.hPutStrLn stderr (prettyText @SystemFailure err)
    Right s -> do
      -- Send Frame events as at a reasonable rate for 30 fps. The
      -- game is responsible for figuring out how many steps to take
      -- each frame to achieve the desired speed, regardless of the
      -- frame rate.  Note that if the game cannot keep up with 30
      -- fps, it's not a problem: the channel will fill up and this
      -- thread will block.  So the force of the threadDelay is just
      -- to set a *maximum* possible frame rate.
      --
      -- 5 is the size of the bounded channel; when it gets that big,
      -- any writes to it will block.  Probably 1 would work fine,
      -- though it seems like it could be good to have a bit of buffer
      -- just so the app never has to wait for the thread to wake up
      -- and do another write.

      chan <- newBChan 5
      _ <- forkIO $
        forever $ do
          threadDelay 33_333 -- cap maximum framerate at 30 FPS
          writeBChan chan Frame

      _ <- forkIO $ do
        upRel <- getNewerReleaseVersion (repoGitInfo opts)
        writeBChan chan (UpstreamVersion upRel)

      -- Start the web service with a reference to the game state.
      -- NOTE: This reference should be considered read-only by
      -- the web service; the game alone shall host the canonical state.
      appStateRef <- newIORef s
      eport <-
        Swarm.Web.startWebThread
          (userWebPort opts)
          (mkReadonly appStateRef)
          chan

      let logP p = logEvent SystemLog Info "Web API" ("started on :" <> T.pack (show p))
      let logE e = logEvent SystemLog Error "Web API" (T.pack e)
      let s' =
            s
              & runtimeState
                %~ case eport of
                  Right p -> (webPort ?~ p) . (eventLog %~ logP p)
                  Left e -> eventLog %~ logE e

      -- Update the reference for every event
      let eventHandler e = do
            curSt <- get
            liftIO $ writeIORef appStateRef curSt
            handleEvent e

      -- Setup virtual terminal
      let buildVty = V.mkVty $ V.defaultConfig {V.colorMode = colorMode opts}
      initialVty <- buildVty
      V.setMode (V.outputIface initialVty) V.Mouse True

      -- Run the app.
      void $ customMain initialVty buildVty (Just chan) (app eventHandler) s'

-- | A demo program to run the web service directly, without the terminal application.
--   This is useful to live update the code using @ghcid -W --test "Swarm.App.demoWeb"@.
demoWeb :: IO ()
demoWeb = do
  let demoPort = 8080
  res <-
    runM . runThrow $ initAppState (defaultAppOpts {userScenario = demoScenario})
  case res of
    Left err -> T.putStrLn (prettyText @SystemFailure err)
    Right s -> do
      appStateRef <- newIORef s
      chan <- newBChan 5
      webMain
        Nothing
        demoPort
        (mkReadonly appStateRef)
        chan
 where
  demoScenario = Just "./data/scenarios/Testing/475-wait-one.yaml"

-- | If available for the terminal emulator, enable bracketed paste mode.
enablePasteMode :: EventM n s ()
enablePasteMode = do
  vty <- getVtyHandle
  let output = V.outputIface vty
  when (V.supportsMode output V.BracketedPaste) $
    liftIO $
      V.setMode output V.BracketedPaste True