mirror of
https://github.com/simonmichael/hledger.git
synced 2025-01-06 02:23:46 +03:00
203 lines
8.0 KiB
Haskell
203 lines
8.0 KiB
Haskell
-- The error screen, showing a current error condition (such as a parse error after reloading the journal)
|
||
|
||
{-# LANGUAGE FlexibleContexts #-}
|
||
{-# LANGUAGE OverloadedStrings #-}
|
||
{-# LANGUAGE RecordWildCards #-}
|
||
{-# OPTIONS_GHC -Wno-incomplete-record-updates #-}
|
||
|
||
module Hledger.UI.ErrorScreen
|
||
(errorScreen
|
||
,uiCheckBalanceAssertions
|
||
,uiReloadJournal
|
||
,uiReloadJournalIfChanged
|
||
)
|
||
where
|
||
|
||
import Brick
|
||
-- import Brick.Widgets.Border ("border")
|
||
import Control.Monad
|
||
import Control.Monad.Except (liftIO)
|
||
import Data.Time.Calendar (Day)
|
||
import Data.Void (Void)
|
||
import Graphics.Vty (Event(..),Key(..),Modifier(..))
|
||
import Lens.Micro ((^.))
|
||
import Text.Megaparsec
|
||
import Text.Megaparsec.Char
|
||
|
||
import Hledger.Cli hiding (progname,prognameandversion)
|
||
import Hledger.UI.UIOptions
|
||
import Hledger.UI.UITypes
|
||
import Hledger.UI.UIState
|
||
import Hledger.UI.UIUtils
|
||
import Hledger.UI.Editor
|
||
|
||
errorScreen :: Screen
|
||
errorScreen = ErrorScreen{
|
||
sInit = esInit
|
||
,sDraw = esDraw
|
||
,sHandle = esHandle
|
||
,esError = ""
|
||
}
|
||
|
||
esInit :: Day -> Bool -> UIState -> UIState
|
||
esInit _ _ ui@UIState{aScreen=ErrorScreen{}} = ui
|
||
esInit _ _ _ = error "init function called with wrong screen type, should not happen" -- PARTIAL:
|
||
|
||
esDraw :: UIState -> [Widget Name]
|
||
esDraw UIState{aopts=UIOpts{uoCliOpts=copts}
|
||
,aScreen=ErrorScreen{..}
|
||
,aMode=mode
|
||
} =
|
||
case mode of
|
||
Help -> [helpDialog copts, maincontent]
|
||
-- Minibuffer e -> [minibuffer e, maincontent]
|
||
_ -> [maincontent]
|
||
where
|
||
maincontent = Widget Greedy Greedy $ do
|
||
render $ defaultLayout toplabel bottomlabel $ withAttr (attrName "error") $ str $ esError
|
||
where
|
||
toplabel =
|
||
withAttr (attrName "border" <> attrName "bold") (str "Oops. Please fix this problem then press g to reload")
|
||
-- <+> (if ignore_assertions_ copts then withAttr ("border" <> "query") (str " ignoring") else str " not ignoring")
|
||
|
||
bottomlabel = quickhelp
|
||
-- case mode of
|
||
-- Minibuffer ed -> minibuffer ed
|
||
-- _ -> quickhelp
|
||
where
|
||
quickhelp = borderKeysStr [
|
||
("h", "help")
|
||
,("ESC", "cancel/top")
|
||
,("E", "editor")
|
||
,("g", "reload")
|
||
,("q", "quit")
|
||
]
|
||
|
||
esDraw _ = error "draw function called with wrong screen type, should not happen" -- PARTIAL:
|
||
|
||
esHandle :: BrickEvent Name AppEvent -> EventM Name UIState ()
|
||
esHandle ev = do
|
||
ui0 <- get'
|
||
case ui0 of
|
||
ui@UIState{aScreen=ErrorScreen{..}
|
||
,aopts=UIOpts{uoCliOpts=copts}
|
||
,ajournal=j
|
||
,aMode=mode
|
||
} ->
|
||
case mode of
|
||
Help ->
|
||
case ev of
|
||
VtyEvent (EvKey (KChar 'q') []) -> halt
|
||
VtyEvent (EvKey (KChar 'l') [MCtrl]) -> redraw
|
||
VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui
|
||
_ -> helpHandle ev
|
||
|
||
_ -> do
|
||
let d = copts^.rsDay
|
||
case ev of
|
||
VtyEvent (EvKey (KChar 'q') []) -> halt
|
||
VtyEvent (EvKey KEsc []) -> put' $ uiCheckBalanceAssertions d $ resetScreens d ui
|
||
VtyEvent (EvKey (KChar c) []) | c `elem` ['h','?'] -> put' $ setMode Help ui
|
||
VtyEvent (EvKey (KChar 'E') []) -> suspendAndResume $ void (runEditor pos f) >> uiReloadJournalIfChanged copts d j (popScreen ui)
|
||
where
|
||
(pos,f) = case parsewithString hledgerparseerrorpositionp esError of
|
||
Right (f,l,c) -> (Just (l, Just c),f)
|
||
Left _ -> (endPosition, journalFilePath j)
|
||
e | e `elem` [VtyEvent (EvKey (KChar 'g') []), AppEvent FileChange] ->
|
||
liftIO (uiReloadJournal copts d (popScreen ui)) >>= put' . uiCheckBalanceAssertions d
|
||
-- (ej, _) <- liftIO $ journalReloadIfChanged copts d j
|
||
-- case ej of
|
||
-- Left err -> continue ui{aScreen=s{esError=err}} -- show latest parse error
|
||
-- Right j' -> continue $ regenerateScreens j' d $ popScreen ui -- return to previous screen, and reload it
|
||
VtyEvent (EvKey (KChar 'I') []) -> put' $ uiCheckBalanceAssertions d (popScreen $ toggleIgnoreBalanceAssertions ui)
|
||
VtyEvent (EvKey (KChar 'l') [MCtrl]) -> redraw
|
||
VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui
|
||
_ -> return ()
|
||
|
||
_ -> errorWrongScreenType "event handler"
|
||
|
||
-- | Parse the file name, line and column number from a hledger parse error message, if possible.
|
||
-- Temporary, we should keep the original parse error location. XXX
|
||
-- Keep in sync with 'Hledger.Data.Transaction.showGenericSourcePos'
|
||
hledgerparseerrorpositionp :: ParsecT Void String t (String, Int, Int)
|
||
hledgerparseerrorpositionp = do
|
||
anySingle `manyTill` char '"'
|
||
f <- anySingle `manyTill` (oneOf ['"','\n'])
|
||
choice [
|
||
do
|
||
string " (line "
|
||
l <- read <$> some digitChar
|
||
string ", column "
|
||
c <- read <$> some digitChar
|
||
return (f, l, c),
|
||
do
|
||
string " (lines "
|
||
l <- read <$> some digitChar
|
||
char '-'
|
||
some digitChar
|
||
char ')'
|
||
return (f, l, 1)
|
||
]
|
||
|
||
|
||
-- | Unconditionally reload the journal, regenerating the current screen
|
||
-- and all previous screens in the history as of the provided today-date.
|
||
-- If reloading fails, enter the error screen, or if we're already
|
||
-- on the error screen, update the error displayed.
|
||
-- Defined here so it can reference the error screen.
|
||
--
|
||
-- The provided CliOpts are used for reloading, and then saved in the
|
||
-- UIState if reloading is successful (otherwise the UIState keeps its old
|
||
-- CliOpts.) (XXX needed for.. ?)
|
||
--
|
||
-- Forecasted transactions are always generated, as at hledger-ui startup.
|
||
-- If a forecast period is specified in the provided opts, or was specified
|
||
-- at startup, it is preserved.
|
||
--
|
||
uiReloadJournal :: CliOpts -> Day -> UIState -> IO UIState
|
||
uiReloadJournal copts d ui = do
|
||
ej <-
|
||
let copts' = enableForecastPreservingPeriod ui copts
|
||
in runExceptT $ journalReload copts'
|
||
return $ case ej of
|
||
Right j -> regenerateScreens j d ui
|
||
Left err ->
|
||
case ui of
|
||
UIState{aScreen=s@ErrorScreen{}} -> ui{aScreen=s{esError=err}}
|
||
_ -> screenEnter d errorScreen{esError=err} ui
|
||
-- XXX GHC 9.2 warning:
|
||
-- hledger-ui/Hledger/UI/ErrorScreen.hs:164:59: warning: [-Wincomplete-record-updates]
|
||
-- Pattern match(es) are non-exhaustive
|
||
-- In a record-update construct:
|
||
-- Patterns of type ‘Screen’ not matched:
|
||
-- AccountsScreen _ _ _ _ _
|
||
-- RegisterScreen _ _ _ _ _ _
|
||
-- TransactionScreen _ _ _ _ _ _
|
||
|
||
-- | Like uiReloadJournal, but does not re-parse the journal if the file(s)
|
||
-- have not changed since last loaded. Always regenerates the screens though,
|
||
-- since the provided options or today-date may have changed.
|
||
uiReloadJournalIfChanged :: CliOpts -> Day -> Journal -> UIState -> IO UIState
|
||
uiReloadJournalIfChanged copts d j ui = do
|
||
let copts' = enableForecastPreservingPeriod ui copts
|
||
ej <- runExceptT $ journalReloadIfChanged copts' d j
|
||
return $ case ej of
|
||
Right (j', _) -> regenerateScreens j' d ui
|
||
Left err -> case ui of
|
||
UIState{aScreen=s@ErrorScreen{}} -> ui{aScreen=s{esError=err}}
|
||
_ -> screenEnter d errorScreen{esError=err} ui
|
||
|
||
-- Re-check any balance assertions in the current journal, and if any
|
||
-- fail, enter (or update) the error screen. Or if balance assertions
|
||
-- are disabled, do nothing.
|
||
uiCheckBalanceAssertions :: Day -> UIState -> UIState
|
||
uiCheckBalanceAssertions d ui@UIState{ajournal=j}
|
||
| ui^.ignore_assertions = ui
|
||
| otherwise =
|
||
case journalCheckBalanceAssertions j of
|
||
Nothing -> ui
|
||
Just err ->
|
||
case ui of
|
||
UIState{aScreen=s@ErrorScreen{}} -> ui{aScreen=s{esError=err}}
|
||
_ -> screenEnter d errorScreen{esError=err} ui
|