packages feed

hledger-ui 1.50 → 1.50.1

raw patch · 13 files changed

+248/−130 lines, 13 filesdep +stmdep ~hledgerdep ~hledger-libPVP ok

version bump matches the API change (PVP)

Dependencies added: stm

Dependency ranges changed: hledger, hledger-lib

API changes (from Hackage documentation)

Files

CHANGES.md view
@@ -23,6 +23,36 @@ See also the hledger changelog.  +# 1.50.1 2025-09-16++Fixes++- The transaction screen and error screen now update on data changes like other screens,+  eg when using the E key, g key, or --watch.+  [#2014], [#2288]++- When the journal is reloaded by the `g` key or `--watch`, the+  --pivot (and --obfuscate) options are now preserved,+  and spurious errors are avoided.+  [#2451]++- The Z key (and the -E command line flag) toggles zero-balance accounts again.+  (Stephen Morgan, [#2454])++Improvements++- Debug output has improved, eg it's easier to see changes to the screen stack.++API++- Hledger.UI.ErrorScreen:+  uiReloadJournal -> uiReload,+  uiReloadJournalIfChanged -> uiReloadIfFileChanged+- Hledger.UI.UIState:+  enableForecastPreservingPeriod -> enableForecast+++ # 1.50 2025-09-03  Breaking changes
Hledger/UI/AccountsScreen.hs view
@@ -48,7 +48,7 @@ import Hledger.UI.UIUtils import Hledger.UI.UIScreens import Hledger.UI.Editor-import Hledger.UI.ErrorScreen (uiReloadJournal, uiCheckBalanceAssertions, uiReloadJournalIfChanged)+import Hledger.UI.ErrorScreen (uiCheckBalanceAssertions, uiReload, uiReloadIfFileChanged) import Hledger.UI.RegisterScreen (rsCenterSelection) import Data.Either (fromRight) import Control.Arrow ((>>>))@@ -69,7 +69,7 @@ asDrawHelper UIState{aScreen=scr, aopts=uopts, ajournal=j, aMode=mode} ropts scrname =   dbgui "asDrawHelper" $   case toAccountsLikeScreen scr of-    Nothing          -> dbgui "asDrawHelper" $ errorWrongScreenType "draw helper"  -- PARTIAL:+    Nothing          -> dbgui "asDrawHelper" $ errorWrongScreenType "asDrawHelper"  -- PARTIAL:     Just (ALS _ ass) -> case mode of       Help -> [helpDialog, maincontent]       _    -> [maincontent]@@ -189,7 +189,7 @@   dbguiEv "asHandle"   ui0@UIState{aScreen=scr, aMode=mode} <- get'   case toAccountsLikeScreen scr of-    Nothing -> dbgui "asHandle" $ errorWrongScreenType "event handler"  -- PARTIAL:+    Nothing -> dbgui "asHandle" $ errorWrongScreenType "asHandle"  -- PARTIAL:     Just als@(ALS scons ass) -> do       -- save the currently selected account, in case we leave this screen and lose the selection       put' ui0{aScreen=scons ass{_assSelectedAccount=asSelectedAccount ass}}@@ -231,7 +231,7 @@     -- XXX be sure we don't leave unconsumed app events piling up     -- A data file has changed (or the user has pressed g): reload.     e | e `elem` [AppEvent FileChange, VtyEvent (EvKey (KChar 'g') [])] ->-      liftIO (uiReloadJournal copts d ui) >>= put'+      uiReload copts d ui >>= put'      -- The date has changed (and we are viewing a standard period which contained the old date):     -- adjust the viewed period and regenerate, just in case needed.@@ -245,9 +245,9 @@     VtyEvent (EvKey k           []) | k `elem` [KBS, KDel] -> modify' (resetFilter >>> regenerateScreens j d)      -- run external programs:-    VtyEvent (EvKey (KChar 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add (cliOptsDropArgs copts) j >> uiReloadJournalIfChanged copts d j ui-    VtyEvent (EvKey (KChar 'A') []) -> suspendAndResume $ void (runIadd (journalFilePath j)) >> uiReloadJournalIfChanged copts d j ui-    VtyEvent (EvKey (KChar 'E') []) -> suspendAndResume $ void (runEditor endPosition (journalFilePath j)) >> uiReloadJournalIfChanged 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 'E') []) -> suspendAndResume $ void (runEditor endPosition (journalFilePath j)) >> uiReloadIfFileChanged copts d j ui      -- adjust the period displayed:     VtyEvent (EvKey (KChar 'T') []) ->       modify' (setReportPeriod (DayPeriod d)    >>> regenerateScreens j d)
Hledger/UI/ErrorScreen.hs view
@@ -11,6 +11,8 @@  ,esDraw  ,esHandle  ,uiCheckBalanceAssertions+ ,uiReload+ ,uiReloadIfFileChanged  ,uiReloadJournal  ,uiReloadJournalIfChanged  )@@ -18,12 +20,12 @@  import Brick -- import Brick.Widgets.Border ("border")-import Control.Monad import Control.Monad.IO.Class (liftIO) import Data.Time.Calendar (Day) import Data.Void (Void) import Graphics.Vty (Event(..),Key(..),Modifier(..)) import Lens.Micro ((^.))+import Safe (headMay) import Text.Megaparsec import Text.Megaparsec.Char @@ -35,6 +37,10 @@ import Hledger.UI.UIScreens import Hledger.UI.Editor +-- legacy aliases+uiReloadJournal = uiReload+uiReloadJournalIfChanged = uiReloadIfFileChanged+ esDraw :: UIState -> [Widget Name] esDraw UIState{aScreen=ES ESS{..}               ,aMode=mode@@ -88,24 +94,42 @@             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 _essError 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++            -- g or file change: reload the journal and rebuild app state.+            e | e `elem` [VtyEvent (EvKey (KChar 'g') []), AppEvent FileChange] -> esReload copts d ui++            -- E: run editor, reload the journal.+            VtyEvent (EvKey (KChar 'E') []) -> do+              suspendAndResume' $ do+                let+                  (pos,f) = case parsewithString hledgerparseerrorpositionp _essError of+                              Right (f',l,c) -> (Just (l, Just c),f')+                              Left  _       -> (endPosition, journalFilePath j)+                runEditor pos f+              esReloadIfFileChanged copts d j ui+             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"+    _ -> errorWrongScreenType "esHandle" +    where+      -- Reload and fully regenerate the error screen.+      -- XXX On an error screen below the transaction screen, this is tricky because of a current limitation of regenerateScreens.+      -- For now we try to work around by re-entering the transaction screen.+      -- This can show flicker in the UI and it's hard to handle all situations robustly.+      esReload copts d ui = uiReload copts d ui >>= maybeReloadErrorScreen copts d+      esReloadIfFileChanged copts d j ui = liftIO (uiReloadIfFileChanged copts d j ui) >>= maybeReloadErrorScreen copts d+      maybeReloadErrorScreen copts d ui =+        case headMay $ aPrevScreens ui of+          Just (TS _) -> do+            -- check balance assertions, exit to register screen, enter transaction screen, reload once more+            put' $ popScreen $ popScreen $ uiCheckBalanceAssertions d ui+            sendVtyEvents [EvKey KEnter [], EvKey (KChar 'g') []]  -- XXX Might be disrupted if other events are queued ?+          _ -> uiReload copts d (popScreen ui) >>= put' . uiCheckBalanceAssertions d+ -- | 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'@@ -130,29 +154,27 @@       ]  --- | 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.+-- Defined here so it can reference the error screen:++-- | 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.+-- As a convenience (usually), if journal reloading fails, this enters the error screen, or if already there, updates its message. ----- 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.. ?)+-- The provided cli options can influence reloading; then if reloading succeeds they are saved in the ui state,+-- otherwise the UIState keeps its old options. (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.+-- Like at hledger-ui startup, --forecast is always enabled.+-- A forecast period specified in the provided opts, or at startup, is preserved. ---uiReloadJournal :: CliOpts -> Day -> UIState -> IO UIState-uiReloadJournal copts d ui = do+uiReload :: CliOpts -> Day -> UIState -> EventM Name UIState UIState+uiReload copts d ui = liftIO $ do   ej <--    let copts' = enableForecastPreservingPeriod ui copts-    in runExceptT $ journalReload copts'-  -- dbg1IO "uiReloadJournal before reload" (map tdescription $ jtxns $ ajournal ui)+    let copts' = enableForecast (astartupopts ui) copts+    in runExceptT $ journalTransform copts' <$> journalReload copts'+  -- dbg1IO "uiReload before reload" (map tdescription $ jtxns $ ajournal ui)   return $ case ej of     Right j  ->-      -- dbg1 "uiReloadJournal after reload" (map tdescription $ jtxns j) $+      -- dbg1 "uiReload after reload" (map tdescription $ jtxns j) $       regenerateScreens j d ui     Left err ->       case ui of@@ -167,12 +189,12 @@       --             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+-- | Like uiReload, except it skips re-reading the journal if its file(s) have not changed+-- since it was last loaded. The up app state is always updated, since the options or today-date may have changed.+-- Also, this one runs in IO, suitable for suspendAndResume.+uiReloadIfFileChanged :: CliOpts -> Day -> Journal -> UIState -> IO UIState+uiReloadIfFileChanged copts d j ui = do+  let copts' = enableForecast (astartupopts ui) copts   ej <- runExceptT $ journalReloadIfChanged copts' d j   return $ case ej of     Right (j', _) -> regenerateScreens j' d ui
Hledger/UI/Main.hs view
@@ -118,7 +118,7 @@     _ | boolopt "man"     rawopts -> runManForTopic  "hledger-ui" Nothing     _ | boolopt "version" rawopts -> putStrLn prognameandversion     -- _ | boolopt "binary-filename" rawopts -> putStrLn (binaryfilename progname)-    _                                         -> withJournalDo copts' (runBrickUi opts)+    _                                         -> withJournal copts' (runBrickUi opts)    when (ghcDebugMode == GDPauseAtEnd) $ ghcDebugPause' 
Hledger/UI/MenuScreen.hs view
@@ -33,7 +33,7 @@ import Hledger.UI.UIState import Hledger.UI.UIUtils import Hledger.UI.UIScreens-import Hledger.UI.ErrorScreen (uiReloadJournal, uiCheckBalanceAssertions, uiReloadJournalIfChanged)+import Hledger.UI.ErrorScreen (uiCheckBalanceAssertions, uiReload, uiReloadIfFileChanged) import Hledger.UI.Editor (runIadd, runEditor, endPosition) import Brick.Widgets.Edit (getEditContents, handleEditorEvent) import Control.Arrow ((>>>))@@ -87,7 +87,7 @@               ,("q", str "quit")               ] -msDraw _ =  dbgui "msDraw" $ errorWrongScreenType "draw function"  -- PARTIAL:+msDraw _ =  dbgui "msDraw" $ errorWrongScreenType "msDraw"  -- PARTIAL:  -- msDrawItem :: (Int,Int) -> Bool -> MenuScreenItem -> Widget Name -- msDrawItem (_acctwidth, _balwidth) _selected MenuScreenItem{..} =@@ -155,12 +155,11 @@               put' $ regenerateScreens j d $ setReportPeriod (DayPeriod d) ui               where                 p = reportPeriod ui-            e | e `elem` [VtyEvent (EvKey (KChar 'g') []), AppEvent FileChange] ->-              liftIO (uiReloadJournal 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 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add (cliOptsDropArgs copts) j >> uiReloadJournalIfChanged copts d j ui-            VtyEvent (EvKey (KChar 'A') []) -> suspendAndResume $ void (runIadd (journalFilePath j)) >> uiReloadJournalIfChanged copts d j ui-            VtyEvent (EvKey (KChar 'E') []) -> suspendAndResume $ void (runEditor endPosition (journalFilePath j)) >> uiReloadJournalIfChanged 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 'E') []) -> suspendAndResume $ void (runEditor endPosition (journalFilePath j)) >> uiReloadIfFileChanged copts d j ui  --             VtyEvent (EvKey (KChar 'B') []) -> put' $ regenerateScreens j d $ toggleConversionOp ui --             VtyEvent (EvKey (KChar 'V') []) -> put' $ regenerateScreens j d $ toggleValue ui@@ -257,7 +256,7 @@             MouseUp{}   -> return ()             AppEvent _  -> return () -    _ -> dbguiEv "msHandle" >> errorWrongScreenType "event handler"+    _ -> dbguiEv "msHandle" >> errorWrongScreenType "msHandle"  msEnterScreen :: Day -> ScreenName -> UIState -> EventM Name UIState () msEnterScreen d scrname ui@UIState{ajournal=j, aopts=uopts} = do
Hledger/UI/RegisterScreen.hs view
@@ -43,13 +43,13 @@ import Hledger.UI.UIUtils import Hledger.UI.UIScreens import Hledger.UI.Editor-import Hledger.UI.ErrorScreen (uiReloadJournal, uiCheckBalanceAssertions, uiReloadJournalIfChanged)+import Hledger.UI.ErrorScreen (uiCheckBalanceAssertions, uiReload, uiReloadIfFileChanged)  rsDraw :: UIState -> [Widget Name] rsDraw UIState{aopts=_uopts@UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec}}               ,aScreen=RS RSS{..}               ,aMode=mode-              } = dbgui "rsDraw 1" $+              } = dbgui "rsDraw" $   case mode of     Help       -> [helpDialog, maincontent]     _          -> [maincontent]@@ -156,7 +156,7 @@               -- ,("q", "quit")               ] -rsDraw _ = dbgui "rsDraw 2" $ errorWrongScreenType "draw function"  -- PARTIAL:+rsDraw _ = dbgui "rsDraw" $ errorWrongScreenType "rsDraw"  -- PARTIAL:  rsDrawItem :: (Int,Int,Int,Int,Int) -> Bool -> RegisterScreenItem -> Widget Name rsDrawItem (datewidth,descwidth,acctswidth,changewidth,balwidth) selected RegisterScreenItem{..} =@@ -187,7 +187,7 @@ rsHandle :: BrickEvent Name AppEvent -> EventM Name UIState () rsHandle ev = do   ui0 <- get'-  dbguiEv "rsHandle 1"+  dbguiEv "rsHandle"   case ui0 of     ui@UIState{       aScreen=RS sst@RSS{..}@@ -245,14 +245,13 @@               where                 p = reportPeriod ui -            e | e `elem` [AppEvent FileChange, VtyEvent (EvKey (KChar 'g') [])] ->-              liftIO (uiReloadJournal 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 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add (cliOptsDropArgs copts) j >> uiReloadJournalIfChanged copts d j ui-            VtyEvent (EvKey (KChar 'A') []) -> suspendAndResume $ void (runIadd (journalFilePath j)) >> uiReloadJournalIfChanged 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 'T') []) -> put' $ regenerateScreens j d $ setReportPeriod (DayPeriod d) ui-            VtyEvent (EvKey (KChar 'E') []) -> suspendAndResume $ void (runEditor pos f) >> uiReloadJournalIfChanged copts d j ui+            VtyEvent (EvKey (KChar 'E') []) -> suspendAndResume $ void (runEditor pos f) >> uiReloadIfFileChanged copts d j ui               where                 (pos,f) = case listSelectedElement _rssList of                             Nothing -> (endPosition, journalFilePath j)@@ -332,7 +331,7 @@             MouseUp{}         -> return ()             AppEvent _        -> return () -    _ -> dbgui "rsHandle 2" $ errorWrongScreenType "event handler"+    _ -> errorWrongScreenType "rsHandle"  isBlankElement mel = ((rsItemDate . snd) <$> mel) == Just "" @@ -354,8 +353,4 @@ rsEnterTransactionScreen :: AccountName -> [NumberedTransaction] -> NumberedTransaction -> UIState -> EventM Name UIState () rsEnterTransactionScreen acct nts nt ui = do   dbguiEv "rsEnterTransactionScreen"-  put' $-    pushScreen (tsNew acct nts nt)-    ui--+  put' $ pushScreen (tsNew acct nts nt) ui
Hledger/UI/TransactionScreen.hs view
@@ -12,14 +12,15 @@ ,tsHandle ) where -import Control.Monad+import Brick+import Brick.Widgets.Edit (editorText, renderEditor)+import Brick.Widgets.List (listMoveTo) import Control.Monad.IO.Class (liftIO) import Data.List import Data.Maybe import qualified Data.Text as T import Graphics.Vty (Event(..),Key(..),Modifier(..), Button (BLeft))-import Brick-import Brick.Widgets.List (listMoveTo)+import System.Exit (ExitCode (..))  import Hledger import Hledger.Cli hiding (mode, prices, progname,prognameandversion)@@ -29,8 +30,8 @@ import Hledger.UI.UIUtils import Hledger.UI.UIScreens import Hledger.UI.Editor-import Brick.Widgets.Edit (editorText, renderEditor)-import Hledger.UI.ErrorScreen (uiReloadJournalIfChanged, uiCheckBalanceAssertions, uiReloadJournal)+import Hledger.UI.ErrorScreen (uiCheckBalanceAssertions, uiReload, uiReloadIfFileChanged)+import Hledger.UI.RegisterScreen (rsHandle)  tsDraw :: UIState -> [Widget Name] tsDraw UIState{aopts=UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}}@@ -95,7 +96,7 @@               -- ,("q", "quit")               ] -tsDraw _ = errorWrongScreenType "draw function"  -- PARTIAL:+tsDraw _ = errorWrongScreenType "tsDraw"  -- PARTIAL:  -- Render a transaction suitably for the transaction screen. showTxn :: ReportOpts -> ReportSpec -> Journal -> Transaction -> T.Text@@ -137,23 +138,30 @@             VtyEvent (EvKey (KChar 'q') []) -> halt             VtyEvent (EvKey KEsc        []) -> put' $ resetScreens d ui             VtyEvent (EvKey (KChar c)   []) | c == '?' -> put' $ setMode Help ui-            VtyEvent (EvKey (KChar 'E') []) -> suspendAndResume $ void (runEditor pos f) >> uiReloadJournalIfChanged copts d j ui-              where-                (pos,f) = case tsourcepos t of-                            (SourcePos f' l1 c1,_) -> (Just (unPos l1, Just $ unPos c1),f')++            -- g or file change: reload the journal and rebuild app state.+            e | e `elem` [VtyEvent (EvKey (KChar 'g') []), AppEvent FileChange] ->+              tsReload copts d ui++              -- for debugging; leaving these here because they were hard to find+              -- \u -> dbguiEv (pshow u) >> put' u  -- doesn't log+              -- \UIState{aScreen=TS tss} -> error' $ pshow $ _tssTransaction tss++            -- E: run editor, reload the journal.+            VtyEvent (EvKey (KChar 'E') []) -> do+              suspendAndResume' $ do+                let (pos,f) = case tsourcepos t of (SourcePos f' l1 c1,_) -> (Just (unPos l1, Just $ unPos c1),f')+                exitcode <- runEditor pos f+                case exitcode of+                  ExitSuccess   -> return ()+                  ExitFailure c -> error' $ "running the text editor failed with exit code " ++ show c+              tsReloadIfFileChanged copts d j ui+             AppEvent (DateChange old _) | isStandardPeriod p && p `periodContainsDate` old ->               put' $ regenerateScreens j d $ setReportPeriod (DayPeriod d) ui               where                 p = reportPeriod ui -            -- Reload. Warning, this updates parent screens but not the transaction screen itself (see tsUpdate).-            -- To see the updated transaction, one must exit and re-enter the transaction screen.-            e | e `elem` [VtyEvent (EvKey (KChar 'g') []), AppEvent FileChange] ->-              liftIO (uiReloadJournal copts d ui) >>= put'-                -- debugging.. leaving these here because they were hard to find-                -- \u -> dbguiEv (pshow u) >> put' u  -- doesn't log-                -- \UIState{aScreen=TS tss} -> error' $ pshow $ _tssTransaction tss-             VtyEvent (EvKey (KChar 'I') []) -> put' $ uiCheckBalanceAssertions d (toggleIgnoreBalanceAssertions ui)              -- for toggles that may change the current/prev/next transactions,@@ -176,7 +184,54 @@             VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui             _ -> return () -    _ -> errorWrongScreenType "event handler"+    _ -> errorWrongScreenType "tsHandle"++    where+      -- Reload and fully regenerate the transaction screen.+      -- XXX On transaction screen or below, this is tricky because of a current limitation of regenerateScreens.+      -- For now we try to work around by re-entering the screen(s).+      -- This can show flicker in the UI and it's hard to handle all situations robustly.+      tsReload copts d ui = uiReload copts d ui >>= reEnterTransactionScreen copts d+      tsReloadIfFileChanged copts d j ui = liftIO (uiReloadIfFileChanged copts d j ui) >>= reEnterTransactionScreen copts d+      +      reEnterTransactionScreen _copts d ui = do+        -- 1. If uiReload (or checking balance assertions) moved us to the error screen, save that, and return to the transaction screen.+        let+          (merrscr, uiTxn) = case aScreen $ uiCheckBalanceAssertions d ui of+            s@(ES _) -> (Just s,  popScreen ui)+            _        -> (Nothing, ui)+        -- 2. Exit to register screen+        let uiReg = popScreen uiTxn+        put' uiReg+        -- 3. Re-enter the transaction screen+        rsHandle (VtyEvent (EvKey KEnter [])) -- PARTIAL assumes we are on the register screen.+        -- 4. Return to the error screen (below the transaction screen) if there was one.+        -- Next events will be handled by esHandle. Error repair will return to the transaction screen.+        maybe (return ()) (put' . flip pushScreen uiTxn) merrscr+          -- doesn't uiTxn have old state from before step 3 ? seems to work++        -- XXX some problem:+        -- 4. Reload once more, possibly re-entering the error screen, by sending a g event.+        -- sendVtyEvents [EvKey (KChar 'g') []]  --  XXX Might be disrupted if other events are queued++        -- XXX doesn't update on non-error change:+        -- 4. Reload once more, possibly re-entering the error screen.+        -- uiTxnOrErr <- uiReload copts d uiTxn+          -- uiReloadIfChanged ?+          -- uiCheckBalanceAssertions ? seems unneeded+        -- put' uiTxnOrErr++        -- XXX not working right:+        -- -- 1. If uiReload (or checking balance assertions) moved us to the error screen, exit to the transaction screen.+        -- let+        --   uiTxn = case aScreen $ uiCheckBalanceAssertions d ui of+        --     ES _ -> popScreen ui+        --     _    -> ui+        -- -- 2. Exit to register screen+        -- put' $ popScreen uiTxn+        -- -- 3. Re-enter the transaction screen, and reload once more.+        -- sendVtyEvents [EvKey KEnter [], EvKey (KChar 'g') []]  -- XXX Might be disrupted if other events are queued+  -- | Select a new transaction and update the previous register screen tsSelect :: Integer -> Transaction -> UIState -> UIState
Hledger/UI/UIState.hs view
@@ -35,7 +35,7 @@ ,resetDepth ,popScreen ,pushScreen-,enableForecastPreservingPeriod+,enableForecast ,resetFilter ,resetScreens ,regenerateScreens@@ -60,6 +60,7 @@ import Hledger.UI.UITypes import Hledger.UI.UIOptions (UIOpts(uoCliOpts)) import Hledger.UI.UIScreens (screenUpdate)+import Hledger.UI.UIUtils (showScreenId, showScreenStack)  -- | Make an initial UI state with the given options, journal, -- parent screen stack if any, and starting screen.@@ -201,20 +202,19 @@   where     newForecast = case ui^.forecast of       Just _  -> Nothing-      Nothing -> enableForecastPreservingPeriod ui (ui^.cliOpts) ^. forecast+      Nothing -> enableForecast (astartupopts ui) (ui^.cliOpts) ^. forecast --- | Ensure this CliOpts enables forecasted transactions.--- If a forecast period was specified in the old CliOpts,--- or in the provided UIState's startup options,--- it is preserved.-enableForecastPreservingPeriod :: UIState -> CliOpts -> CliOpts-enableForecastPreservingPeriod ui copts = set forecast mforecast copts+-- | Enable forecasting in this CliOpts.+-- If it previously specified a forecast period, or else if the given ui startup options did,+-- preserve that as the forecast period.+enableForecast :: UIOpts -> CliOpts -> CliOpts+enableForecast startopts currentopts = set forecast mforecast currentopts   where-    mforecast = asum [mprovidedforecastperiod, mstartupforecastperiod, mdefaultforecastperiod]+    mforecast = asum [mcurrentforecastperiod, mstartupforecastperiod, mdefaultforecastperiod]       where-        mprovidedforecastperiod = copts ^. forecast-        mstartupforecastperiod  = astartupopts ui ^. forecast-        mdefaultforecastperiod  = Just nulldatespan+        mcurrentforecastperiod = currentopts ^. forecast+        mstartupforecastperiod = startopts ^. forecast+        mdefaultforecastperiod = Just nulldatespan  -- | Toggle between showing all and showing only real (non-virtual) items. toggleReal :: UIState -> UIState@@ -340,12 +340,16 @@ setMode m ui = ui{aMode=m}  pushScreen :: Screen -> UIState -> UIState-pushScreen scr ui = ui{aPrevScreens=(aScreen ui:aPrevScreens ui)-                      ,aScreen=scr-                      }+pushScreen scr ui =+  dbg1Msg ("pushing screen " <> showScreenId scr <> ". " <> showScreenStack "" showScreenId ui1)+  ui1+  where ui1 = ui{aPrevScreens=aScreen ui:aPrevScreens ui, aScreen=scr }  popScreen :: UIState -> UIState-popScreen ui@UIState{aPrevScreens=s:ss} = ui{aScreen=s, aPrevScreens=ss}+popScreen ui@UIState{aPrevScreens = s : ss} =+  dbg1Msg ("popping screen " <> showScreenId (aScreen ui) <> ". " <> showScreenStack "" showScreenId ui1)+  ui1+  where ui1 = ui{aPrevScreens = ss ,aScreen = s } popScreen ui = ui  -- | Reset options to their startup values, discard screen navigation history,@@ -361,7 +365,9 @@ -- then regenerate the content of all screens in the stack -- (using the ui state's current options), preserving the screen navigation history. -- Note, does not save the reporting date.+--+-- XXX Currently this does not properly regenerate the transaction screen or error screen,+-- which depend on state from their parent(s); those screens' handlers must do additional work, which is fragile. regenerateScreens :: Journal -> Day -> UIState -> UIState regenerateScreens j d ui@UIState{aopts=opts, aScreen=s,aPrevScreens=ss} =   ui{ajournal=j, aScreen=screenUpdate opts d j s, aPrevScreens=map (screenUpdate opts d j) ss}-
Hledger/UI/UIUtils.hs view
@@ -42,6 +42,7 @@   ,mapScreens   ,uiNumBlankItems   ,showScreenStack+  ,sendVtyEvents   ) where @@ -51,6 +52,7 @@ import Brick.Widgets.Dialog import Brick.Widgets.Edit import Brick.Widgets.List (List, listSelectedL, listNameL, listItemHeightL, listSelected, listMoveDown, listMoveUp, GenericList, listElements)+import Control.Concurrent.STM (atomically, writeTChan)  -- GHC only import Control.Monad.IO.Class import Data.Bifunctor (second) import Data.List@@ -59,6 +61,7 @@ import Graphics.Vty   (Event(..),Key(..),Modifier(..),Vty(..),Color,Attr,currentAttr,refresh, displayBounds   -- ,Output(displayBounds,mkDisplayContext),DisplayContext(..)+  ,Vty (inputIface), InternalEvent (InputEvent), Input (eventChannel)   ) import Lens.Micro.Platform @@ -530,3 +533,10 @@   -- | debugLevel >= uiDebugLevel = 0    -- suppress to improve debug output.   -- | otherwise    = 100  -- 100 ought to be enough for anyone++-- Send some events to vty, atomically so they won't have other events interleaved.+-- (But there may be events already in the channel ahead of them.)+sendVtyEvents :: [Event] -> EventM n s ()+sendVtyEvents evs = do+  input <- eventChannel . inputIface <$> getVtyHandle+  liftIO $ atomically $ mapM_ (writeTChan input . InputEvent) evs
hledger-ui.1 view
@@ -1,5 +1,5 @@ -.TH "HLEDGER\-UI" "1" "September 2025" "hledger-ui-1.50 " "hledger User Manuals"+.TH "HLEDGER\-UI" "1" "September 2025" "hledger-ui-1.50.1 " "hledger User Manuals"   @@ -17,7 +17,7 @@ .PD \f[CR]hledger ui [OPTS] [QUERYARGS]\f[R] .SH DESCRIPTION-This manual is for hledger\[aq]s terminal interface, version 1.50.+This manual is for hledger\[aq]s terminal interface, version 1.50.1. See also the hledger manual for common concepts and file formats. .PP hledger is a robust, user\-friendly, cross\-platform set of programs for
hledger-ui.cabal view
@@ -1,11 +1,11 @@ cabal-version: 2.2 --- This file has been generated from package.yaml by hpack version 0.38.2.+-- This file has been generated from package.yaml by hpack version 0.38.1. -- -- see: https://github.com/sol/hpack  name:           hledger-ui-version:        1.50+version:        1.50.1 synopsis:       Terminal interface for the hledger accounting system description:    A simple terminal user interface for the hledger accounting system.                 It can be a more convenient way to browse your accounts than the CLI.@@ -74,7 +74,7 @@   hs-source-dirs:       ./   ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind-  cpp-options: -DVERSION="1.50" -DVERSION="1.50"+  cpp-options: -DVERSION="1.50.1" -DVERSION="1.50.1"   build-depends:       ansi-terminal >=0.9     , async@@ -89,8 +89,8 @@     , filepath     , fsnotify >=0.4.2.0 && <0.5     , githash >=0.1.6.2-    , hledger ==1.50.*-    , hledger-lib ==1.50.*+    , hledger >=1.50.1 && <1.51+    , hledger-lib >=1.50.1 && <1.51     , megaparsec >=7.0.0 && <9.8     , microlens >=0.4     , microlens-platform >=0.2.3.1@@ -98,6 +98,7 @@     , process >=1.2     , safe >=0.3.20     , split >=0.1+    , stm     , text >=1.2.4.1     , text-zipper >=0.4     , time >=1.5@@ -124,7 +125,7 @@   hs-source-dirs:       app   ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind -with-rtsopts=-T-  cpp-options: -DVERSION="1.50"+  cpp-options: -DVERSION="1.50.1"   build-depends:       base >=4.18 && <4.22     , hledger-ui
hledger-ui.info view
@@ -18,7 +18,7 @@ or 'hledger ui [OPTS] [QUERYARGS]' -   This manual is for hledger's terminal interface, version 1.50.  See+   This manual is for hledger's terminal interface, version 1.50.1.  See also the hledger manual for common concepts and file formats.     hledger is a robust, user-friendly, cross-platform set of programs@@ -565,22 +565,22 @@  Tag Table: Node: Top221-Node: OPTIONS1867-Node: MOUSE8753-Node: KEYS9085-Node: SCREENS14089-Node: Menu screen14829-Node: Cash accounts screen15145-Node: Balance sheet accounts screen15506-Node: Income statement accounts screen15842-Node: All accounts screen16227-Node: Register screen16590-Node: Transaction screen19033-Node: Error screen20608-Node: WATCH MODE20974-Node: --watch problems21872-Node: ENVIRONMENT23225-Node: BUGS23458+Node: OPTIONS1869+Node: MOUSE8755+Node: KEYS9087+Node: SCREENS14091+Node: Menu screen14831+Node: Cash accounts screen15147+Node: Balance sheet accounts screen15508+Node: Income statement accounts screen15844+Node: All accounts screen16229+Node: Register screen16592+Node: Transaction screen19035+Node: Error screen20610+Node: WATCH MODE20976+Node: --watch problems21874+Node: ENVIRONMENT23227+Node: BUGS23460  End Tag Table 
hledger-ui.txt view
@@ -11,7 +11,7 @@        hledger ui [OPTS] [QUERYARGS]  DESCRIPTION-       This manual is for hledger's terminal  interface,  version  1.50.   See+       This manual is for hledger's terminal interface, version  1.50.1.   See        also the hledger manual for common concepts and file formats.         hledger  is a robust, user-friendly, cross-platform set of programs for@@ -471,4 +471,4 @@ SEE ALSO        hledger(1), hledger-ui(1), hledger-web(1), ledger(1) -hledger-ui-1.50                 September 2025                   HLEDGER-UI(1)+hledger-ui-1.50.1               September 2025                   HLEDGER-UI(1)