ghcitui-0.1.0.0: lib/ghcitui-brick/Ghcitui/Brick/BrickUI.hs
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
module Ghcitui.Brick.BrickUI
( launchBrick
, AppState (..)
) where
import qualified Brick as B
import qualified Brick.Widgets.Border as B
import qualified Brick.Widgets.Center as B
import Brick.Widgets.Core ((<+>), (<=>))
import qualified Brick.Widgets.Dialog as B
import qualified Brick.Widgets.Edit as BE
import Control.Error (headMay)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Graphics.Vty as V
import Lens.Micro ((&), (^.))
import qualified Text.Wrap as Wrap
import qualified Ghcitui.Brick.AppConfig as AppConfig
import qualified Ghcitui.Brick.AppInterpState as AIS
import Ghcitui.Brick.AppState
( ActiveWindow (..)
, AppState (..)
, appInterpState
, liveEditor
, makeInitialState
)
import qualified Ghcitui.Brick.AppState as AppState
import Ghcitui.Brick.AppTopLevel (AppName (..))
import qualified Ghcitui.Brick.DrawSourceViewer as DrawSourceViewer
import qualified Ghcitui.Brick.Events as Events
import qualified Ghcitui.Brick.HelpText as HelpText
import qualified Ghcitui.Brick.SourceWindow as SourceWindow
import qualified Ghcitui.Ghcid.Daemon as Daemon
import qualified Ghcitui.Loc as Loc
import qualified Ghcitui.NameBinding as NameBinding
import qualified Ghcitui.Util as Util
-- | Alias for 'AppState AppName' convenience.
type AppS = AppState AppName
appDraw :: AppS -> [B.Widget AppName]
appDraw s =
[ drawDialogLayer s
, drawBaseLayer s
]
dialogMaxWidth :: (Integral a) => a
dialogMaxWidth = 94
{- | Draw the dialog layer.
If there's no dialog, returns an 'emptyWidget'.
-}
drawDialogLayer :: AppS -> B.Widget AppName
-- Quit Dialog
drawDialogLayer AppState{activeWindow = ActiveDialogQuit} =
B.withAttr (B.attrName "dialog") $ B.renderDialog dialogObj body
where
dialogObj = B.dialog (Just titleW) Nothing dialogMaxWidth
titleW = B.txt "Please don't go. The drones need you. They look up to you."
body =
B.hCenter
(B.padAll 1 (B.txt "Do you want to halt the current program and quit?"))
<=> B.hCenter (B.padAll 1 (B.txt "[Enter] -> QUIT" <=> B.txt "[Esc/q] -> Go back"))
-- Help Dialog
drawDialogLayer AppState{activeWindow = ActiveDialogHelp} =
B.withAttr (B.attrName "dialog") $ B.renderDialog dialogObj body
where
dialogObj = B.dialog (Just titleW) Nothing dialogMaxWidth
titleW = B.txt "Actually reading the manual, huh?"
body =
( B.hCenter
. B.withVScrollBars B.OnRight
. B.viewport HelpViewport B.Vertical
$ B.padAll 1 (B.txt HelpText.helpText)
)
<=> ( B.hCenter
. B.padAll 1
$ B.txt "[Esc/Enter/q] -> Go back"
)
-- No Dialog
drawDialogLayer _ = B.emptyWidget
drawBaseLayer :: AppS -> B.Widget AppName
drawBaseLayer s =
(sourceWindowBox <=> interpreterBox <=> debugBox) <+> infoBox s
where
sourceLabel =
markLabel
(s.activeWindow == ActiveCodeViewport)
( "Source: " <> maybe "?" T.pack (AppState.selectedFile s)
)
"[Esc]"
interpreterLabel =
markLabel
(s.activeWindow == ActiveLiveInterpreter)
( if s ^. appInterpState . AIS.viewLock
then "GHCi"
else "GHCi (Scrolling)"
)
"[Ctrl+x]"
-- For seeing the source code.
sourceWindowBox :: B.Widget AppName
sourceWindowBox =
B.borderWithLabel sourceLabel
. appendLastCommand
. B.padRight B.Max
. B.padBottom B.Max
$ DrawSourceViewer.drawSourceViewer s
where
appendLastCommand w =
B.padBottom B.Max (w <=> B.hBorder <=> (lastCmdWidget <+> lineNumRatioWidget))
where
selectedLine = AppState.selectedLine s
totalLines = s ^. AppState.sourceWindow & SourceWindow.srcWindowLength
percentageNum =
if totalLines > 0
then (selectedLine * 100) `div` totalLines
else 0
lineNumRatioWidget =
B.txt
( Util.showT selectedLine
<> "/"
<> Util.showT totalLines
<> "L ("
<> Util.showT percentageNum
<> "%)"
)
lastCmdWidget =
B.padRight
B.Max
( case headMay (Daemon.execHist (AppState.interpState s)) of
Just h -> B.txt h
_ -> B.txt " "
)
-- For the REPL.
interpreterBox :: B.Widget AppName
interpreterBox =
B.borderWithLabel interpreterLabel
. B.vLimit (AppState.getReplHeight s)
. B.withVScrollBars B.OnRight
. B.viewport LiveInterpreterViewport B.Vertical
$ previousOutput <=> lockToBottomOnViewLock promptLine
where
enableCursor = True
previousOutput =
if null s.interpLogs
then B.emptyWidget
else
B.txt
. T.unlines
. reverse
$ s.interpLogs
promptLine :: B.Widget AppName
promptLine =
B.txt (AppConfig.getInterpreterPrompt . AppState.appConfig $ s)
<+> BE.renderEditor displayF enableCursor (s ^. liveEditor)
where
displayF :: [T.Text] -> B.Widget AppName
displayF t = B.vBox $ B.txt <$> t
lockToBottomOnViewLock w =
if s ^. appInterpState . AIS.viewLock
then B.visible w
else w
debugBox =
if s.displayDebugConsoleLogs
then
let logDisplay =
if null s.debugConsoleLogs then [" "] else s.debugConsoleLogs
applyVisTo (x : xs) = B.visible x : xs
applyVisTo [] = []
in B.borderWithLabel (B.txt "Debug")
. B.vLimit 10
. B.withVScrollBars B.OnRight
. B.viewport DebugPanel B.Vertical
. B.padRight B.Max
. B.vBox
. reverse
. applyVisTo
$ (B.txt <$> logDisplay)
else B.emptyWidget
-- | Draw the info panel.
infoBox :: AppS -> B.Widget AppName
infoBox appState =
B.borderWithLabel infoLabel
. B.hLimit (AppState.getInfoWidth appState)
. B.padRight B.Max
. B.padBottom B.Max
$ bindingBox
<=> B.hBorderWithLabel modulesLabel
<=> moduleBox appState
<=> B.hBorderWithLabel (B.txt "Trace History")
<=> drawTraceBox appState
where
isActive = activeWindow appState == ActiveInfoWindow
infoLabel = B.txt "Info"
modulesLabel =
markLabel
isActive
"Modules"
(if activeWindow appState /= ActiveLiveInterpreter then "[M]" else mempty)
intState = interpState appState
bindingBox :: B.Widget AppName
bindingBox = B.viewport BindingViewport B.Vertical contents
where
contents = case NameBinding.renderNamesTxt <$> Daemon.bindings intState of
Left _ -> B.txt "<Error displaying bindings>"
Right [] -> B.txt " " -- Can't be an empty widget due to padding?
Right bs -> B.vBox (B.txtWrapWith wrapSettings <$> bs)
wrapSettings =
Wrap.defaultWrapSettings
{ Wrap.preserveIndentation = True
, Wrap.breakLongWords = True
, Wrap.fillStrategy = Wrap.FillIndent 2
}
moduleBox :: AppS -> B.Widget AppName
moduleBox appState =
B.cached ModulesViewport $
if null mfmAssocs
then B.hCenter $ B.txt "<No module mappings>"
else
B.withVScrollBars B.OnRight
. B.viewport ModulesViewport B.Vertical
$ B.vBox moduleEntries
where
mfmAssocs = Loc.moduleFileMapAssocs (Daemon.moduleFileMap (AppState.interpState appState))
moduleEntries = zipWith mkModEntryWidget [0 ..] mfmAssocs
mkModEntryWidget :: Int -> (T.Text, FilePath) -> B.Widget n
mkModEntryWidget idx (modName, fp) =
if isSelected && isActive
then
B.visible
( B.withAttr
(B.attrName "selected-marker")
(B.txt cursor <+> B.txtWrapWith wrapSettings entryText)
)
else B.txt padding <+> B.txt entryText
where
isSelected = AppState.getSelectedModuleInInfoPanel appState == idx
isActive = AppState.activeWindow appState == ActiveInfoWindow
entryText = modName <> " = " <> T.pack fp
padding = " "
cursor = "> "
wrapSettings =
Wrap.defaultWrapSettings
{ Wrap.preserveIndentation = True
, Wrap.breakLongWords = True
, Wrap.fillStrategy = Wrap.FillIndent 2
}
-- | Draw the trace box in the info panel.
drawTraceBox :: AppState AppName -> B.Widget AppName
drawTraceBox s = contents
where
contents =
if null traceHist
then B.txt "<No trace>"
else B.vBox $ B.txt <$> traceHist
traceHist :: [T.Text]
traceHist = Daemon.traceHist (AppState.interpState s)
-- | Mark the label if the first arg is True.
markLabel
:: Bool
-- ^ Conditional to mark with.
-> T.Text
-- ^ Text to use for the label.
-> T.Text
-- ^ Addendum unfocused text.
-> B.Widget a
markLabel False labelTxt focus = B.txt . appendFocusButton $ labelTxt
where
appendFocusButton t = if focus == mempty then t else t <> " " <> focus
markLabel True labelTxt _ =
B.withAttr (B.attrName "highlight") (B.txt ("#> " <> labelTxt <> " <#"))
-- -------------------------------------------------------------------------------------------------
-- Brick Main
-- -------------------------------------------------------------------------------------------------
-- | Brick main program.
brickApp :: B.App AppS e AppName
brickApp =
B.App
{ B.appDraw = appDraw
, B.appChooseCursor = Events.handleCursorPosition
, B.appHandleEvent = Events.handleEvent
, B.appStartEvent = pure ()
, B.appAttrMap =
const $
B.attrMap
V.defAttr
[ (B.attrName "stop-line", B.fg V.red)
, (B.attrName "line-numbers", B.fg V.cyan)
, (B.attrName "selected-line-numbers", B.fg V.yellow)
, (B.attrName "selected-line", B.bg V.brightBlack)
, (B.attrName "selected-marker", B.fg V.yellow)
, (B.attrName "breakpoint-marker", B.fg V.red)
, (B.attrName "underline", B.style V.underline)
, (B.attrName "styled", B.fg V.magenta `V.withStyle` V.bold)
, (B.attrName "highlight", B.style V.standout)
, (B.attrName "dialog", B.style V.standout)
]
}
-- | Start the Brick UI
launchBrick :: AppConfig.AppConfig -> T.Text -> FilePath -> IO ()
launchBrick conf target cwd = do
T.putStrLn $ "Starting up GHCiTUI with: `" <> AppConfig.getCmd conf <> "`..."
T.putStrLn "This can take a while..."
initialState <- makeInitialState conf target cwd
_ <- B.defaultMain brickApp initialState
T.putStrLn "GHCiTUI has shut down; have a nice day :)"
pure ()