-- | Constructors and updaters for all hledger-ui screens. -- -- Constructors (*New) create and initialise a new screen with valid state, -- based on the provided options, reporting date, journal, and screen-specific parameters. -- -- Updaters (*Update) recalculate an existing screen's state, -- based on new options, reporting date, journal, and the old screen state. -- -- These are gathered in this low-level module so that any screen's handler -- can create or regenerate all other screens. -- Drawing and event-handling code is elsewhere, in per-screen modules. {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NamedFieldPuns #-} module Hledger.UI.UIScreens (screenUpdate ,esNew ,esUpdate ,msNew ,msUpdate ,asNew ,asUpdate ,bsNew ,bsUpdate ,isNew ,isUpdate ,rsNew ,rsUpdate ,tsNew ,tsUpdate ) where import Brick.Widgets.List (listMoveTo, listSelectedElement, list) import Data.List import Data.Maybe import Data.Time.Calendar (Day, diffDays) import Safe import qualified Data.Vector as V import Hledger.Cli hiding (mode, progname,prognameandversion) import Hledger.UI.UIOptions import Hledger.UI.UITypes import Hledger.UI.UIUtils import Data.Function ((&)) -- | Regenerate the content of any screen from new options, reporting date and journal. screenUpdate :: UIOpts -> Day -> Journal -> Screen -> Screen screenUpdate opts d j = \case MS sst -> MS $ msUpdate sst -- opts d j ass AS sst -> AS $ asUpdate opts d j sst 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 ES sst -> ES $ esUpdate sst -- | Construct an error screen. -- Screen-specific arguments: the error message to show. esNew :: String -> Screen esNew msg = dbgui "esNew" $ ES ESS { _essError = msg ,_essUnused = () } -- | Update an error screen. Currently a no-op since error screen -- depends only on its screen-specific state. esUpdate :: ErrorScreenState -> ErrorScreenState esUpdate = dbgui "esUpdate`" -- | Construct a menu screen. -- Screen-specific arguments: none. msNew :: Screen msNew = dbgui "msNew" $ MS MSS { _mssList = list MenuList (V.fromList [ -- keep initial screen stack setup in UI.Main synced with these MenuScreenItem "All accounts" Accounts ,MenuScreenItem "Balance sheet accounts" Balancesheet ,MenuScreenItem "Income statement accounts" Incomestatement ]) 1 & listMoveTo 1 -- select balance sheet accounts screen at startup (currently this screen is constructed only then) ,_mssUnused = () } -- | Update a menu screen. Currently a no-op since menu screen -- has unchanging content. msUpdate :: MenuScreenState -> MenuScreenState msUpdate = dbgui "msUpdate" nullass macct = ASS { _assSelectedAccount = fromMaybe "" macct ,_assList = list AccountsList (V.fromList []) 1 } -- | Construct an accounts screen listing the appropriate set of accounts, -- with the appropriate one selected. -- Screen-specific arguments: the account to select if any. asNew :: UIOpts -> Day -> Journal -> Maybe AccountName -> Screen asNew uopts d j macct = dbgui "asNew" $ AS $ asUpdate uopts d j $ nullass macct -- | Update an accounts screen's state from these options, reporting date, and journal. asUpdate :: UIOpts -> Day -> Journal -> AccountsScreenState -> AccountsScreenState asUpdate uopts d = dbgui "asUpdate" . asUpdateHelper rspec d copts roptsmod extraquery where UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec}} = uopts roptsmod = id extraquery = Any -- | Update an accounts-like screen's state from this report spec, reporting date, -- cli options, report options modifier, extra query, and journal. asUpdateHelper :: ReportSpec -> Day -> CliOpts -> (ReportOpts -> ReportOpts) -> Query -> Journal -> AccountsScreenState -> AccountsScreenState asUpdateHelper rspec0 d copts roptsModify extraquery j ass = dbgui "asUpdateHelper" ass{_assList=l} where ropts = roptsModify $ _rsReportOpts rspec0 rspec = updateReportSpec ropts rspec0{_rsDay=d} -- update to the current date, might have changed since program start & either (error "asUpdateHelper: adjusting the query, should not have failed") id -- PARTIAL: & reportSpecSetFutureAndForecast (forecast_ $ inputopts_ copts) -- include/exclude future & forecast transactions & reportSpecAddQuery extraquery -- add any extra restrictions l = listMoveTo selidx $ list AccountsList (V.fromList $ displayitems ++ blankitems) 1 where -- which account should be selected ? selidx = headDef 0 $ catMaybes [ elemIndex a as -- the one previously selected, if it can be found ,findIndex (a `isAccountNamePrefixOf`) as -- or the first account found with the same prefix ,Just $ max 0 (length (filter (< a) as) - 1) -- otherwise, the alphabetically preceding account. ] where a = _assSelectedAccount ass as = map asItemAccountName displayitems displayitems = map displayitem items where -- run the report (items, _) = balanceReport rspec j -- pre-render a list item displayitem (fullacct, shortacct, indent, bal) = AccountsScreenItem{asItemIndentLevel = indent ,asItemAccountName = fullacct ,asItemDisplayAccountName = replaceHiddenAccountsNameWith "All" $ if tree_ ropts then shortacct else fullacct ,asItemMixedAmount = Just bal } -- blanks added for scrolling control, cf RegisterScreen. blankitems = replicate uiNumBlankItems -- XXX ugly hard-coded value. When debugging, changing to 0 reduces noise. AccountsScreenItem{asItemIndentLevel = 0 ,asItemAccountName = "" ,asItemDisplayAccountName = "" ,asItemMixedAmount = Nothing } -- | Construct a balance sheet screen listing the appropriate set of accounts, -- with the appropriate one selected. -- Screen-specific arguments: the account to select if any. bsNew :: UIOpts -> Day -> Journal -> Maybe AccountName -> Screen bsNew uopts d j macct = dbgui "bsNew" $ BS $ bsUpdate uopts d j $ nullass macct -- | Update a balance sheet screen's state from these options, reporting date, and journal. bsUpdate :: UIOpts -> Day -> Journal -> AccountsScreenState -> AccountsScreenState bsUpdate uopts d = dbgui "bsUpdate" . asUpdateHelper rspec d copts roptsmod extraquery where UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec}} = uopts roptsmod ropts = ropts{balanceaccum_=Historical} -- always show historical end balances extraquery = Type [Asset,Liability,Equity] -- restrict to balance sheet accounts -- | Construct an income statement screen listing the appropriate set of accounts, -- with the appropriate one selected. -- Screen-specific arguments: the account to select if any. isNew :: UIOpts -> Day -> Journal -> Maybe AccountName -> Screen isNew uopts d j macct = dbgui "isNew" $ IS $ isUpdate uopts d j $ nullass macct -- | Update an income statement screen's state from these options, reporting date, and journal. isUpdate :: UIOpts -> Day -> Journal -> AccountsScreenState -> AccountsScreenState isUpdate uopts d = dbgui "isUpdate" . asUpdateHelper rspec d copts roptsmod extraquery where UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec}} = uopts roptsmod ropts = ropts{balanceaccum_=PerPeriod} -- always show historical end balances extraquery = Type [Revenue, Expense] -- restrict to income statement accounts -- | Construct a register screen listing the appropriate set of transactions, -- with the appropriate one selected. -- Screen-specific arguments: the account whose register this is, -- whether to force inclusive balances. rsNew :: UIOpts -> Day -> Journal -> AccountName -> Bool -> Screen rsNew uopts d j acct forceinclusive = -- XXX forcedefaultselection - whether to force selecting the last transaction. dbgui "rsNew" $ RS $ rsUpdate uopts d j $ RSS { _rssAccount = replaceHiddenAccountsNameWith "*" acct ,_rssForceInclusive = forceinclusive ,_rssList = list RegisterList (V.fromList []) 1 } -- | 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" rss{_rssList=l'} where UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}} = uopts -- gather arguments and queries -- XXX temp inclusive = tree_ ropts || _rssForceInclusive thisacctq = Acct $ mkregex _rssAccount where mkregex = if inclusive then accountNameToAccountRegex else accountNameToAccountOnlyRegex -- adjust the report options and report spec, carefully as usual to avoid screwups (#1523) ropts' = ropts { -- ignore any depth limit, as in postingsReport; allows register's total to match accounts screen depth_=Nothing -- do not strip prices so we can toggle costs within the ui , show_costs_=True -- XXX aregister also has this, needed ? -- always show historical balance -- , balanceaccum_= Historical } rspec' = updateReportSpec ropts' rspec{_rsDay=d} & either (error "rsUpdate: adjusting the query for register, should not have failed") id -- PARTIAL: & reportSpecSetFutureAndForecast (forecast_ $ inputopts_ copts) -- gather transactions to display items = accountTransactionsReport rspec' j thisacctq items' = (if empty_ ropts then id else filter (not . mixedAmountLooksZero . fifth6)) $ -- without --empty, exclude no-change txns reverse -- most recent last items -- pre-render the list items, helps calculate column widths displayitems = map displayitem items' where displayitem (t, _, _issplit, otheracctsstr, change, bal) = RegisterScreenItem{rsItemDate = showDate $ transactionRegisterDate wd (_rsQuery rspec') thisacctq t ,rsItemStatus = tstatus t ,rsItemDescription = tdescription t ,rsItemOtherAccounts = otheracctsstr -- _ -> "" -- should do this if accounts field width < 30 ,rsItemChangeAmount = showamt change ,rsItemBalanceAmount = showamt bal ,rsItemTransaction = t } where showamt = showMixedAmountB oneLine{displayMaxWidth=Just 3} wd = whichDate ropts' -- 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 RegisterScreenItem{rsItemDate = "" ,rsItemStatus = Unmarked ,rsItemDescription = "" ,rsItemOtherAccounts = "" ,rsItemChangeAmount = mempty ,rsItemBalanceAmount = mempty ,rsItemTransaction = nulltransaction } -- build the new list widget l = list RegisterList (V.fromList $ displayitems ++ blankitems) 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 where endidx = max 0 $ length displayitems - 1 newselidx = -- case (forcedefaultselection, listSelectedElement _rssList) of -- (True, _) -> endidx -- (_, Nothing) -> endidx -- (_, Just (_, RegisterScreenItem{rsItemTransaction=Transaction{tindex=prevselidx, tdate=prevseld}})) -> -- headDef endidx $ catMaybes [ -- findIndex ((==prevselidx) . tindex . rsItemTransaction) displayitems -- ,findIndex ((==nearestidbydatethenid) . Just . tindex . rsItemTransaction) displayitems -- ] -- where -- nearestidbydatethenid = third3 <$> (headMay $ sort -- [(abs $ diffDays (tdate t) prevseld, abs (tindex t - prevselidx), tindex t) | t <- ts]) -- ts = map rsItemTransaction displayitems case listSelectedElement oldlist of Nothing -> endidx Just (_, RegisterScreenItem{rsItemTransaction=Transaction{tindex=prevselidx, tdate=prevseld}}) -> headDef endidx $ catMaybes [ findIndex ((==prevselidx) . tindex . rsItemTransaction) displayitems ,findIndex ((==nearestidbydatethenid) . Just . tindex . rsItemTransaction) displayitems ] where nearestidbydatethenid = third3 <$> (headMay $ sort [(abs $ diffDays (tdate t) prevseld, abs (tindex t - prevselidx), tindex t) | t <- ts]) ts = map rsItemTransaction displayitems -- | Construct a transaction screen showing one of a given list of transactions, -- with the ability to step back and forth through the list. -- Screen-specific arguments: the account whose transactions are being shown, -- the list of showable transactions, the currently shown transaction. tsNew :: AccountName -> [NumberedTransaction] -> NumberedTransaction -> Screen tsNew acct nts nt = dbgui "tsNew" $ TS TSS{ _tssAccount = acct ,_tssTransactions = nts ,_tssTransaction = nt } -- | Update a transaction screen. Currently a no-op since transaction screen -- depends only on its screen-specific state. tsUpdate :: TransactionScreenState -> TransactionScreenState tsUpdate = dbgui "tsUpdate"