Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion hledger-ui/Hledger/UI/RegisterScreen.hs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,9 @@ rsHandle ev = do
where
p = reportPeriod ui

e | e `elem` [AppEvent FileChange, VtyEvent (EvKey (KChar 'g') [])] -> uiReload copts d ui >>= put'
e | e `elem` [AppEvent FileChange, VtyEvent (EvKey (KChar 'g') [])] -> do
ui' <- uiReload copts d ui
put' $ regenerateScreens (ajournal ui') d ui'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reloading in the register screen now has an extra call to regenerateScreens. Does this help ? I think it's also being called by uiReload. Needs an explanatory comment at least.


VtyEvent (EvKey (KChar 'I') []) -> put' $ uiCheckBalanceAssertions d (toggleIgnoreBalanceAssertions ui)
VtyEvent (EvKey (KChar 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add (cliOptsDropArgs copts) j >> uiReloadIfFileChanged copts d j ui
Expand Down
43 changes: 2 additions & 41 deletions hledger-ui/Hledger/UI/TransactionScreen.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import Hledger.UI.UIUtils
import Hledger.UI.UIScreens
import Hledger.UI.Editor
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}}}
Expand Down Expand Up @@ -191,47 +190,9 @@ tsHandle ev = do
-- 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
tsReload copts d ui = uiReload copts d ui >>= put'

tsReloadIfFileChanged copts d j ui = liftIO (uiReloadIfFileChanged copts d j ui) >>= put'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This special handling for regenerating transaction screen was added in 1.50.1 to fix #2288. It has been removed and I think not fully replaced by later commits ? Also is the comment above still accurate ?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS a little more context, my understanding was that the right fix for #2288 would be to make regenerateScreens take parent screens' state into account (fold rather than map).


-- | Select a new transaction and update the previous register screen
tsSelect :: Integer -> Transaction -> UIState -> UIState
Expand Down
40 changes: 25 additions & 15 deletions hledger-ui/Hledger/UI/UIScreens.hs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ screenUpdate opts d j = \case
BS sst -> BS $ bsUpdate opts d j sst
IS sst -> IS $ isUpdate opts d j sst
RS sst -> RS $ rsUpdate opts d j sst
TS sst -> TS $ tsUpdate sst
TS sst -> TS $ tsUpdate opts d j sst
ES sst -> ES $ esUpdate sst

-- | Construct an error screen.
Expand Down Expand Up @@ -241,11 +241,12 @@ rsNew uopts d j acct forceinclusive = -- XXX forcedefaultselection - whether to
}

-- | Update a register screen from these options, reporting date, and journal.
rsUpdate :: UIOpts -> Day -> Journal -> RegisterScreenState -> RegisterScreenState
rsUpdate uopts d j rss@RSS{_rssAccount, _rssForceInclusive, _rssList=oldlist} =
dbgui "rsUpdate"
rsUpdate uopts d j rss@RSS{_rssAccount, _rssForceInclusive} =
dbgui "rsUpdate" $
rss{_rssList=l'}
where
-- Force evaluation of old list to allow GC
!oldlist = _rssList rss
UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}} = uopts
-- gather arguments and queries
-- XXX temp
Expand Down Expand Up @@ -279,7 +280,8 @@ rsUpdate uopts d j rss@RSS{_rssAccount, _rssForceInclusive, _rssList=oldlist} =
items

