tricorder-0.2.0.0: src/Tricorder/CLI/UI.hs
module Tricorder.CLI.UI
( viewUi
) where
import Atelier.Effects.Clock (Clock)
import Atelier.Effects.Conc (Conc)
import Atelier.Effects.Console (Console)
import Atelier.Effects.Delay (Delay)
import Atelier.Effects.File (File)
import Atelier.Effects.Process (Process, getExecutablePath, proc, runProcess)
import Brick (App (..), neverShowCursor)
import Brick.BChan (BChan, writeBChanNonBlocking)
import Brick.Keybindings (KeyConfig)
import Effectful.Concurrent (Concurrent)
import Effectful.Concurrent.STM (TVar, atomically, newTVarIO, readTVarIO, writeTVar)
import Effectful.Exception (bracket_, trySync)
import Effectful.Reader.Static (Reader, ask)
import Atelier.Effects.Conc qualified as Conc
import Tricorder.CLI.Daemon (waitForDaemon)
import Tricorder.CLI.UI.Brick (Brick)
import Tricorder.CLI.UI.BrickChan (BrickChan)
import Tricorder.CLI.UI.Event (Event (..), handleEvent)
import Tricorder.CLI.UI.Keys (KeyEvent, dispatcher)
import Tricorder.CLI.UI.State (State (..), Viewports (..))
import Tricorder.CLI.UI.View (mkAttrMap, view)
import Tricorder.Runtime (SocketPath (..))
import Tricorder.Socket.Client (queryWatch)
import Tricorder.Socket.UnixSocket (UnixSocket)
import Tricorder.CLI.UI.Brick qualified as Brick
import Tricorder.CLI.UI.BrickChan qualified as BrickChan
import Tricorder.CLI.UI.Keys qualified as Keys
import Tricorder.CLI.UI.State qualified as Model
-- | Connect to the daemon and render a live-updating build status display using
-- a brick TUI. Quits on @q@ or @Esc@; arrow keys scroll the viewport; @R@
-- restarts the daemon.
viewUi
:: ( Brick :> es
, BrickChan :> es
, Clock :> es
, Conc :> es
, Concurrent :> es
, Console :> es
, Delay :> es
, File :> es
, Process :> es
, Reader Keys.Config :> es
, Reader SocketPath :> es
, UnixSocket :> es
)
=> Eff es ()
viewUi = do
SocketPath sockPath <- ask
chan <- BrickChan.newBChan 10
restartChan <- BrickChan.newBChan 1
-- Set while a restart we triggered is in flight, so the watch loop stays
-- patient and reconnects to the fresh daemon instead of giving up.
restarting <- newTVarIO False
initialState <- Model.init
Conc.scoped do
_ <-
Conc.fork do
queryWatch sockPath (readTVarIO restarting)
$ BrickChan.writeBChan chan . NewBuildState
BrickChan.writeBChan chan $ FailedBuild "Lost contact with the daemon"
_ <- Conc.fork $ restartWorker restartChan restarting
keyConfig <- Keys.mkKeyConfig
let requestRestart = void $ writeBChanNonBlocking restartChan ()
void
$ Brick.runBrickApp
chan
(watchApp requestRestart keyConfig)
initialState
-- | Wait for restart requests from the TUI and service each one by spawning a
-- fresh @tricorder restart@ process.
--
-- The restart must happen out-of-process: starting the daemon double-forks (see
-- "System.Posix.Daemon"), and forking this vty-controlled process knocks the
-- terminal out of the raw mode brick set up, so keystrokes stop being captured.
-- Delegating to a separate process keeps the fork isolated. The brick event
-- handler signals us over @restartChan@ because it cannot run these effects.
restartWorker
:: ( BrickChan :> es
, Concurrent :> es
, Delay :> es
, Process :> es
, Reader SocketPath :> es
, UnixSocket :> es
)
=> BChan ()
-> TVar Bool
-> Eff es ()
restartWorker restartChan restarting = forever
$ bracket_
(BrickChan.readBChan restartChan >> atomically (writeTVar restarting True))
(atomically $ writeTVar restarting False)
do
self <- getExecutablePath
-- Swallow failures: a restart that errors must not take down the TUI, and
-- the flag has to be cleared either way.
_ <- trySync $ runProcess $ proc self ["restart"]
void waitForDaemon
watchApp :: IO () -> KeyConfig KeyEvent -> App State Event Viewports
watchApp requestRestart kc =
App
{ appDraw = view kc
, appHandleEvent = handleEvent $ dispatcher requestRestart kc
, appStartEvent = pure ()
, appAttrMap = mkAttrMap
, appChooseCursor = neverShowCursor
}