Add Help and Exit dialogs

This commit is contained in:
CrystalSplitter 2023-12-30 14:37:28 -08:00 committed by Jordan R AW
parent 53c3be7c9a
commit 1ea4e95ba1
6 changed files with 287 additions and 50 deletions

View File

@ -47,6 +47,8 @@ data ActiveWindow
= ActiveCodeViewport
| ActiveLiveInterpreter
| ActiveInfoWindow
| ActiveDialogQuit
| ActiveDialogHelp
deriving (Show, Eq, Ord)
data MaxState = NoMaxState | Maximised | Minimised

View File

@ -7,6 +7,7 @@ data AppName
| CodeViewportLine Int
| LiveInterpreter
| LiveInterpreterViewport
| HelpViewport
| BindingViewport
| ModulesViewport
| TraceViewport

View File

@ -10,6 +10,7 @@ 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 Data.Bifunctor (second)
@ -33,6 +34,7 @@ import qualified AppState
import AppTopLevel (AppName (..))
import qualified Events
import qualified Ghcid.Daemon as Daemon
import qualified HelpText
import qualified Loc
import qualified NameBinding
import qualified Util
@ -42,16 +44,60 @@ type AppS = AppState AppName
appDraw :: AppS -> [B.Widget AppName]
appDraw s =
[ (viewportBox <=> interpreterBox <=> debugBox)
[ drawDialogLayer s
, drawBaseLayer s
]
dialogMaxWidth :: (Integral a) => a
dialogMaxWidth = 90
{- | 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 =
verticalBoxes
-- TODO: Make this an expandable viewport, maybe?
<+> infoBox s
]
where
verticalBoxes = viewportBox <=> interpreterBox <=> debugBox
sourceLabel =
markLabel
(s.activeWindow == ActiveCodeViewport)
( "Source: " <> maybe "?" T.pack s.selectedFile
)
"[Esc]"
interpreterLabel =
markLabel
(s.activeWindow == ActiveLiveInterpreter)
@ -59,6 +105,7 @@ appDraw s =
then "Interpreter"
else "Interpreter (Scrolling)"
)
"[Ctrl+x]"
-- For seeing the source code.
viewportBox :: B.Widget AppName
@ -209,6 +256,10 @@ markLabel False labelTxt focus = B.txt . appendFocusButton $ labelTxt
markLabel True labelTxt _ =
B.withAttr (B.attrName "highlight") (B.txt ("#> " <> labelTxt <> " <#"))
-- -------------------------------------------------------------------------------------------------
-- Code Viewport Drawing
-- -------------------------------------------------------------------------------------------------
-- | Information used to compute the gutter status of each line.
data GutterInfo = GutterInfo
{ isStoppedHere :: !Bool
@ -272,6 +323,46 @@ codeViewportDraw s =
codeViewportDraw' :: AppS -> T.Text -> B.Widget AppName
codeViewportDraw' s sourceData = composedTogether
where
composedTogether :: B.Widget AppName
composedTogether = B.vBox (createWidget <$> windowedSplitSourceData)
where
wrapSelectedLine lineno w =
if lineno == s.selectedLine
then -- Add highlighting, then mark it as visible in the viewport.
B.visible $ B.modifyDefAttr (`V.withStyle` V.bold) w
else w
createWidget (num, lineTxt) = wrapSelectedLine num (composedTogetherHelper (num, lineTxt))
-- Select which line widget we want to draw based on both the interpreter
-- state and the app state.
--
-- It's important that the line information is cached, because
-- each line is actually pretty expensive to render.
composedTogetherHelper :: (Int, T.Text) -> B.Widget AppName
composedTogetherHelper (lineno, lineTxt) = lineWidgetCached
where
sr = maybe Loc.unknownSourceRange Loc.sourceRange (Daemon.pauseLoc (interpState s))
mLineno = Loc.singleify sr
lineWidget = case mLineno of
-- This only makes the stopped line widget appear for the start loc.
Just (singleLine, _) | lineno == singleLine -> stoppedLineW lineTxt
-- If it's a range, just try to show the range.
_
| Loc.isLineInside sr lineno -> stoppedRangeW
-- If it's not something we stopped in, just show the selection normally.
_
| lineno == s.selectedLine -> selectedLineW lineTxt
-- Default case.
_ -> ((\w -> prefixLineDefault' (lineno, w)) . B.txt) lineTxt
lineWidgetCached = B.cached (CodeViewportLine lineno) lineWidget
stoppedRangeW :: B.Widget AppName
stoppedRangeW =
prefixLineDefault'
( lineno
, B.forceAttrAllowStyle (B.attrName "stop-line") (B.txt lineTxt)
)
-- Source data split on lines.
splitSourceData = T.lines sourceData
-- Source data split on lines, but only the bit we may want to render.
@ -287,9 +378,6 @@ codeViewportDraw' s sourceData = composedTogether
startLineno = 1
withLineNums = zip [startLineno ..]
breakpoints :: [Int]
breakpoints = maybe mempty (\f -> Daemon.getBpInFile f (interpState s)) (selectedFile s)
gutterInfoForLine :: Int -> GutterInfo
gutterInfoForLine lineno =
GutterInfo
@ -303,6 +391,9 @@ codeViewportDraw' s sourceData = composedTogether
, gutterDigitWidth = Util.getNumDigits $ length splitSourceData
, isSelected = lineno == s.selectedLine
}
where
breakpoints :: [Int]
breakpoints = maybe mempty (\f -> Daemon.getBpInFile f (interpState s)) (selectedFile s)
prefixLineDefault' :: (Int, B.Widget n) -> B.Widget n
prefixLineDefault' (lineno, w) =
@ -323,49 +414,12 @@ codeViewportDraw' s sourceData = composedTogether
lineWidget = makeStoppedLineWidget lineTxt (startCol, endCol)
in prefixLineDefault' (originalLookupLineNo, lineWidget)
stoppedRangeW :: Int -> T.Text -> B.Widget AppName
stoppedRangeW lineno lineTxt =
prefixLineDefault' (lineno, B.forceAttrAllowStyle (B.attrName "stop-line") (B.txt lineTxt))
selectedLineW :: T.Text -> B.Widget AppName
selectedLineW lineTxt =
let transform = id -- Potentialy useful for highlighting spaces?
lineWidget = B.txt $ transform lineTxt
in prefixLineDefault' (s.selectedLine, lineWidget)
-- Select which line widget we want to draw based on both the interpreter
-- state and the app state.
--
-- It's important that the line information is cached, because
-- each line is actually pretty expensive to render.
composedTogetherHelper :: (Int, T.Text) -> B.Widget AppName
composedTogetherHelper (lineno, lineTxt) = lineWidgetCached
where
sr = maybe Loc.unknownSourceRange Loc.sourceRange (Daemon.pauseLoc (interpState s))
mLineno = Loc.singleify sr
lineWidget = case mLineno of
-- This only makes the stopped line widget appear for the start loc.
Just (singleLine, _) | lineno == singleLine -> stoppedLineW lineTxt
-- If it's a range, just try to show the range.
_
| Loc.isLineInside sr lineno -> stoppedRangeW lineno lineTxt
-- If it's not something we stopped in, just show the selection normally.
_
| lineno == s.selectedLine -> selectedLineW lineTxt
-- Default case.
_ -> ((\w -> prefixLineDefault' (lineno, w)) . B.txt) lineTxt
lineWidgetCached = B.cached (CodeViewportLine lineno) lineWidget
composedTogether :: B.Widget AppName
composedTogether = B.vBox (createWidget <$> windowedSplitSourceData)
where
wrapSelectedLine lineno w =
if lineno == s.selectedLine
then -- Add highlighting, then mark it as visible in the viewport.
B.visible $ B.modifyDefAttr (`V.withStyle` V.bold) w
else w
createWidget (num, lineTxt) = wrapSelectedLine num (composedTogetherHelper (num, lineTxt))
-- | Make the Stopped Line widget (the line where we paused execution)
makeStoppedLineWidget :: T.Text -> Loc.ColumnRange -> B.Widget AppName
makeStoppedLineWidget lineData (Nothing, _) =
@ -408,6 +462,7 @@ brickApp =
, (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)
]
}

View File

@ -28,9 +28,14 @@ handleEvent :: B.BrickEvent AppName e -> B.EventM AppName (AppState AppName) ()
handleEvent (B.VtyEvent (V.EvResize _ _)) = B.invalidateCache
handleEvent ev = do
appState <- B.get
case appState.activeWindow of
ActiveCodeViewport -> handleViewportEvent ev
ActiveLiveInterpreter -> handleInterpreterEvent ev
let handler = case appState.activeWindow of
ActiveCodeViewport -> handleViewportEvent
ActiveLiveInterpreter -> handleInterpreterEvent
ActiveInfoWindow -> handleInfoEvent
ActiveDialogQuit -> handleDialogQuit
ActiveDialogHelp -> handleDialogHelp
handler ev
-- -------------------------------------------------------------------------------------------------
-- Info Event Handling
----------------------------------------------------------------------------------------------------
@ -212,7 +217,7 @@ replaceCommandBuffer replacement s = Lens.set liveEditor' newEditor s
newEditor = BE.applyEdit zipp (s ^. liveEditor')
-- -------------------------------------------------------------------------------------------------
-- Viewport Event Handling
-- Code Viewport Event Handling
-- -------------------------------------------------------------------------------------------------
-- TODO: Handle mouse events?
@ -238,11 +243,13 @@ handleViewportEvent (B.VtyEvent (V.EvKey key ms))
| key == V.KChar 'b' = do
appState <- B.get
insertViewportBreakpoint appState
-- j and k are the vim navigation keybindings.
| key `elem` [V.KDown, V.KChar 'j'] = do
moveSelectedLineBy 1
| key `elem` [V.KUp, V.KChar 'k'] = do
moveSelectedLineBy (-1)
-- '+' and '-' move the middle border.
| key == V.KChar '+' && null ms = do
appState <- B.get
@ -288,6 +295,7 @@ handleViewportEvent (B.VtyEvent (V.EvKey key ms))
appState <- B.get
B.put appState{activeWindow = ActiveInfoWindow}
B.invalidateCacheEntry ModulesViewport
| key == V.KChar '?' = B.modify (\state -> state{activeWindow = ActiveDialogHelp})
handleViewportEvent _ = pure ()
moveSelectedLineBy :: Int -> B.EventM AppName (AppState n) ()
@ -306,10 +314,7 @@ moveSelectedLineBy movAmnt = do
-- as we may need to store the dialogue structure in the app state.
-- For now, just quit cleanly.
confirmQuit :: B.EventM AppName (AppState AppName) ()
confirmQuit = do
appState <- B.get
_ <- liftIO $ Daemon.quit appState.interpState
B.halt
confirmQuit = B.put . (\s -> s{activeWindow = ActiveDialogQuit}) =<< B.get
invalidateCachedLine :: Int -> B.EventM AppName s ()
invalidateCachedLine lineno = B.invalidateCacheEntry (CodeViewportLine lineno)
@ -378,6 +383,7 @@ runDaemon2 f appState =
newState <- updateSourceMap appState{interpState = interp}
pure (resetSelectedLine newState, x)
-- | Determine whether to show the cursor.
handleCursorPosition
:: AppState AppName
-- ^ State of the app.
@ -412,3 +418,32 @@ selectedModuleLoc s = eModuleLoc =<< fl
<> showT moduleFileMap
<> "'"
in note errMsg res
-- -------------------------------------------------------------------------------------------------
-- Dialog boxes
-- -------------------------------------------------------------------------------------------------
handleDialogQuit :: B.BrickEvent AppName e -> B.EventM AppName (AppState AppName) ()
handleDialogQuit ev = do
appState <- B.get
case ev of
(B.VtyEvent (V.EvKey key _))
| key == V.KChar 'q' || key == V.KEsc -> do
B.put $ appState{activeWindow = ActiveCodeViewport}
| key == V.KEnter -> do
_ <- liftIO $ Daemon.quit appState.interpState
B.halt
_ -> pure ()
pure ()
handleDialogHelp :: B.BrickEvent AppName e -> B.EventM AppName (AppState AppName) ()
handleDialogHelp (B.VtyEvent (V.EvKey key _))
| key == V.KChar 'q' || key == V.KEsc || key == V.KEnter = do
appState <- B.get
B.put $ appState{activeWindow = ActiveCodeViewport}
| key == V.KPageDown = B.vScrollPage scroller B.Down
| key == V.KPageUp = B.vScrollPage scroller B.Up
| otherwise = pure ()
where
scroller = B.viewportScroll HelpViewport
handleDialogHelp _ = pure ()

143
app/HelpText.hs Normal file
View File

@ -0,0 +1,143 @@
{-# LANGUAGE TemplateHaskell #-}
module HelpText where
import Data.String (IsString)
-- import Data.FileEmbed
helpText :: (IsString a) => a
helpText = "\
\GHCITUI MANUAL() GHCITUI MANUAL()\n\
\\n\
\NAME\n\
\ GHCiTUI MANUAL -\n\
\\n\
\CLI SYNOPSIS\n\
\ Usage: ghcitui [OPTIONS] [TARGET]\n\
\\n\
\ ghcitui: A TUI interface for GHCi\n\
\\n\
\ Available options:\n\
\ -h,--help Show this help text\n\
\ -d,--debug-console Display the debug console\n\
\ -c,--cmd CMD Command to start the internal\n\
\ interpreter\n\
\ -C,--workdir DIR Set working dir\n\
\\n\
\STARTING AND STOPPING\n\
\ Starting\n\
\ GHCiTUI runs a REPL in the current directory by default. By default,\n\
\ it launches cabal repl.\n\
\\n\
\ $ cd your/cabal/project/root/directory\n\
\ $ ghcitui\n\
\\n\
\ You can specify another starting directory with the -C <DIR> flag.\n\
\\n\
\ $ ghcitui -C some/other/directory\n\
\\n\
\ Stopping\n\
\ To quit, press <ESC> or q while in the code viewport panel to quit.\n\
\ While not in the code viewport panel, you may press <ESC> to get to\n\
\ the viewport panel.\n\
\\n\
\LAYOUT\n\
\ GHCiTUI is an in-terminal viewer for GHCi. The TUI is broken up into\n\
\ three primary panels, with some additional auxiliary panels for spe\n\
\ cial use cases:\n\
\\n\
\ \n\
\ Info \n\
\ \n\
\ Source Viewer \n\
\ \n\
\ \n\
\ \n\
\ \n\
\ Live Interpreter \n\
\ \n\
\ \n\
\\n\
\ Source Viewer: This panel shows source code. You can step, continue,\n\
\ and toggle breakpoints among other operations in this panel.\n\
\\n\
\ Live Interpreter: This panel shows the GHCi/REPL passthrough. You can\n\
\ enter expressions and GHCi commands here like you would normally,\n\
\ with some additional keybindings.\n\
\\n\
\ Info: This panel displays miscellaneous info about whatever is cur\n\
\ rently running. For example, it can display the current bindings,\n\
\ loaded modules, and the current program trace.\n\
\\n\
\NAVIGATION\n\
\ At any point in time, you can revert back to the Source Viewer panel\n\
\ with the <Esc> key, and you can always quit by hitting <Esc> in the\n\
\ Source Viewer panel.\n\
\\n\
\KEYBINDINGS\n\
\ At this time, keybindings are hardcoded. This will hopefully change\n\
\ in the future with a keybinding configuration file.\n\
\\n\
\ Source Viewer\n\
\ Ctrl+x: Toggle between the Source Viewer and the Live Interpreter\n\
\ panels.\n\
\\n\
\ <Esc>, q: Quit.\n\
\\n\
\ <Up>, k: Move the cursor up. (j and k from Vim keybinds)\n\
\\n\
\ <Down>, j: Move the cursor down. (j and k from Vim keybinds).\n\
\\n\
\ <PgUp>: Move the source viewer one page up.\n\
\\n\
\ <PgDown>: Move the source viewer one page down.\n\
\\n\
\ +, -: Increase/decrease the Info panel size.\n\
\\n\
\ b: Toggle breakpoint at current line. Not every line in a source\n\
\ file can have a breakpoint placed on it.\n\
\\n\
\ s: Advance execution by one step. Same as the :step in GHCi.\n\
\\n\
\ c: Advance execution until next breakpoint. Same as :continue in\n\
\ GHCi.\n\
\\n\
\ t: Advance execution until next breakpoint under tracing. Same as\n\
\ :trace in GHCi.\n\
\\n\
\ Live Interpreter (REPL)\n\
\ Ctrl+x: Toggle between the Source Viewer and the Live Interpreter\n\
\ panels.\n\
\\n\
\ <Esc>: Switch to Source Viewer.\n\
\\n\
\ <Up>: Scroll back in time through the REPL command history.\n\
\\n\
\ <Down>: Scroll forward in time through the REPL command history.\n\
\\n\
\ <PgUp>: Scroll the Live Interpreter window one page up.\n\
\\n\
\ <PgDown>: Scroll the Live Interpreter window one page down.\n\
\\n\
\ Ctrl+n: Toggle scrolling mode.\n\
\\n\
\ +, - while in scrolling mode: Increase/decrease the live panel\n\
\ size.\n\
\\n\
\ <Enter>: Enter a command to the REPL.\n\
\\n\
\ Modules\n\
\ Ctrl+x: Switch to the Live Interpreter.\n\
\\n\
\ <Esc>: Switch to Source Viewer.\n\
\\n\
\ <Up>, k: Move the module selection up.\n\
\\n\
\ <Down>, j: Move the module selection down.\n\
\\n\
\ +, -: Increase/decrease the info panel size.\n\
\\n\
\ <Enter>, o: Open the selected module.\n\
\\n\
\ GHCITUI MANUAL()"

View File

@ -49,6 +49,7 @@ executable ghcitui
, AppInterpState
, AppConfig
, Events
, HelpText
ghc-options: -rtsopts
-threaded
-Wall