-- pre-render the list items, helps calculate column widths
displayitems = map displayitem items'
-- Force evaluation to prevent thunk accumulation
!displayitems = map displayitem items'
where
displayitem (t, _, _issplit, otheraccts, change, bal) =
RegisterScreenItem{rsItemDate = showDate $ transactionRegisterDate wd (_rsQuery rspec') thisacctq t
Expand All @@ -297,7 +299,7 @@ rsUpdate uopts d j rss@RSS{_rssAccount, _rssForceInclusive, _rssList=oldlist} =

-- blank items are added to allow more control of scroll position; we won't allow movement over these.
-- XXX Ugly. Changing to 0 helps when debugging.
blankitems = replicate uiNumBlankItems
!blankitems = replicate uiNumBlankItems
RegisterScreenItem{rsItemDate = ""
,rsItemStatus = Unmarked
,rsItemDescription = ""
Expand All @@ -308,15 +310,16 @@ rsUpdate uopts d j rss@RSS{_rssAccount, _rssForceInclusive, _rssList=oldlist} =
}

-- build the new list widget
l = list RegisterList (V.fromList $ displayitems ++ blankitems) 1
!itemsVector = V.fromList $ displayitems ++ blankitems
!l = list RegisterList itemsVector 1

-- ensure the appropriate list item is selected:
-- if forcedefaultselection is true, the last (latest) transaction; XXX still needed ?
-- otherwise, the previously selected transaction if possible;
-- otherwise, the transaction nearest in date to it;
-- or if there's several with the same date, the nearest in journal order;
-- otherwise, the last (latest) transaction.
l' = listMoveTo newselidx l
!l' = listMoveTo newselidx l
where
endidx = max 0 $ length displayitems - 1
newselidx =
Expand Down Expand Up @@ -357,10 +360,17 @@ tsNew acct nts nt =
,_tssTransaction = nt
}

-- | Update a transaction screen.
-- This currently does nothing because the initialisation in rsHandle is not so easy to extract.
-- To see the updated transaction, one must exit and re-enter the transaction screen.
-- See also tsHandle.
tsUpdate :: TransactionScreenState -> TransactionScreenState
tsUpdate = dbgui "tsUpdate"

-- | Update a transaction screen by refreshing the current transaction from the journal.
-- This preserves the current transaction selection while updating its data.
-- XXX Caveat, this works by showing the transaction at the same index in the processed ledger,
-- if transactions have been inserted or removed before the one shown this will just show
-- whatever transaction landed at the same index in the new data set.
tsUpdate :: UIOpts -> Day -> Journal -> TransactionScreenState -> TransactionScreenState
tsUpdate _ _ j tss@TSS{_tssTransaction=(currentIdx,currentTxn)} =
dbgui "tsUpdate" $
let
-- Find the updated version of the current transaction in the journal
updatedTxn = case find (\t -> tindex t == tindex currentTxn) (jtxns j) of
Just t -> t
Nothing -> currentTxn -- fallback to current if not found
in tss { _tssTransaction = (currentIdx, updatedTxn) }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this replace the special regeneration previously in TransactionScreen.hs ?
Jumping to a different transaction after reload or file change is not ideal.
I think the old code exited to the register screen in that situation.

20 changes: 15 additions & 5 deletions hledger-ui/Hledger/UI/UIState.hs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import Hledger
import Hledger.Cli.CliOptions
import Hledger.UI.UITypes
import Hledger.UI.UIOptions (UIOpts(uoCliOpts))
import Hledger.UI.UIScreens (screenUpdate)
import Hledger.UI.UIScreens
import Hledger.UI.UIUtils (showScreenId, showScreenStack)

-- | Make an initial UI state with the given options, journal,
Expand Down Expand Up @@ -366,8 +366,18 @@ resetScreens d ui@UIState{astartupopts=origopts, ajournal=j, aScreen=s,aPrevScre
-- (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.
-- XXX Currently this does not properly regenerate or error screen,
-- which depends on state from their parent(s); that screens' handler 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}
regenerateScreens j d ui@UIState{aScreen=s} =
let !ui' = ui{ajournal=j, aScreen=s'}
!s' = case s of
MS mss -> MS $! msUpdate mss
AS ass -> AS $! asUpdate (aopts ui') d j ass
CS ass -> CS $! csUpdate (aopts ui') d j ass
BS ass -> BS $! bsUpdate (aopts ui') d j ass
IS ass -> IS $! isUpdate (aopts ui') d j ass
RS rss -> RS $! rsUpdate (aopts ui') d j rss
TS tss -> TS $! tsUpdate (aopts ui') d j tss
ES _ -> s
in ui'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screen types and functions are listed explicitly in a bunch of places, so maybe it's no great loss to do that here also - but is it possible to achieve the same strictness without listing them all ?

More seriously, doesn't it now only regenerate the current screen ?

Copy link
Owner

@simonmichael simonmichael Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Comments so far are based on code review, I haven't tested yet. Unfortunately it's easy to break behaviours in hledger-ui in ways that currently are hard to test, except manually.)

4 changes: 0 additions & 4 deletions hledger-ui/hledger-ui.m4.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,14 +331,10 @@ eg to toggle cleared mode, or to explore the history.
([#1617](https://github.com/simonmichael/hledger/issues/1617))
- It may not detect changes made from outside a virtual machine, ie by an editor running on the host system.
- It may not detect file changes on certain less common filesystems.
- It may use increasing CPU and RAM over time, especially with large files.
(This is probably not --watch specific, you may be able to reproduce it by pressing `g` repeatedly.)
([#1825](https://github.com/simonmichael/hledger/issues/1825))

Tips/workarounds:

- If --watch won't work for you, press `g` to reload data manually instead.
- If --watch is leaking resources over time, quit and restart (or suspend and resume) hledger-ui when you're not using it.
- When running hledger-ui inside a VM, also make file changes inside the VM.
- When working with files mounted from another machine, make sure the system clocks on both machines are roughly in agreement.

Expand Down