fix:ui: re-check balance assertions properly when --pivot is used [#2451]

When hledger-ui is started with --pivot, re-enabling balance
assertions with the I key now does a full journal reload, to check
balance assertions accurately. It means that in pivot mode, the I key
can also show other data changes (as if you pressed the g key).
This commit is contained in:
Simon Michael 2025-10-09 08:36:12 -10:00
parent 1c86e02d99
commit 4aa7d7e20d
5 changed files with 40 additions and 18 deletions

View File

@ -48,7 +48,7 @@ import Hledger.UI.UIState
import Hledger.UI.UIUtils import Hledger.UI.UIUtils
import Hledger.UI.UIScreens import Hledger.UI.UIScreens
import Hledger.UI.Editor import Hledger.UI.Editor
import Hledger.UI.ErrorScreen (uiCheckBalanceAssertions, uiReload, uiReloadIfFileChanged) import Hledger.UI.ErrorScreen (uiReload, uiReloadIfFileChanged, uiToggleBalanceAssertions)
import Hledger.UI.RegisterScreen (rsCenterSelection) import Hledger.UI.RegisterScreen (rsCenterSelection)
import Data.Either (fromRight) import Data.Either (fromRight)
import Control.Arrow ((>>>)) import Control.Arrow ((>>>))
@ -257,7 +257,7 @@ asHandleNormalMode (ALS scons ass) ev = do
VtyEvent (EvKey (KLeft) [MShift]) -> modify' (previousReportPeriod journalspan >>> regenerateScreens j d) VtyEvent (EvKey (KLeft) [MShift]) -> modify' (previousReportPeriod journalspan >>> regenerateScreens j d)
-- various toggles and settings: -- various toggles and settings:
VtyEvent (EvKey (KChar 'I') []) -> modify' (toggleIgnoreBalanceAssertions >>> uiCheckBalanceAssertions d) VtyEvent (EvKey (KChar 'I') []) -> get' >>= uiToggleBalanceAssertions d
VtyEvent (EvKey (KChar 'F') []) -> modify' (toggleForecast d >>> regenerateScreens j d) VtyEvent (EvKey (KChar 'F') []) -> modify' (toggleForecast d >>> regenerateScreens j d)
VtyEvent (EvKey (KChar 'B') []) -> modify' (toggleConversionOp >>> regenerateScreens j d) VtyEvent (EvKey (KChar 'B') []) -> modify' (toggleConversionOp >>> regenerateScreens j d)
VtyEvent (EvKey (KChar 'V') []) -> modify' (toggleValue >>> regenerateScreens j d) VtyEvent (EvKey (KChar 'V') []) -> modify' (toggleValue >>> regenerateScreens j d)

View File

@ -13,6 +13,7 @@ module Hledger.UI.ErrorScreen
,uiCheckBalanceAssertions ,uiCheckBalanceAssertions
,uiReload ,uiReload
,uiReloadIfFileChanged ,uiReloadIfFileChanged
,uiToggleBalanceAssertions
) )
where where
@ -102,7 +103,7 @@ esHandle ev = do
runEditor pos f runEditor pos f
esReloadIfFileChanged copts d j ui esReloadIfFileChanged copts d j ui
VtyEvent (EvKey (KChar 'I') []) -> put' $ uiCheckBalanceAssertions d (popScreen $ toggleIgnoreBalanceAssertions ui) VtyEvent (EvKey (KChar 'I') []) -> uiToggleBalanceAssertions d (popScreen ui)
VtyEvent (EvKey (KChar 'l') [MCtrl]) -> redraw VtyEvent (EvKey (KChar 'l') [MCtrl]) -> redraw
VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui
_ -> return () _ -> return ()
@ -150,6 +151,10 @@ hledgerparseerrorpositionp = do
-- Defined here so it can reference the error screen: -- Defined here so it can reference the error screen:
-- | Modify some input options for hledger-ui (enable --forecast).
uiAdjustOpts :: UIOpts -> CliOpts -> CliOpts
uiAdjustOpts uopts = enableForecast uopts
-- | Reload the journal from its input files, then update the ui app state accordingly. -- | Reload the journal from its input files, then update the ui app state accordingly.
-- This means regenerate the entire screen stack from top level down to the current screen, using the provided today-date. -- This means regenerate the entire screen stack from top level down to the current screen, using the provided today-date.
-- As a convenience (usually), if journal reloading fails, this enters the error screen, or if already there, updates its message. -- As a convenience (usually), if journal reloading fails, this enters the error screen, or if already there, updates its message.
@ -163,8 +168,8 @@ hledgerparseerrorpositionp = do
uiReload :: CliOpts -> Day -> UIState -> EventM Name UIState UIState uiReload :: CliOpts -> Day -> UIState -> EventM Name UIState UIState
uiReload copts d ui = liftIO $ do uiReload copts d ui = liftIO $ do
ej <- ej <-
let copts' = enableForecast (astartupopts ui) copts let copts1 = uiAdjustOpts (astartupopts ui) copts
in runExceptT $ journalTransform copts' <$> journalReload copts' in runExceptT $ journalTransform copts1 <$> journalReload copts1
-- dbg1IO "uiReload before reload" (map tdescription $ jtxns $ ajournal ui) -- dbg1IO "uiReload before reload" (map tdescription $ jtxns $ ajournal ui)
return $ case ej of return $ case ej of
Right j -> Right j ->
@ -188,20 +193,24 @@ uiReload copts d ui = liftIO $ do
-- Also, this one runs in IO, suitable for suspendAndResume. -- Also, this one runs in IO, suitable for suspendAndResume.
uiReloadIfFileChanged :: CliOpts -> Day -> Journal -> UIState -> IO UIState uiReloadIfFileChanged :: CliOpts -> Day -> Journal -> UIState -> IO UIState
uiReloadIfFileChanged copts d j ui = do uiReloadIfFileChanged copts d j ui = do
let copts' = enableForecast (astartupopts ui) copts ej <-
ej <- runExceptT $ journalReloadIfChanged copts' d j let copts1 = uiAdjustOpts (astartupopts ui) copts
in runExceptT $ journalReloadIfChanged copts1 d j
return $ case ej of return $ case ej of
Right (j', _) -> regenerateScreens j' d ui Right (j', _) -> regenerateScreens j' d ui
Left err -> case aScreen ui of Left err -> case aScreen ui of
ES _ -> ui{aScreen=esNew err} ES _ -> ui{aScreen=esNew err}
_ -> pushScreen (esNew err) ui _ -> pushScreen (esNew err) ui
-- Re-check any balance assertions in the current journal, and if any -- Re-check any balance assertions in the current journal,
-- fail, enter (or update) the error screen. Or if balance assertions -- and if any fail, enter (or update) the error screen.
-- are disabled, do nothing. -- Or if balance assertions are disabled or pivot is active, do nothing.
-- (When pivot is active, assertions have already been checked on the pre-pivot journal,
-- and the current post-pivot journal's account names don't match the original assertions.)
uiCheckBalanceAssertions :: Day -> UIState -> UIState uiCheckBalanceAssertions :: Day -> UIState -> UIState
uiCheckBalanceAssertions _d ui@UIState{ajournal=j} uiCheckBalanceAssertions _d ui@UIState{ajournal=j, aopts=UIOpts{uoCliOpts=CliOpts{inputopts_=InputOpts{pivot_=pval}}}}
| ui^.ignore_assertions = ui | ui^.ignore_assertions = ui -- user disabled checks
| not (null pval) = ui -- post-pivot journal, assertions already checked pre-pivot
| otherwise = | otherwise =
case journalCheckBalanceAssertions j of case journalCheckBalanceAssertions j of
Right () -> ui Right () -> ui
@ -209,3 +218,16 @@ uiCheckBalanceAssertions _d ui@UIState{ajournal=j}
case ui of case ui of
UIState{aScreen=ES sst} -> ui{aScreen=ES sst{_essError=err}} UIState{aScreen=ES sst} -> ui{aScreen=ES sst{_essError=err}}
_ -> pushScreen (esNew err) ui _ -> pushScreen (esNew err) ui
-- | Toggle ignoring balance assertions (when user presses I), and if no longer ignoring, recheck them.
-- Normally the recheck is done quickly on the in-memory journal.
-- But if --pivot is active, a full journal reload is done instead
-- (because we can't check balance assertions after pivoting has occurred).
-- In that case, this operation could be slower and could reveal other data changes (not just balance assertion failures).
uiToggleBalanceAssertions :: Day -> UIState -> EventM Name UIState ()
uiToggleBalanceAssertions d ui@UIState{aopts=UIOpts{uoCliOpts=copts@CliOpts{inputopts_=InputOpts{pivot_=pivotval}}}} =
let ui' = toggleIgnoreBalanceAssertions ui
in case (ui'^.ignore_assertions, null pivotval) of
(True, _) -> put' ui' -- ignoring enabled, no check needed
(False, True) -> put' $ uiCheckBalanceAssertions d ui' -- unpivoted journal, can check in memory
(False, False) -> uiReload copts d ui' >>= put' -- pivoted journal, must reload to check it

View File

@ -33,7 +33,7 @@ import Hledger.UI.UITypes
import Hledger.UI.UIState import Hledger.UI.UIState
import Hledger.UI.UIUtils import Hledger.UI.UIUtils
import Hledger.UI.UIScreens import Hledger.UI.UIScreens
import Hledger.UI.ErrorScreen (uiCheckBalanceAssertions, uiReload, uiReloadIfFileChanged) import Hledger.UI.ErrorScreen (uiReload, uiReloadIfFileChanged, uiToggleBalanceAssertions)
import Hledger.UI.Editor (runIadd, runEditor, endPosition) import Hledger.UI.Editor (runIadd, runEditor, endPosition)
import Brick.Widgets.Edit (getEditContents, handleEditorEvent) import Brick.Widgets.Edit (getEditContents, handleEditorEvent)
import Control.Arrow ((>>>)) import Control.Arrow ((>>>))
@ -156,7 +156,7 @@ msHandle ev = do
where where
p = reportPeriod ui p = reportPeriod ui
e | e `elem` [VtyEvent (EvKey (KChar 'g') []), AppEvent FileChange] -> uiReload copts d ui >>= put' e | e `elem` [VtyEvent (EvKey (KChar 'g') []), AppEvent FileChange] -> uiReload copts d ui >>= put'
VtyEvent (EvKey (KChar 'I') []) -> put' $ uiCheckBalanceAssertions d (toggleIgnoreBalanceAssertions ui) VtyEvent (EvKey (KChar 'I') []) -> uiToggleBalanceAssertions d ui
VtyEvent (EvKey (KChar 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add (cliOptsDropArgs copts) j >> uiReloadIfFileChanged copts d j ui VtyEvent (EvKey (KChar 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add (cliOptsDropArgs copts) j >> uiReloadIfFileChanged copts d j ui
VtyEvent (EvKey (KChar 'A') []) -> suspendAndResume $ void (runIadd (journalFilePath j)) >> uiReloadIfFileChanged copts d j ui VtyEvent (EvKey (KChar 'A') []) -> suspendAndResume $ void (runIadd (journalFilePath j)) >> uiReloadIfFileChanged copts d j ui
VtyEvent (EvKey (KChar 'E') []) -> suspendAndResume $ void (runEditor endPosition (journalFilePath j)) >> uiReloadIfFileChanged copts d j ui VtyEvent (EvKey (KChar 'E') []) -> suspendAndResume $ void (runEditor endPosition (journalFilePath j)) >> uiReloadIfFileChanged copts d j ui

View File

@ -43,7 +43,7 @@ import Hledger.UI.UIState
import Hledger.UI.UIUtils import Hledger.UI.UIUtils
import Hledger.UI.UIScreens import Hledger.UI.UIScreens
import Hledger.UI.Editor import Hledger.UI.Editor
import Hledger.UI.ErrorScreen (uiCheckBalanceAssertions, uiReload, uiReloadIfFileChanged) import Hledger.UI.ErrorScreen (uiReload, uiReloadIfFileChanged, uiToggleBalanceAssertions)
rsDraw :: UIState -> [Widget Name] rsDraw :: UIState -> [Widget Name]
rsDraw UIState{aopts=_uopts@UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec}} rsDraw UIState{aopts=_uopts@UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec}}
@ -247,7 +247,7 @@ rsHandle ev = do
e | e `elem` [AppEvent FileChange, VtyEvent (EvKey (KChar 'g') [])] -> uiReload copts d ui >>= put' e | e `elem` [AppEvent FileChange, VtyEvent (EvKey (KChar 'g') [])] -> uiReload copts d ui >>= put'
VtyEvent (EvKey (KChar 'I') []) -> put' $ uiCheckBalanceAssertions d (toggleIgnoreBalanceAssertions ui) VtyEvent (EvKey (KChar 'I') []) -> uiToggleBalanceAssertions d ui
VtyEvent (EvKey (KChar 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add (cliOptsDropArgs copts) j >> uiReloadIfFileChanged copts d j ui VtyEvent (EvKey (KChar 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add (cliOptsDropArgs copts) j >> uiReloadIfFileChanged copts d j ui
VtyEvent (EvKey (KChar 'A') []) -> suspendAndResume $ void (runIadd (journalFilePath j)) >> uiReloadIfFileChanged copts d j ui VtyEvent (EvKey (KChar 'A') []) -> suspendAndResume $ void (runIadd (journalFilePath j)) >> uiReloadIfFileChanged copts d j ui
VtyEvent (EvKey (KChar 'T') []) -> put' $ regenerateScreens j d $ setReportPeriod (DayPeriod d) ui VtyEvent (EvKey (KChar 'T') []) -> put' $ regenerateScreens j d $ setReportPeriod (DayPeriod d) ui

View File

@ -30,7 +30,7 @@ import Hledger.UI.UIState
import Hledger.UI.UIUtils import Hledger.UI.UIUtils
import Hledger.UI.UIScreens import Hledger.UI.UIScreens
import Hledger.UI.Editor import Hledger.UI.Editor
import Hledger.UI.ErrorScreen (uiCheckBalanceAssertions, uiReload, uiReloadIfFileChanged) import Hledger.UI.ErrorScreen (uiCheckBalanceAssertions, uiReload, uiReloadIfFileChanged, uiToggleBalanceAssertions)
import Hledger.UI.RegisterScreen (rsHandle) import Hledger.UI.RegisterScreen (rsHandle)
tsDraw :: UIState -> [Widget Name] tsDraw :: UIState -> [Widget Name]
@ -162,7 +162,7 @@ tsHandle ev = do
where where
p = reportPeriod ui p = reportPeriod ui
VtyEvent (EvKey (KChar 'I') []) -> put' $ uiCheckBalanceAssertions d (toggleIgnoreBalanceAssertions ui) VtyEvent (EvKey (KChar 'I') []) -> uiToggleBalanceAssertions d ui
-- for toggles that may change the current/prev/next transactions, -- for toggles that may change the current/prev/next transactions,
-- we must regenerate the transaction list, like the g handler above ? with regenerateTransactions ? TODO WIP -- we must regenerate the transaction list, like the g handler above ? with regenerateTransactions ? TODO WIP