ghc-debug-brick-0.8.0.0: src/GHC/Debug/Brick/Update.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE GADTs #-}
module GHC.Debug.Brick.Update where
import Brick
import Brick.BChan
import Brick.Forms
import Brick.Widgets.List
import Control.Applicative
import Control.Concurrent.Async (cancel)
import Control.Monad.Catch (bracket)
import Control.Monad.IO.Class
import qualified Data.ByteString.Lazy as BS
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import Data.Ord (comparing)
import qualified Data.Ord as Ord
import qualified Data.Sequence as Seq
import Data.Text (Text)
import qualified Data.Text as T
import qualified Graphics.Vty as Vty
import Graphics.Vty.Input.Events (Key (..))
import Lens.Micro.Platform
import System.Directory
import System.FilePath
import Text.Read (readMaybe)
import GHC.Debug.Profile (writeCensusByClosureType)
import qualified GHC.Debug.Types.Closures as Debug
import GHC.Debug.Types.Ptr (arrWordsBS, readInfoTablePtr)
import Brick.Widgets.IOTree
import Brick.Widgets.ListPicker (ListPicker (..))
import qualified Brick.Widgets.ListPicker as ListPicker
import GHC.Debug.Brick.Action.ArrWords
import GHC.Debug.Brick.Action.Profile
import GHC.Debug.Brick.Action.Retainers
import GHC.Debug.Brick.Action.Strings
import GHC.Debug.Brick.Action.Thunk
import GHC.Debug.Brick.ClosureDetails
import GHC.Debug.Brick.IOTree
import GHC.Debug.Brick.Lib as GD
import GHC.Debug.Brick.Model as Model
import GHC.Debug.Brick.Render.Closure
import qualified GHC.Debug.Brick.Render.Command as Command
import GHC.Debug.Brick.Render.Footer
import qualified GHC.Debug.Brick.Render.HistoryPicker as History
import GHC.Debug.Brick.Render.Utils
import GHC.Debug.Brick.UI.Async
import GHC.Debug.Brick.UI.Filter
-- ----------------------------------------------------------------------------
-- Window Management
-- ----------------------------------------------------------------------------
handleMainWindowEvent :: Handler () OperationalState
handleMainWindowEvent brickEvent = do
os <- get
case brickEvent of
VtyEvent (Vty.EvKey (KChar 'p') [Vty.MCtrl]) ->
navigationDisabledOr $ do
put $ os & keybindingsMode .~ CommandPicker (commandListPicker (os ^. Model.version))
VtyEvent (Vty.EvKey (KChar 'r') [Vty.MCtrl]) -> do
navigationDisabledOr $ do
put $ os & keybindingsMode .~ HistoryPicker (historyListPicker (os ^. navigator))
-- A generic event
VtyEvent event
| Just cmd <- findCommand event ->
if isCmdDisabled (os ^. Model.version) cmd
then return () -- Command is disabled, don't dispatch the command
else dispatchCommand cmd
-- Navigate the tree of closures
VtyEvent event -> case os ^. treeMode of
GcRoots r t-> do
newTree <- handleIOTreeEvent event t
put (os & treeMode .~ GcRoots r newTree)
Retainer r t -> do
newTree <- handleIOTreeEvent event t
put (os & treeMode .~ Retainer r newTree)
Profile r t -> do
newTree <- handleIOTreeEvent event t
put (os & treeMode .~ Profile r newTree)
Strings r t -> do
newTree <- handleIOTreeEvent event t
put (os & treeMode .~ Strings r newTree)
ArrWords r t -> do
newTree <- handleIOTreeEvent event t
put (os & treeMode .~ ArrWords r newTree)
Thunk r t -> do
newTree <- handleIOTreeEvent event t
put (os & treeMode .~ Thunk r newTree)
_ -> return ()
-- Event handling when the main window has focus
handleMain :: Debuggee -> Handler Event OperationalState
handleMain dbg e = do
os <- get
case e of
AppEvent event -> case event of
PollTick -> return ()
ProgressMessage t -> do
put $ footerMessage t os
ProgressFinished desc runtime ->
put $ os
& running_task .~ Nothing
& last_run_time .~ Just (desc, runtime)
& footerMode .~ FooterInfo
AsyncFinished action -> action
AsyncAborted exc -> do
-- As the running task was aborted for some reason
-- remove it from the running_task field
put $ os & running_task .~ Nothing
put $ footerMessage (T.pack $ show exc) os
NewSampleEvent sampleEv ->
handleSampleEvent sampleEv
NavigatorEvent navEv ->
navigationDisabledOr $ do
handleHistoryNavigatorEvent navEv
DoSearchWithHistoryPush searchCmd -> do
put (os & navigator %~ navPush searchCmd)
-- We perform a full search if the cached results
-- are partial.
handleSearchEvent dbg True searchCmd
DoSearch searchCmd ->
-- This is a navigation event, show partial results if there are any.
handleSearchEvent dbg False searchCmd
CacheSearch cmd result ->
cacheCompleteResult cmd result
CachePartialSearch cmd result ->
cachePartialResult cmd result
_ ->
case view keybindingsMode os of
KeybindingsShown ->
case e of
VtyEvent (Vty.EvKey _ _) -> put $ os & keybindingsMode .~ NoOverlay
_ -> put os
HistoryPicker listPicker ->
handleHistoryListPicker e listPicker
CommandPicker listPicker ->
handleCommandListPicker e listPicker
NoOverlay -> case view footerMode os of
FooterInput fm form -> inputFooterHandler dbg fm form handleMainWindowEvent (() <$ e)
_ -> handleMainWindowEvent (() <$ e)
handleHistoryListPicker :: BrickEvent Name Event -> ListPicker Name OperationalState Event HistoryCommand -> EventM Name OperationalState ()
handleHistoryListPicker e picker = do
ListPicker.handleListPicker e picker >>= \ case
ListPicker.Unchanged -> pure ()
ListPicker.Changed newPicker -> modify (keybindingsMode .~ HistoryPicker newPicker)
ListPicker.Exit -> modify (keybindingsMode .~ NoOverlay)
handleCommandListPicker :: BrickEvent Name Event -> ListPicker Name OperationalState Event Command -> EventM Name OperationalState ()
handleCommandListPicker e picker = do
ListPicker.handleListPicker e picker >>= \ case
ListPicker.Unchanged -> pure ()
ListPicker.Changed newPicker -> modify (keybindingsMode .~ CommandPicker newPicker)
ListPicker.Exit -> modify (keybindingsMode .~ NoOverlay)
navigationDisabledOr :: EventM Name OperationalState () -> EventM Name OperationalState ()
navigationDisabledOr act = do
os <- get
case os ^. running_task of
Nothing ->
act
Just _ ->
modify (footerMessage "Navigation disabled during a running heap traversal, press ESC to cancel")
handleHistoryNavigatorEvent :: NavigatorEvent -> EventM Name OperationalState ()
handleHistoryNavigatorEvent navEv = do
os <- get
case navEv of
GoOneBackwards -> do
let
nav = os ^. navigator
(mEvent, newNav) = navBackwards nav
put $
os & navigator .~ newNav
case mEvent of
Nothing ->
put (os & footerMessage "No previous view available")
Just ev -> do
sendEvent $ DoSearch ev
GoOneForwards -> do
let
nav = os ^. navigator
(mEvent, newNav) = navForward nav
put $
os & navigator .~ newNav
case mEvent of
Nothing ->
put (os & footerMessage "No next view available")
Just ev -> do
sendEvent $ DoSearch ev
handleSearchEvent :: Debuggee -> Bool -> SomeSearchCmd -> EventM Name OperationalState ()
handleSearchEvent dbg fullSearch (SomeSearchCmd searchCmd) = do
os <- get
case searchCmd of
DoGcRootsSearch ->
cachedOrRun
fullSearch
DoGcRootsSearch
(\ gcRoots ->
put (os & treeMode .~ gcRootsTreeMode (gcRootsIOTree dbg gcRoots))
)
(do
-- TODO: This logic belongs in GHC.Debug.Brick.GcRoots
-- TODO: introduce event to set the 'rootsFrom'
(closures, roots) <- liftIO $ gcRootsAction dbg
modify (\ o -> o & rootsFrom .~ DefaultRoots roots)
-- TODO: This is awkward, but this makes sure the view is updated.
-- Rather, we should have two types of events, one that changes the
-- view and one that sends values.
sendEvent $ NewSampleEvent $ GcRootsSample closures
sendEvent $ CacheSearch DoGcRootsSearch closures
)
DoRetainerSearch limit uifilters ->
cachedOrRun
fullSearch
(DoRetainerSearch limit uifilters)
(\ retainers ->
put (os & treeMode .~ retainerTreeMode (os ^. resultSize) (retainerTree dbg retainers) (topRetainersFromRetainerMap retainers))
)
(searchWithCurrentFiltersIncremental dbg uifilters)
DoProfileSearch lvl ->
cachedOrRun
fullSearch
(DoProfileSearch lvl)
(\ census ->
put (os & treeMode .~ profileTreeMode (os ^. resultSize) (profileTree dbg) census)
)
(profileAction dbg lvl)
DoStringSearch ->
cachedOrRun
fullSearch
DoStringSearch
(\ census ->
put (os & treeMode .~ stringTreeMode (os ^. resultSize) (stringTree dbg) census)
)
(stringsAction dbg)
DoArrWordsSearch ->
cachedOrRun
fullSearch
DoArrWordsSearch
(\ census ->
put (os & treeMode .~ arrWordsTreeMode (os ^. resultSize) (arrWordsTree dbg) census)
)
(arrWordsAction dbg)
DoThunkSearch ->
cachedOrRun
fullSearch
DoThunkSearch
(\ census ->
put (os & treeMode .~ thunkTreeMode (os ^. resultSize) (thunkTree dbg) census)
)
(thunkAnalysisActionAsync dbg)
handleSampleEvent :: SampleEvent -> EventM Name OperationalState ()
handleSampleEvent ev = do
os <- get
case ev of
GcRootsSample closures -> do
case os ^. treeMode of
GcRoots render t -> do
put $ os & treeMode .~ GcRoots render (setIOTreeRoots closures t)
-- TODO: support partial results
_ ->
-- If a sample is received but not displaying a gcRoots view in the tree for some reason, just ignore it.
return ()
RetainerSample closure_retainer_stack_sample -> do
case os ^. treeMode of
Retainer render t -> do
case closure_retainer_stack_sample of
[] -> pure () -- Theoretically, this should never occur.
(new_sample_clos_detail:_) -> do
-- Why do we only need the first element?
-- Because the first element of the 'new_closure_stack_sample'
-- is the closure of interest (or Leaf closure), while the tail
-- is the retainer stack of closures.
let root = RetainerStackRoot new_sample_clos_detail
put $ os & treeMode .~ Retainer render (addIOTreeRoots [root] t)
-- TODO: support partial results
_ ->
-- If a sample is received but not displaying a retainer view in the tree for some reason, just ignore it.
return ()
ProfileSample lvl census -> do
case os ^. treeMode of
Profile _ t -> do
put $ os & treeMode .~ profileTreeMode (os ^. resultSize) t census
sendEvent $ CachePartialSearch (DoProfileSearch lvl) census
_ ->
-- If a sample is received but not displaying a profile view in the tree for some reason, just ignore it.
return ()
StringSample census -> do
case os ^. treeMode of
Strings _render t -> do
put $ os & treeMode .~ stringTreeMode (os ^. resultSize) t census
sendEvent $ CachePartialSearch DoStringSearch census
_ ->
-- If a sample is received but not displaying a strings view in the tree for some reason, just ignore it.
return ()
ArrWordsSample census -> do
case os ^. treeMode of
ArrWords _render t -> do
put $ os & treeMode .~ arrWordsTreeMode (os ^. resultSize) t census
sendEvent $ CachePartialSearch DoArrWordsSearch census
_ ->
-- If a sample is received but not displaying a arr words view in the tree for some reason, just ignore it.
return ()
ThunkSample census -> do
case os ^. treeMode of
Thunk _render t -> do
put $ os & treeMode .~ thunkTreeMode (os ^. resultSize) t census
sendEvent $ CachePartialSearch DoThunkSearch census
_ ->
-- If a sample is received but not displaying a thunk view in the tree for some reason, just ignore it.
return ()
inputFooterHandler :: Debuggee
-> FooterInputMode
-> Form Text () Name
-> Handler () OperationalState
-> Handler () OperationalState
inputFooterHandler dbg m form _k re@(VtyEvent e) =
case e of
Vty.EvKey KEsc [] -> do
modify resetFooter
Vty.EvKey KEnter [] -> do
dispatchFooterInput dbg m form
_
| isInvertFilterEvent e ->
let m' = invertInput m in
modify (footerMode .~ (FooterInput m' (updateFormState (formState form) $ footerInputForm m')))
| otherwise -> do
zoom (lens (const form) (\ os form' -> set footerMode (FooterInput m form') os)) (handleFormEvent re)
inputFooterHandler _ _ _ k re = k re
-- | What happens when we press enter in footer input mode
dispatchFooterInput :: Debuggee
-> FooterInputMode
-> Form Text () n
-> EventM n OperationalState ()
dispatchFooterInput _ (FClosureAddress runf invert) form = addOrSendUiFilter form runf readClosurePtr (pure . UIAddressFilter invert)
dispatchFooterInput _ (FInfoTableAddress runf invert) form = addOrSendUiFilter form runf readInfoTablePtr (pure . UIInfoAddressFilter invert)
dispatchFooterInput _ (FConstructorName runf invert) form = addOrSendUiFilter form runf Just (pure . UIConstructorFilter invert)
dispatchFooterInput _ (FClosureName runf invert) form = addOrSendUiFilter form runf Just (pure . UIInfoNameFilter invert)
dispatchFooterInput _ FArrWordsSize form = addOrSendUiFilter form True readMaybe (\size -> [UIClosureTypeFilter False Debug.ARR_WORDS, UISizeFilter False size])
dispatchFooterInput _ (FFilterEras runf invert) form = addOrSendUiFilter form runf (parseEraRange . T.pack) (pure . UIEraFilter invert)
dispatchFooterInput _ (FFilterClosureSize invert) form = addOrSendUiFilter form False readMaybe (pure . UISizeFilter invert)
dispatchFooterInput _ (FFilterClosureType invert) form = addOrSendUiFilter form False readMaybe (pure . UIClosureTypeFilter invert)
dispatchFooterInput _ (FFilterCcId runf invert) form = addOrSendUiFilter form runf readMaybe (pure . UICcId invert)
dispatchFooterInput _ FDumpArrWords form = do
os <- get
let
act node = asyncAction_ "dumping ARR_WORDS payload" $
case node of
Just ClosureDetails{_closure = Closure{_closureSized = Debug.unDCS -> Debug.ArrWordsClosure{bytes, arrWords}}} ->
BS.writeFile (T.unpack $ formState form) $ arrWordsBS (take (fromIntegral bytes) arrWords)
_ -> pure ()
case view treeMode os of
Retainer _ iotree -> act (fmap retainerNodeClosure $ ioTreeSelection iotree)
Profile _ _ -> put (os & footerMessage "Dump for profile mode not implemented yet")
Strings _ iotree -> act (stringLineClosureDetails =<< ioTreeSelection iotree)
Thunk _ _ -> put (os & footerMessage "Dump for thunk mode not implemented yet")
ArrWords _ iotree -> act (arrWordLineClosureDetails =<< ioTreeSelection iotree)
GcRoots _ iotree -> act (ioTreeSelection iotree)
dispatchFooterInput _ FSetResultSize form = do
asyncAction "setting result size" (pure ()) $ \() -> do
os <- get
case readMaybe $ T.unpack (formState form) of
Just n
| n <= 0 -> put (os & resultSize .~ Nothing)
| otherwise -> put (os & resultSize .~ (Just n))
Nothing -> pure ()
dispatchFooterInput dbg FSnapshot form = do
asyncAction_ "Taking snapshot" $ snapshot dbg (T.unpack (formState form))
dispatchFooterInput _ FSaveTree form = do
os <- get
case os ^. treeMode of
Profile _ t -> do
let
profLineToCensusByClosure = \ case
ProfileLine key args stats -> Just ((key, args), stats)
ClosureLine _ -> Nothing
census = Map.fromList $ Maybe.mapMaybe profLineToCensusByClosure (getIOTreeRoots t)
liftIO $ writeCensusByClosureType (T.unpack $ formState form) census
_ ->
put (os & footerMessage "Saving to file only supported for Profile views")
addOrSendUiFilter :: Form Text e n -> Bool -> (String -> Maybe t) -> (t -> [UIFilter]) -> EventM n OperationalState ()
addOrSendUiFilter form doRun parse newFilter = do
os <- get
addFilterOrSendEvent form doRun parse newFilter (DoRetainerSearch (os ^. resultSize))
addFilterOrSendEvent :: Form Text e n -> Bool -> (String -> Maybe t) -> (t -> [UIFilter]) -> ([UIFilter] -> SearchCmd RetainerMap) -> EventM n OperationalState ()
addFilterOrSendEvent form doRun parse createFilterM mkEvent = do
case parse (T.unpack (formState form)) of
Just x
| doRun -> do
let newFilter = createFilterM x
let searchCmd = SomeSearchCmd $ mkEvent newFilter
sendEvent (DoSearchWithHistoryPush searchCmd)
| otherwise -> do
let newFilter = createFilterM x
modify (resetFooter . addFilters newFilter)
Nothing ->
pure ()
myAppHandleEvent :: BrickEvent Name Event -> EventM Name AppState ()
myAppHandleEvent brickEvent = do
appState@(AppState majorState' eventChan) <- get
case brickEvent of
_ -> case majorState' of
Setup st knownDebuggees' knownSnapshots' -> case brickEvent of
VtyEvent (Vty.EvKey KEsc _) -> halt
VtyEvent event -> case event of
-- Connect to the selected debuggee
Vty.EvKey (KChar '\t') [] -> do
put $ appState & majorState . setupKind %~ toggleSetup
Vty.EvKey KEnter _ ->
case st of
Snapshot
| Just (_debuggeeIx, socket) <- listSelectedElement knownSnapshots'
-> do
debuggee' <- liftIO $ snapshotConnect (writeBChan eventChan . ProgressMessage) (view socketLocation socket)
put $ appState & majorState .~ Connected
{ _debuggeeSocket = socket
, _debuggee = debuggee'
, _mode = RunningMode -- TODO should we query the debuggee for this?
}
Socket
| Just (_debuggeeIx, socket) <- listSelectedElement knownDebuggees'
-> do
bracket
(liftIO $ debuggeeConnect (writeBChan eventChan . ProgressMessage) (view socketLocation socket))
(\debuggee' -> liftIO $ resume debuggee')
(\debuggee' ->
put $ appState & majorState .~ Connected
{ _debuggeeSocket = socket
, _debuggee = debuggee'
, _mode = RunningMode -- TODO should we query the debuggee for this?
})
_ -> return ()
-- Navigate through the list.
_ -> do
case st of
Snapshot -> do
zoom (majorState . knownSnapshots) (handleListEventVi handleListEvent event)
Socket -> do
zoom (majorState . knownDebuggees) (handleListEventVi handleListEvent event)
AppEvent event -> case event of
PollTick -> do
-- Poll for debuggees
knownDebuggees'' <- updateListFrom socketDirectory knownDebuggees'
knownSnapshots'' <- updateListFrom snapshotDirectory knownSnapshots'
put $ appState & majorState . knownDebuggees .~ knownDebuggees''
& majorState . knownSnapshots .~ knownSnapshots''
_ -> return ()
_ -> return ()
Connected _socket' debuggee' mode' -> case mode' of
RunningMode -> case brickEvent of
-- Exit
VtyEvent (Vty.EvKey KEsc _) ->
halt
-- Pause the debuggee
VtyEvent (Vty.EvKey (KChar 'p') []) -> do
liftIO $ pause debuggee'
ver <- liftIO $ GD.version debuggee'
put (appState & majorState . mode .~
PausedMode
OperationalState
{ _running_task = Nothing
, _last_run_time = Nothing
, _treeMode = gcRootsTreeMode (gcRootsIOTree debuggee' [])
, _keybindingsMode = NoOverlay
, _footerMode = FooterInfo
, _rootsFrom = DefaultRoots []
, _event_chan = eventChan
, _resultSize = Just 100
, _filters = []
, _asyncSamplingInterval = 1_000_000 {- 1 Second -}
, _version = ver
, _viewCache = emptyCache
, _navigator = newHistoryNavigator $ SomeSearchCmd DoGcRootsSearch
}
)
liftIO $ writeBChan eventChan $ DoSearch $ SomeSearchCmd DoGcRootsSearch
_ -> return ()
PausedMode os -> case brickEvent of
_ -> case brickEvent of
VtyEvent (Vty.EvKey (KEsc) _) | NoOverlay <- view keybindingsMode os
, not (isFocusedFooter (view footerMode os)) -> do
case view running_task os of
Just task -> do
liftIO $ cancel task
put $ appState & majorState . mode . pausedMode . running_task .~ Nothing
& majorState . mode . pausedMode %~ resetFooter
Nothing -> do
liftIO $ resume debuggee'
put $ initialAppState (_appChan appState)
-- handle any other more local events; mostly key events
_ -> liftHandler (majorState . mode) os PausedMode (handleMain debuggee') brickEvent
gcRootsTreeMode :: IOTree ClosureDetails Name -> TreeMode
gcRootsTreeMode = GcRoots renderClosureDetails
gcRootsIOTree :: Debuggee -> [ClosureDetails] -> IOTree ClosureDetails Name
gcRootsIOTree dbg closures =
mkIOTree dbg closures getChildren renderInlineClosureDesc id
gcRootsAction :: Debuggee -> IO ([ClosureDetails], [(Text, Ptr)])
gcRootsAction dbg = do
raw_roots <- take 1000 . map ("GC Roots",) <$> GD.rootClosures dbg
rootClosures' <- mapM (completeClosureDetails dbg) raw_roots
raw_saved <- map ("Saved Object",) <$> GD.savedClosures dbg
savedClosures' <- mapM (completeClosureDetails dbg) raw_saved
pure (savedClosures' <> rootClosures', fmap toPtr <$> (raw_roots ++ raw_saved))
updateListFrom :: MonadIO m =>
IO FilePath
-> GenericList n Seq.Seq SocketInfo
-> m (GenericList n Seq.Seq SocketInfo)
updateListFrom dirIO llist = liftIO $ do
dir :: FilePath <- dirIO
debuggeeSocketFiles :: [FilePath] <- listDirectory dir <|> return []
-- Sort the sockets by the time they have been created, newest
-- first.
debuggeeSockets <- List.sortBy (comparing Ord.Down)
<$> mapM (mkSocketInfo . (dir </>)) debuggeeSocketFiles
let currentSelectedPathMay :: Maybe SocketInfo
currentSelectedPathMay = fmap snd (listSelectedElement llist)
newSelection :: Maybe Int
newSelection = do
currentSelectedPath <- currentSelectedPathMay
List.findIndex ((currentSelectedPath ==)) debuggeeSockets
return $ listReplace
(Seq.fromList debuggeeSockets)
(newSelection <|> (if Prelude.null debuggeeSockets then Nothing else Just 0))
llist
-- ----------------------------------------------------------------------------
-- Command & History ListPicker
-- ----------------------------------------------------------------------------
commandListPicker :: GD.Version -> ListPicker Name OperationalState Event Command
commandListPicker ver =
ListPicker
{ _listPickerInput = newForm [(\w -> forceAttr inputAttr w) @@= editTextField id Overlay (Just 1)] ""
, _listPickerElements = list CommandPicker_List commandList 1
, _listPickerAllElements = commandList
, _listPickerSelect = \ case
cmd
| isCmdDisabled ver cmd ->
pure ListPicker.Unchanged
| otherwise -> do
dispatchCommand cmd
pure ListPicker.Exit
, _listPickerDescription = commandDescription
, _listPickerRenderRow = Command.renderCommand ver
, _listPickerLabel = "Command Picker"
, _listPickerHighlightAttrName = highlightAttr
}
historyListPicker :: HistoryNavigator SomeSearchCmd -> ListPicker Name OperationalState Event HistoryCommand
historyListPicker historyNav =
ListPicker
{ _listPickerInput = newForm [(\w -> forceAttr inputAttr w) @@= editTextField id Overlay (Just 1)] ""
, _listPickerElements = list HistoryPicker_List historyCommands 1
, _listPickerAllElements = historyCommands
, _listPickerSelect = \ cmd -> do
historyCommandDispatch cmd
pure ListPicker.Exit
, _listPickerDescription = History.commandDescription
, _listPickerRenderRow = History.commandWidget
, _listPickerLabel = "History Picker"
, _listPickerHighlightAttrName = highlightAttr
}
where
historyCommands = Seq.fromList $ map HistoryCommand navStack
navStack = navList historyNav
-- ----------------------------------------------------------------------------
-- Commands and Shortcut constants
-- ----------------------------------------------------------------------------
invertFilterEvent :: Vty.Event
invertFilterEvent = Vty.EvKey (KChar 'g') [Vty.MCtrl]
isInvertFilterEvent :: Vty.Event -> Bool
isInvertFilterEvent = (invertFilterEvent ==)
-- All the commands which we support, these show up in keybindings and also the command picker
commandList :: Seq.Seq Command
commandList = Seq.fromList
[ mkCommand "Show key bindings" (Vty.EvKey (KChar '?') []) (modify $ keybindingsMode .~ KeybindingsShown)
, mkCommand "Clear filters" (withCtrlKey 'w') (modify clearFilters)
, Command "Search with current filters" (Just $ withCtrlKey 'f') doRetainerSearch NoReq
, mkCommand "Set search limit (default 100)" (withCtrlKey 'l') (setFooterInputMode FSetResultSize)
, mkCommand "GC Roots" (withCtrlKey 's') (doSearch DoGcRootsSearch)
, mkCommand "Find Address" (withCtrlKey 'a') (setFooterInputMode (FClosureAddress True False))
, mkCommand "Find Info Table" (withCtrlKey 't') (setFooterInputMode (FInfoTableAddress True False))
, mkCommand "Find Retainers" (withCtrlKey 'e') (setFooterInputMode (FConstructorName True False))
, mkCommand' "Find Retainers (Exact)" (setFooterInputMode (FClosureName True False))
, mkFilterCmd "Find closures by era" (withCtrlKey 'v') (setFooterInputMode (FFilterEras True False)) ReqErasProfiling
, mkCommand "Find Retainers of large ARR_WORDS" (withCtrlKey 'u') (setFooterInputMode FArrWordsSize)
, mkCommand "Dump ARR_WORDS payload" (withCtrlKey 'j') (setFooterInputMode FDumpArrWords)
, mkCommand "Profile" (withCtrlKey 'b') (doSearch (DoProfileSearch OneLevel))
, mkCommand' "Profile (2 level)" (doSearch (DoProfileSearch TwoLevel))
, Command "Thunk Analysis" Nothing (doSearch DoThunkSearch) NoReq
, mkCommand "Take Snapshot" (withCtrlKey 'x') (setFooterInputMode FSnapshot)
, Command "ARR_WORDS Count" Nothing (doSearch DoArrWordsSearch) NoReq
, Command "Strings Count" Nothing (doSearch DoStringSearch) NoReq
, mkCommand' "Save View to file" (setFooterInputMode FSaveTree)
, mkCommand "Go to previous view" (withAltKey KLeft) (sendEvent $ NavigatorEvent GoOneBackwards)
, mkCommand "Go to next view" (withAltKey KRight) (sendEvent $ NavigatorEvent GoOneForwards)
, mkCommand "Show History" (withCtrlKey 'r') (sendEvent $ NavigatorEvent GoOneForwards)
] <> addFilterCommands
where
doSearch = sendEvent . DoSearchWithHistoryPush . SomeSearchCmd
doRetainerSearch = do
os <- get
doSearch $ DoRetainerSearch (os ^. resultSize) (os ^. filters)
setFooterInputMode m = modify $ footerMode .~ footerInput m
addFilterCommands :: Seq.Seq Command
addFilterCommands = Seq.fromList
[ mkCommand' "Add filter for address" (setFooterInputMode (FClosureAddress False False))
, mkCommand' "Add filter for info table ptr" (setFooterInputMode (FInfoTableAddress False False))
, mkCommand' "Add filter for constructor name" (setFooterInputMode (FConstructorName False False))
, mkCommand' "Add filter for closure name" (setFooterInputMode (FClosureName False False))
, mkFilterCmd' "Add filter for era" (setFooterInputMode (FFilterEras False False)) ReqErasProfiling
, mkFilterCmd' "Add filter for cost centre id" (setFooterInputMode (FFilterCcId False False)) ReqSomeProfiling
, mkCommand' "Add filter for closure size" (setFooterInputMode (FFilterClosureSize False))
, mkCommand' "Add filter for closure type" (setFooterInputMode (FFilterClosureType False))
, mkCommand' "Add exclusion for address" (setFooterInputMode (FClosureAddress False True))
, mkCommand' "Add exclusion for info table ptr" (setFooterInputMode (FInfoTableAddress False True))
, mkCommand' "Add exclusion for constructor name" (setFooterInputMode (FConstructorName False True))
, mkCommand' "Add exclusion for closure name" (setFooterInputMode (FClosureName False True))
, mkFilterCmd' "Add exclusion for era" (setFooterInputMode (FFilterEras False True)) ReqErasProfiling
, mkFilterCmd' "Add exclusion for cost centre id" (setFooterInputMode (FFilterCcId False True)) ReqSomeProfiling
, mkCommand' "Add exclusion for closure size" (setFooterInputMode (FFilterClosureSize True))
, mkCommand' "Add exclusion for closure type" (setFooterInputMode (FFilterClosureType True))
]
withCtrlKey char = Vty.EvKey (KChar char) [Vty.MCtrl]
withAltKey key = Vty.EvKey key [Vty.MMeta]
findCommand :: Vty.Event -> Maybe Command
findCommand event = do
i <- Seq.findIndexL (\cmd -> commandKey cmd == Just event) commandList
Seq.lookup i commandList