hledger-ui 1.27.1 → 1.28
raw patch · 18 files changed
+2153/−1078 lines, 18 filesdep −breakpointdep ~basedep ~brickdep ~fsnotify
Dependencies removed: breakpoint
Dependency ranges changed: base, brick, fsnotify, hledger, hledger-lib, megaparsec
Files
- CHANGES.md +54/−0
- Hledger/UI/AccountsScreen.hs +310/−308
- Hledger/UI/BalancesheetScreen.hs +29/−0
- Hledger/UI/ErrorScreen.hs +23/−30
- Hledger/UI/IncomestatementScreen.hs +29/−0
- Hledger/UI/Main.hs +134/−63
- Hledger/UI/MenuScreen.hs +273/−0
- Hledger/UI/RegisterScreen.hs +74/−150
- Hledger/UI/TransactionScreen.hs +38/−69
- Hledger/UI/UIOptions.hs +9/−5
- Hledger/UI/UIScreens.hs +329/−0
- Hledger/UI/UIState.hs +74/−37
- Hledger/UI/UITypes.hs +174/−60
- Hledger/UI/UIUtils.hs +142/−42
- hledger-ui.1 +95/−51
- hledger-ui.cabal +14/−11
- hledger-ui.info +163/−99
- hledger-ui.txt +189/−153
CHANGES.md view
@@ -9,6 +9,60 @@ User-visible changes in hledger-ui. See also the hledger changelog. +# 1.28 2022-11-30++Features++- New "Balance sheet accounts" and "Income statement accounts" screens have been added,+ along with a new top-level "Menu" screen for navigating between these and the+ "All accounts" screen.++- hledger-ui now starts in the "Balance sheet accounts" screen by default+ (unless no asset/liability/equity accounts can be detected,+ or command line account query arguments are provided).+ This provides a more useful default view than the giant "All accounts" list.+ Or, you can force a particular starting screen with the new --menu/--all/--bs/--is flags+ (eg, `hledger-ui --all` to replicate the old behaviour).++Improvements++- The ENTER key is equivalent to RIGHT for navigation.++- hledger-ui debug output is now always logged to ./hledger-ui.log rather than the console,+ --debug with no argument is equivalent to --debug=1,+ and debug output is much more informative.++- Support GHC 9.4.++- Support megaparsec 9.3 (Felix Yan)++- Support (and require) brick 1.5, fsnotify 0.4.x.++Fixes++- Mouse-clicking in empty space below the last list item no longer navigates+ back. It was too obtrusive, eg when you just want to focus the window. You can still navigate back with the mouse by clicking the left edge of the window.++- A possible bug with detecting change of date while in --watch mode has been fixed.++API++- hledger-ui's internal types have been changed to allow fewer invalid states and make it easier to develop and debug.+ (#1889, #1919).++- Debug logging helpers have been added and cleaned up in Hledger.Ui.UIUtils:+ dbgui+ dbguiIO+ dbguiEv+ dbguiScreensEv+ mapScreens+ screenId+ screenRegisterDescriptions++# 1.27.1 2022-09-18++- Uses hledger-1.27.1+ # 1.27 2022-09-01 Improvements
Hledger/UI/AccountsScreen.hs view
@@ -1,12 +1,19 @@ -- The accounts screen, showing accounts and balances like the CLI balance command. -{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TupleSections #-} module Hledger.UI.AccountsScreen- (accountsScreen- ,asInit+ (asNew+ ,asUpdate+ ,asDraw+ ,asDrawHelper+ ,asHandle+ ,handleHelpMode+ ,handleMinibufferMode+ ,asHandleNormalMode+ ,enterRegisterScreen ,asSetSelectedAccount ) where@@ -21,189 +28,134 @@ import qualified Data.Text as T import Data.Time.Calendar (Day) import qualified Data.Vector as V+import Data.Vector ((!?)) import Graphics.Vty (Event(..),Key(..),Modifier(..), Button (BLeft, BScrollDown, BScrollUp)) import Lens.Micro.Platform-import Safe import System.Console.ANSI import System.FilePath (takeFileName) import Text.DocLayout (realLength) import Hledger-import Hledger.Cli hiding (mode, progname, prognameandversion)+import Hledger.Cli hiding (Mode, mode, progname, prognameandversion) import Hledger.UI.UIOptions import Hledger.UI.UITypes import Hledger.UI.UIState import Hledger.UI.UIUtils+import Hledger.UI.UIScreens import Hledger.UI.Editor-import Hledger.UI.RegisterScreen-import Hledger.UI.ErrorScreen-import Data.Vector ((!?))---accountsScreen :: Screen-accountsScreen = AccountsScreen{- sInit = asInit- ,sDraw = asDraw- ,sHandle = asHandle- ,_asList = list AccountsList V.empty 1- ,_asSelectedAccount = ""- }--asInit :: Day -> Bool -> UIState -> UIState-asInit d reset ui@UIState{- aopts=UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}},- ajournal=j,- aScreen=s@AccountsScreen{}- } = dlogUiTrace "asInit 1" $- ui{aScreen=s & asList .~ newitems'}- where- newitems = list AccountsList (V.fromList $ displayitems ++ blankitems) 1-- -- decide which account is selected:- -- if reset is true, the first account;- -- otherwise, the previously selected account if possible;- -- otherwise, the first account with the same prefix (eg first leaf account when entering flat mode);- -- otherwise, the alphabetically preceding account.- newitems' = listMoveTo selidx newitems- where- selidx = case (reset, listSelectedElement $ _asList s) of- (True, _) -> 0- (_, Nothing) -> 0- (_, Just (_,AccountsScreenItem{asItemAccountName=a})) ->- headDef 0 $ catMaybes [- elemIndex a as- ,findIndex (a `isAccountNamePrefixOf`) as- ,Just $ max 0 (length (filter (< a) as) - 1)- ]- where- as = map asItemAccountName displayitems-- rspec' =- -- Further restrict the query based on the current period and future/forecast mode.- (reportSpecSetFutureAndForecast d (forecast_ $ inputopts_ copts) rspec)- -- always show declared accounts even if unused- {_rsReportOpts=ropts{declared_=True}}-- -- run the report- (items,_total) = balanceReport rspec' j-- -- pre-render the list items- displayitem (fullacct, shortacct, indent, bal) =- AccountsScreenItem{asItemIndentLevel = indent- ,asItemAccountName = fullacct- ,asItemDisplayAccountName = replaceHiddenAccountsNameWith "All" $ if tree_ ropts then shortacct else fullacct- ,asItemMixedAmount = Just bal- }- displayitems = map displayitem items- -- blanks added for scrolling control, cf RegisterScreen.- -- XXX Ugly. Changing to 0 helps when debugging.- blankitems = replicate uiNumBlankItems- AccountsScreenItem{asItemIndentLevel = 0- ,asItemAccountName = ""- ,asItemDisplayAccountName = ""- ,asItemMixedAmount = Nothing- }+import Hledger.UI.ErrorScreen (uiReloadJournal, uiCheckBalanceAssertions, uiReloadJournalIfChanged)+import Hledger.UI.RegisterScreen (rsCenterSelection)+import Data.Either (fromRight)+import Control.Arrow ((>>>))+import Safe (headDef) -asInit _ _ _ = dlogUiTrace "asInit 2" $ errorWrongScreenType "init function" -- PARTIAL:- asDraw :: UIState -> [Widget Name]-asDraw UIState{aopts=_uopts@UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec}}- ,ajournal=j- ,aScreen=s@AccountsScreen{}- ,aMode=mode- } = dlogUiTrace "asDraw 1" $- case mode of- Help -> [helpDialog copts, maincontent]- -- Minibuffer e -> [minibuffer e, maincontent]- _ -> [maincontent]+asDraw ui = dbgui "asDraw" $ asDrawHelper ui ropts' scrname where- maincontent = Widget Greedy Greedy $ do- c <- getContext- let- availwidth =- -- ltrace "availwidth" $- c^.availWidthL- - 2 -- XXX due to margin ? shouldn't be necessary (cf UIUtils)- displayitems = s ^. asList . listElementsL+ ropts' = _rsReportOpts $ reportspec_ $ uoCliOpts $ aopts ui+ scrname = "account " ++ if ishistorical then "balances" else "changes"+ where ishistorical = balanceaccum_ ropts' == Historical - acctwidths = V.map (\AccountsScreenItem{..} -> asItemIndentLevel + realLength asItemDisplayAccountName) displayitems- balwidths = V.map (maybe 0 (wbWidth . showMixedAmountB oneLine) . asItemMixedAmount) displayitems- preferredacctwidth = V.maximum acctwidths- totalacctwidthseen = V.sum acctwidths- preferredbalwidth = V.maximum balwidths- totalbalwidthseen = V.sum balwidths+-- | Help draw any accounts-like screen (all accounts, balance sheet, income statement..).+-- The provided ReportOpts are used instead of the ones in the UIState.+-- The other argument is the screen display name.+asDrawHelper :: UIState -> ReportOpts -> String -> [Widget Name]+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:+ Just (ALS _ ass) -> case mode of+ Help -> [helpDialog, maincontent]+ _ -> [maincontent]+ where+ UIOpts{uoCliOpts=copts} = uopts+ maincontent = Widget Greedy Greedy $ do+ c <- getContext+ let+ availwidth =+ -- ltrace "availwidth" $+ c^.availWidthL+ - 2 -- XXX due to margin ? shouldn't be necessary (cf UIUtils)+ displayitems = ass ^. assList . listElementsL - totalwidthseen = totalacctwidthseen + totalbalwidthseen- shortfall = preferredacctwidth + preferredbalwidth + 2 - availwidth- acctwidthproportion = fromIntegral totalacctwidthseen / fromIntegral totalwidthseen- adjustedacctwidth = min preferredacctwidth . max 15 . round $ acctwidthproportion * fromIntegral (availwidth - 2) -- leave 2 whitespace for padding- adjustedbalwidth = availwidth - 2 - adjustedacctwidth+ acctwidths = V.map (\AccountsScreenItem{..} -> asItemIndentLevel + realLength asItemDisplayAccountName) displayitems+ balwidths = V.map (maybe 0 (wbWidth . showMixedAmountB oneLine) . asItemMixedAmount) displayitems+ preferredacctwidth = V.maximum acctwidths+ totalacctwidthseen = V.sum acctwidths+ preferredbalwidth = V.maximum balwidths+ totalbalwidthseen = V.sum balwidths - -- XXX how to minimise the balance column's jumping around as you change the depth limit ?+ totalwidthseen = totalacctwidthseen + totalbalwidthseen+ shortfall = preferredacctwidth + preferredbalwidth + 2 - availwidth+ acctwidthproportion = fromIntegral totalacctwidthseen / fromIntegral totalwidthseen+ adjustedacctwidth = min preferredacctwidth . max 15 . round $ acctwidthproportion * fromIntegral (availwidth - 2) -- leave 2 whitespace for padding+ adjustedbalwidth = availwidth - 2 - adjustedacctwidth - colwidths | shortfall <= 0 = (preferredacctwidth, preferredbalwidth)- | otherwise = (adjustedacctwidth, adjustedbalwidth)+ -- XXX how to minimise the balance column's jumping around as you change the depth limit ? - render $ defaultLayout toplabel bottomlabel $ renderList (asDrawItem colwidths) True (_asList s)+ colwidths | shortfall <= 0 = (preferredacctwidth, preferredbalwidth)+ | otherwise = (adjustedacctwidth, adjustedbalwidth) - where- ropts = _rsReportOpts rspec- ishistorical = balanceaccum_ ropts == Historical+ render $ defaultLayout toplabel bottomlabel $ renderList (asDrawItem colwidths) True (ass ^. assList) - toplabel =- withAttr (attrName "border" <> attrName "filename") files- <+> toggles- <+> str (" account " ++ if ishistorical then "balances" else "changes")- <+> borderPeriodStr (if ishistorical then "at end of" else "in") (period_ ropts)- <+> borderQueryStr (T.unpack . T.unwords . map textQuoteIfNeeded $ querystring_ ropts)- <+> borderDepthStr mdepth- <+> str (" ("++curidx++"/"++totidx++")")- <+> (if ignore_assertions_ . balancingopts_ $ inputopts_ copts- then withAttr (attrName "border" <> attrName "query") (str " ignoring balance assertions")- else str "") where- files = case journalFilePaths j of- [] -> str ""- f:_ -> str $ takeFileName f- -- [f,_:[]] -> (withAttr ("border" <> "bold") $ str $ takeFileName f) <+> str " (& 1 included file)"- -- f:fs -> (withAttr ("border" <> "bold") $ str $ takeFileName f) <+> str (" (& " ++ show (length fs) ++ " included files)")- toggles = withAttr (attrName "border" <> attrName "query") $ str $ unwords $ concat [- [""]- ,if empty_ ropts then [] else ["nonzero"]- ,uiShowStatus copts $ statuses_ ropts- ,if real_ ropts then ["real"] else []- ]- mdepth = depth_ ropts- curidx = case _asList s ^. listSelectedL of- Nothing -> "-"- Just i -> show (i + 1)- totidx = show $ V.length nonblanks- where- nonblanks = V.takeWhile (not . T.null . asItemAccountName) $ s ^. asList . listElementsL+ ishistorical = balanceaccum_ ropts == Historical - bottomlabel = case mode of- Minibuffer label ed -> minibuffer label ed- _ -> quickhelp- where- quickhelp = borderKeysStr' [- ("?", str "help")--- ,("RIGHT", str "register")- ,("t", renderToggle (tree_ ropts) "list" "tree")- -- ,("t", str "tree")- -- ,("l", str "list")- ,("-+", str "depth")- ,("H", renderToggle (not ishistorical) "end-bals" "changes")- ,("F", renderToggle1 (isJust . forecast_ $ inputopts_ copts) "forecast")- --,("/", "filter")- --,("DEL", "unfilter")- --,("ESC", "cancel/top")- ,("a", str "add")--- ,("g", "reload")- ,("q", str "quit")- ]+ toplabel =+ withAttr (attrName "border" <> attrName "filename") files+ <+> toggles+ <+> str (" " ++ scrname)+ <+> borderPeriodStr (if ishistorical then "at end of" else "in") (period_ ropts)+ <+> borderQueryStr (T.unpack . T.unwords . map textQuoteIfNeeded $ querystring_ ropts)+ <+> borderDepthStr mdepth+ <+> str (" ("++curidx++"/"++totidx++")")+ <+> (if ignore_assertions_ . balancingopts_ $ inputopts_ copts+ then withAttr (attrName "border" <> attrName "query") (str " ignoring balance assertions")+ else str "")+ where+ files = case journalFilePaths j of+ [] -> str ""+ f:_ -> str $ takeFileName f+ -- [f,_:[]] -> (withAttr ("border" <> "bold") $ str $ takeFileName f) <+> str " (& 1 included file)"+ -- f:fs -> (withAttr ("border" <> "bold") $ str $ takeFileName f) <+> str (" (& " ++ show (length fs) ++ " included files)")+ toggles = withAttr (attrName "border" <> attrName "query") $ str $ unwords $ concat [+ [""]+ ,if empty_ ropts then [] else ["nonzero"]+ ,uiShowStatus copts $ statuses_ ropts+ ,if real_ ropts then ["real"] else []+ ]+ mdepth = depth_ ropts+ curidx = case ass ^. assList . listSelectedL of+ Nothing -> "-"+ Just i -> show (i + 1)+ totidx = show $ V.length nonblanks+ where+ nonblanks = V.takeWhile (not . T.null . asItemAccountName) $ ass ^. assList . listElementsL -asDraw _ = dlogUiTrace "asDraw 2" $ errorWrongScreenType "draw function" -- PARTIAL:+ bottomlabel = case mode of+ Minibuffer label ed -> minibuffer label ed+ _ -> quickhelp+ where+ quickhelp = borderKeysStr' [+ ("?", str "help")+ -- ,("RIGHT", str "register")+ ,("t", renderToggle (tree_ ropts) "list" "tree")+ -- ,("t", str "tree")+ -- ,("l", str "list")+ ,("-+", str "depth")+ ,case scr of+ BS _ -> ("", str "")+ IS _ -> ("", str "")+ _ -> ("H", renderToggle (not ishistorical) "end-bals" "changes")+ ,("F", renderToggle1 (isJust . forecast_ $ inputopts_ copts) "forecast")+ --,("/", "filter")+ --,("DEL", "unfilter")+ --,("ESC", "cancel/top")+ ,("a", str "add")+ -- ,("g", "reload")+ ,("q", str "quit")+ ] asDrawItem :: (Int,Int) -> Bool -> AccountsScreenItem -> Widget Name asDrawItem (acctwidth, balwidth) selected AccountsScreenItem{..} =@@ -225,178 +177,228 @@ sel | selected = (<> attrName "selected") | otherwise = id +-- | Handle events on any accounts-like screen (all accounts, balance sheet, income statement..). asHandle :: BrickEvent Name AppEvent -> EventM Name UIState () asHandle ev = do- ui0 <- get'- dlogUiTraceM "asHandle 1"- case ui0 of- ui1@UIState{- aScreen=scr@AccountsScreen{..}- ,aopts=UIOpts{uoCliOpts=copts}- ,ajournal=j- ,aMode=mode- } -> do+ dbguiEv "asHandle"+ ui0@UIState{aScreen=scr, aMode=mode} <- get'+ case toAccountsLikeScreen scr of+ Nothing -> dbgui "asHandle" $ errorWrongScreenType "event handler" -- 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}}+ case mode of+ Normal -> asHandleNormalMode als ev+ Minibuffer _ ed -> handleMinibufferMode ed ev+ Help -> handleHelpMode ev - let- -- save the currently selected account, in case we leave this screen and lose the selection- selacct = case listSelectedElement _asList of- Just (_, AccountsScreenItem{..}) -> asItemAccountName- Nothing -> scr ^. asSelectedAccount- ui = ui1{aScreen=scr & asSelectedAccount .~ selacct}- nonblanks = V.takeWhile (not . T.null . asItemAccountName) $ _asList^.listElementsL- lastnonblankidx = max 0 (length nonblanks - 1)- journalspan = journalDateSpan False j- d = copts^.rsDay+-- | Handle events when in normal mode on any accounts-like screen.+-- The provided AccountsLikeScreen should correspond to the ui state's current screen.+asHandleNormalMode :: AccountsLikeScreen -> BrickEvent Name AppEvent -> EventM Name UIState ()+asHandleNormalMode (ALS scons ass) ev = do+ dbguiEv "asHandleNormalMode" - case mode of- Minibuffer _ ed ->- case ev of- VtyEvent (EvKey KEsc []) -> put' $ closeMinibuffer ui- VtyEvent (EvKey KEnter []) -> put' $ regenerateScreens j d $- case setFilter s $ closeMinibuffer ui of- Left bad -> showMinibuffer "Cannot compile regular expression" (Just bad) ui- Right ui' -> ui'- where s = chomp $ unlines $ map strip $ getEditContents ed- VtyEvent (EvKey (KChar 'l') [MCtrl]) -> redraw- VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui- VtyEvent e -> do- ed' <- nestEventM' ed $ handleEditorEvent (VtyEvent e)- put' ui{aMode=Minibuffer "filter" ed'}- AppEvent _ -> return ()- MouseDown{} -> return ()- MouseUp{} -> return ()+ ui@UIState{aopts=UIOpts{uoCliOpts=copts}, ajournal=j} <- get'+ d <- liftIO getCurrentDay+ let+ l = _assList ass+ selacct = asSelectedAccount ass+ centerSelection = scrollSelectionToMiddle l+ clickedAcctAt y =+ case asItemAccountName <$> listElements l !? y of+ Just t | not $ T.null t -> Just t+ _ -> Nothing+ nonblanks = V.takeWhile (not . T.null . asItemAccountName) $ listElements l+ lastnonblankidx = max 0 (length nonblanks - 1)+ journalspan = journalDateSpan False j - Help ->- case ev of- -- VtyEvent (EvKey (KChar 'q') []) -> halt- VtyEvent (EvKey (KChar 'l') [MCtrl]) -> redraw- VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui- _ -> helpHandle ev+ case ev of - Normal ->- case ev of- VtyEvent (EvKey (KChar 'q') []) -> halt- -- EvKey (KChar 'l') [MCtrl] -> do- VtyEvent (EvKey KEsc []) -> put' $ resetScreens d ui- VtyEvent (EvKey (KChar c) []) | c == '?' -> put' $ setMode Help ui- -- XXX AppEvents currently handled only in Normal mode- -- XXX be sure we don't leave unconsumed events piling up- AppEvent (DateChange old _) | isStandardPeriod p && p `periodContainsDate` old ->- 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'- VtyEvent (EvKey (KChar 'I') []) -> put' $ uiCheckBalanceAssertions d (toggleIgnoreBalanceAssertions ui)- VtyEvent (EvKey (KChar 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add 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 'B') []) -> put' $ regenerateScreens j d $ toggleConversionOp ui- VtyEvent (EvKey (KChar 'V') []) -> put' $ regenerateScreens j d $ toggleValue ui- VtyEvent (EvKey (KChar '0') []) -> put' $ regenerateScreens j d $ setDepth (Just 0) ui- VtyEvent (EvKey (KChar '1') []) -> put' $ regenerateScreens j d $ setDepth (Just 1) ui- VtyEvent (EvKey (KChar '2') []) -> put' $ regenerateScreens j d $ setDepth (Just 2) ui- VtyEvent (EvKey (KChar '3') []) -> put' $ regenerateScreens j d $ setDepth (Just 3) ui- VtyEvent (EvKey (KChar '4') []) -> put' $ regenerateScreens j d $ setDepth (Just 4) ui- VtyEvent (EvKey (KChar '5') []) -> put' $ regenerateScreens j d $ setDepth (Just 5) ui- VtyEvent (EvKey (KChar '6') []) -> put' $ regenerateScreens j d $ setDepth (Just 6) ui- VtyEvent (EvKey (KChar '7') []) -> put' $ regenerateScreens j d $ setDepth (Just 7) ui- VtyEvent (EvKey (KChar '8') []) -> put' $ regenerateScreens j d $ setDepth (Just 8) ui- VtyEvent (EvKey (KChar '9') []) -> put' $ regenerateScreens j d $ setDepth (Just 9) ui- VtyEvent (EvKey (KChar '-') []) -> put' $ regenerateScreens j d $ decDepth ui- VtyEvent (EvKey (KChar '_') []) -> put' $ regenerateScreens j d $ decDepth ui- VtyEvent (EvKey (KChar c) []) | c `elem` ['+','='] -> put' $ regenerateScreens j d $ incDepth ui- VtyEvent (EvKey (KChar 'T') []) -> put' $ regenerateScreens j d $ setReportPeriod (DayPeriod d) ui+ VtyEvent (EvKey (KChar 'q') []) -> halt -- q: quit+ VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui -- C-z: suspend+ VtyEvent (EvKey (KChar 'l') [MCtrl]) -> centerSelection >> redraw -- C-l: redraw+ VtyEvent (EvKey KEsc []) -> modify' (resetScreens d) -- ESC: reset+ VtyEvent (EvKey (KChar c) []) | c == '?' -> modify' (setMode Help) -- ?: enter help mode - -- display mode/query toggles- VtyEvent (EvKey (KChar 'H') []) -> modify' (regenerateScreens j d . toggleHistorical) >> asCenterAndContinue- VtyEvent (EvKey (KChar 't') []) -> modify' (regenerateScreens j d . toggleTree) >> asCenterAndContinue- VtyEvent (EvKey (KChar c) []) | c `elem` ['z','Z'] -> modify' (regenerateScreens j d . toggleEmpty) >> asCenterAndContinue- VtyEvent (EvKey (KChar 'R') []) -> modify' (regenerateScreens j d . toggleReal) >> asCenterAndContinue- VtyEvent (EvKey (KChar 'U') []) -> modify' (regenerateScreens j d . toggleUnmarked) >> asCenterAndContinue- VtyEvent (EvKey (KChar 'P') []) -> modify' (regenerateScreens j d . togglePending) >> asCenterAndContinue- VtyEvent (EvKey (KChar 'C') []) -> modify' (regenerateScreens j d . toggleCleared) >> asCenterAndContinue- VtyEvent (EvKey (KChar 'F') []) -> modify' (regenerateScreens j d . toggleForecast d)+ -- AppEvents come from the system, in --watch mode.+ -- XXX currently they are handled only in Normal mode+ -- 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' - VtyEvent (EvKey (KDown) [MShift]) -> put' $ regenerateScreens j d $ shrinkReportPeriod d ui- VtyEvent (EvKey (KUp) [MShift]) -> put' $ regenerateScreens j d $ growReportPeriod d ui- VtyEvent (EvKey (KRight) [MShift]) -> put' $ regenerateScreens j d $ nextReportPeriod journalspan ui- VtyEvent (EvKey (KLeft) [MShift]) -> put' $ regenerateScreens j d $ previousReportPeriod journalspan ui- VtyEvent (EvKey (KChar '/') []) -> put' $ regenerateScreens j d $ showMinibuffer "filter" Nothing ui- VtyEvent (EvKey k []) | k `elem` [KBS, KDel] -> (put' $ regenerateScreens j d $ resetFilter ui)- VtyEvent e | e `elem` moveLeftEvents -> put' $ popScreen ui- VtyEvent (EvKey (KChar 'l') [MCtrl]) -> scrollSelectionToMiddle _asList >> redraw- VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui+ -- 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.+ -- (Eg: when watching data for "today" and the time has just passed midnight.)+ AppEvent (DateChange old _) | isStandardPeriod p && p `periodContainsDate` old ->+ modify' (setReportPeriod (DayPeriod d) >>> regenerateScreens j d)+ where p = reportPeriod ui - -- enter register screen for selected account (if there is one),- -- centering its selected transaction if possible- VtyEvent e | e `elem` moveRightEvents- , not $ isBlankElement $ listSelectedElement _asList -> asEnterRegister d selacct ui+ -- set or reset a filter:+ VtyEvent (EvKey (KChar '/') []) -> modify' (showMinibuffer "filter" Nothing >>> regenerateScreens j d)+ VtyEvent (EvKey k []) | k `elem` [KBS, KDel] -> modify' (resetFilter >>> regenerateScreens j d) - -- MouseDown is sometimes duplicated, https://github.com/jtdaugherty/brick/issues/347- -- just use it to move the selection- MouseDown _n BLeft _mods Location{loc=(_x,y)} | not $ (=="") clickedacct -> do- put' ui{aScreen=scr} -- XXX does this do anything ?- where clickedacct = maybe "" asItemAccountName $ listElements _asList !? y- -- and on MouseUp, enter the subscreen- MouseUp _n (Just BLeft) Location{loc=(_x,y)} | not $ (=="") clickedacct -> do- asEnterRegister d clickedacct ui- where clickedacct = maybe "" asItemAccountName $ listElements _asList !? y+ -- run external programs:+ VtyEvent (EvKey (KChar 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add 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 - -- when selection is at the last item, DOWN scrolls instead of moving, until maximally scrolled- VtyEvent e | e `elem` moveDownEvents, isBlankElement mnextelement -> do- vScrollBy (viewportScroll $ _asList^.listNameL) 1- where mnextelement = listSelectedElement $ listMoveDown _asList+ -- adjust the period displayed:+ VtyEvent (EvKey (KChar 'T') []) -> modify' (setReportPeriod (DayPeriod d) >>> regenerateScreens j d)+ VtyEvent (EvKey (KDown) [MShift]) -> modify' (shrinkReportPeriod d >>> regenerateScreens j d)+ VtyEvent (EvKey (KUp) [MShift]) -> modify' (growReportPeriod d >>> regenerateScreens j d)+ VtyEvent (EvKey (KRight) [MShift]) -> modify' (nextReportPeriod journalspan >>> regenerateScreens j d)+ VtyEvent (EvKey (KLeft) [MShift]) -> modify' (previousReportPeriod journalspan >>> regenerateScreens j d) - -- mouse scroll wheel scrolls the viewport up or down to its maximum extent,- -- pushing the selection when necessary.- MouseDown name btn _mods _loc | btn `elem` [BScrollUp, BScrollDown] -> do- let scrollamt = if btn==BScrollUp then -1 else 1- list' <- nestEventM' _asList $ listScrollPushingSelection name (asListSize _asList) scrollamt- put' ui{aScreen=scr{_asList=list'}}+ -- various toggles and settings:+ VtyEvent (EvKey (KChar 'I') []) -> modify' (toggleIgnoreBalanceAssertions >>> uiCheckBalanceAssertions d)+ VtyEvent (EvKey (KChar 'F') []) -> modify' (toggleForecast d >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar 'B') []) -> modify' (toggleConversionOp >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar 'V') []) -> modify' (toggleValue >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar '0') []) -> modify' (setDepth (Just 0) >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar '1') []) -> modify' (setDepth (Just 1) >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar '2') []) -> modify' (setDepth (Just 2) >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar '3') []) -> modify' (setDepth (Just 3) >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar '4') []) -> modify' (setDepth (Just 4) >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar '5') []) -> modify' (setDepth (Just 5) >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar '6') []) -> modify' (setDepth (Just 6) >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar '7') []) -> modify' (setDepth (Just 7) >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar '8') []) -> modify' (setDepth (Just 8) >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar '9') []) -> modify' (setDepth (Just 9) >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar c) []) | c `elem` ['-','_'] -> modify' (decDepth >>> regenerateScreens j d)+ VtyEvent (EvKey (KChar c) []) | c `elem` ['+','='] -> modify' (incDepth >>> regenerateScreens j d)+ -- toggles after which the selection should be recentered:+ VtyEvent (EvKey (KChar 'H') []) -> modify' (toggleHistorical >>> regenerateScreens j d) >> centerSelection -- harmless on BS/IS screens+ VtyEvent (EvKey (KChar 't') []) -> modify' (toggleTree >>> regenerateScreens j d) >> centerSelection+ VtyEvent (EvKey (KChar 'R') []) -> modify' (toggleReal >>> regenerateScreens j d) >> centerSelection+ VtyEvent (EvKey (KChar 'U') []) -> modify' (toggleUnmarked >>> regenerateScreens j d) >> centerSelection+ VtyEvent (EvKey (KChar 'P') []) -> modify' (togglePending >>> regenerateScreens j d) >> centerSelection+ VtyEvent (EvKey (KChar 'C') []) -> modify' (toggleCleared >>> regenerateScreens j d) >> centerSelection+ VtyEvent (EvKey (KChar c) []) | c `elem` ['z','Z'] -> modify' (toggleEmpty >>> regenerateScreens j d) >> centerSelection -- back compat: accept Z as well as z - -- if page down or end leads to a blank padding item, stop at last non-blank- VtyEvent e@(EvKey k []) | k `elem` [KPageDown, KEnd] -> do- l <- nestEventM' _asList $ handleListEvent e- if isBlankElement $ listSelectedElement l- then do- let l' = listMoveTo lastnonblankidx l- scrollSelectionToMiddle l'- put' ui{aScreen=scr{_asList=l'}}- else- put' ui{aScreen=scr{_asList=l}}+ -- LEFT key or a click in the app's left margin: exit to the parent screen.+ VtyEvent e | e `elem` moveLeftEvents -> modify' popScreen+ VtyEvent (EvMouseUp 0 _ (Just BLeft)) -> modify' popScreen -- this mouse click is a VtyEvent since not in a clickable widget - -- fall through to the list's event handler (handles up/down)- VtyEvent e -> do- list' <- nestEventM' _asList $ handleListEvent (normaliseMovementKeys e)- put' ui{aScreen=scr & asList .~ list' & asSelectedAccount .~ selacct }+ -- RIGHT key or MouseUp on an account: enter the register screen for the selected account+ VtyEvent e | e `elem` moveRightEvents, not $ isBlankItem $ listSelectedElement l -> enterRegisterScreen d selacct ui+ MouseUp _n (Just BLeft) Location{loc=(_,y)} | Just clkacct <- clickedAcctAt y -> enterRegisterScreen d clkacct ui - MouseDown{} -> return ()- MouseUp{} -> return ()- AppEvent _ -> return ()+ -- MouseDown: this is not debounced and can repeat (https://github.com/jtdaugherty/brick/issues/347)+ -- so we only let it do something harmless: move the selection.+ MouseDown _n BLeft _mods Location{loc=(_,y)} | not $ isBlankItem clickeditem ->+ put' ui{aScreen=scons ass'}+ where+ clickeditem = (0,) <$> listElements l !? y+ ass' = ass{_assList=listMoveTo y l} - _ -> dlogUiTraceM "asHandle 2" >> errorWrongScreenType "event handler"+ -- Mouse scroll wheel: scroll up or down to the maximum extent, pushing the selection when necessary.+ MouseDown name btn _mods _loc | btn `elem` [BScrollUp, BScrollDown] -> do+ let scrollamt = if btn==BScrollUp then -1 else 1+ l' <- nestEventM' l $ listScrollPushingSelection name (asListSize l) scrollamt+ put' ui{aScreen=scons ass{_assList=l'}} -asEnterRegister :: Day -> AccountName -> UIState -> EventM Name UIState ()-asEnterRegister d selacct ui = do- dlogUiTraceM "asEnterRegister"+ -- PGDOWN/END keys: handle with List's default handler, but restrict the selection to stop+ -- (and center) at the last non-blank item.+ VtyEvent e@(EvKey k []) | k `elem` [KPageDown, KEnd] -> do+ l1 <- nestEventM' l $ handleListEvent e+ if isBlankItem $ listSelectedElement l1+ then do+ let l2 = listMoveTo lastnonblankidx l1+ scrollSelectionToMiddle l2+ put' ui{aScreen=scons ass{_assList=l2}}+ else+ put' ui{aScreen=scons ass{_assList=l1}}++ -- DOWN key when selection is at the last item: scroll instead of moving, until maximally scrolled+ VtyEvent e | e `elem` moveDownEvents, isBlankItem mnextelement -> vScrollBy (viewportScroll $ l^.listNameL) 1+ where mnextelement = listSelectedElement $ listMoveDown l++ -- Any other vty event (UP, DOWN, PGUP etc): handle with List's default handler.+ VtyEvent e -> do+ l' <- nestEventM' l $ handleListEvent (normaliseMovementKeys e)+ put' ui{aScreen=scons $ ass & assList .~ l' & assSelectedAccount .~ selacct}++ -- Any other mouse/app event: ignore+ MouseDown{} -> return ()+ MouseUp{} -> return ()+ AppEvent _ -> return ()++-- | Handle events when in minibuffer mode on any screen.+handleMinibufferMode ed ev = do+ ui@UIState{ajournal=j} <- get'+ d <- liftIO getCurrentDay+ case ev of+ VtyEvent (EvKey KEsc []) -> put' $ closeMinibuffer ui+ VtyEvent (EvKey KEnter []) -> put' $ regenerateScreens j d ui'+ where+ ui' = setFilter s (closeMinibuffer ui)+ & fromRight (showMinibuffer "Cannot compile regular expression" (Just s) ui)+ where s = chomp $ unlines $ map strip $ getEditContents ed+ VtyEvent (EvKey (KChar 'l') [MCtrl]) -> redraw+ VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui+ VtyEvent e -> do+ ed' <- nestEventM' ed $ handleEditorEvent (VtyEvent e)+ put' ui{aMode=Minibuffer "filter" ed'}+ AppEvent _ -> return ()+ MouseDown{} -> return ()+ MouseUp{} -> return ()++-- | Handle events when in help mode on any screen.+handleHelpMode ev = do+ ui <- get'+ case ev of+ -- VtyEvent (EvKey (KChar 'q') []) -> halt+ VtyEvent (EvKey (KChar 'l') [MCtrl]) -> redraw+ VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui+ _ -> helpHandle ev++enterRegisterScreen :: Day -> AccountName -> UIState -> EventM Name UIState ()+enterRegisterScreen d acct ui@UIState{ajournal=j, aopts=uopts} = do+ dbguiEv "enterRegisterScreen" let- regscr = rsSetAccount selacct isdepthclipped registerScreen+ regscr = rsNew uopts d j acct isdepthclipped where isdepthclipped = case getDepth ui of- Just de -> accountNameLevel selacct >= de+ Just de -> accountNameLevel acct >= de Nothing -> False- rsCenterSelection (screenEnter d regscr ui) >>= put'+ ui1 = pushScreen regscr ui+ rsCenterSelection ui1 >>= put' -asSetSelectedAccount a s@AccountsScreen{} = s & asSelectedAccount .~ a-asSetSelectedAccount _ s = s+-- | From any accounts screen's state, get the account name from the +-- currently selected list item, or otherwise the last known selected account name.+asSelectedAccount :: AccountsScreenState -> AccountName+asSelectedAccount ass =+ case listSelectedElement $ _assList ass of+ Just (_, AccountsScreenItem{..}) -> asItemAccountName+ Nothing -> ass ^. assSelectedAccount -isBlankElement mel = ((asItemAccountName . snd) <$> mel) == Just ""+-- | Set the selected account on any of the accounts screens. Has no effect on other screens.+-- Sets the high-level property _assSelectedAccount and also selects the corresponding or+-- best alternative item in the list widget (_assList).+asSetSelectedAccount :: AccountName -> Screen -> Screen+asSetSelectedAccount acct scr =+ case scr of+ (AS ass) -> AS $ assSetSelectedAccount acct ass+ (BS ass) -> BS $ assSetSelectedAccount acct ass+ (IS ass) -> IS $ assSetSelectedAccount acct ass+ _ -> scr+ where+ assSetSelectedAccount a ass@ASS{_assList=l} =+ ass{_assSelectedAccount=a, _assList=listMoveTo selidx l}+ where+ -- which list item should be selected ?+ selidx = headDef 0 $ catMaybes [+ elemIndex a as -- the specified account, 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+ as = map asItemAccountName $ V.toList $ listElements l -asCenterAndContinue :: EventM Name UIState ()-asCenterAndContinue = do- ui <- get'- scrollSelectionToMiddle (_asList $ aScreen ui)+isBlankItem mitem = ((asItemAccountName . snd) <$> mitem) == Just "" asListSize = V.length . V.takeWhile ((/="").asItemAccountName) . listElements++
+ Hledger/UI/BalancesheetScreen.hs view
@@ -0,0 +1,29 @@+-- The balance sheet screen, like the accounts screen but restricted to balance sheet accounts.++module Hledger.UI.BalancesheetScreen+ (bsNew+ ,bsUpdate+ ,bsDraw+ ,bsHandle+ )+where++import Brick hiding (bsDraw)++import Hledger+import Hledger.Cli hiding (mode, progname, prognameandversion)+import Hledger.UI.UIOptions+import Hledger.UI.UITypes+import Hledger.UI.UIUtils+import Hledger.UI.UIScreens+import Hledger.UI.AccountsScreen (asHandle, asDrawHelper)+++bsDraw :: UIState -> [Widget Name]+bsDraw ui = dbgui "bsDraw" $ asDrawHelper ui ropts' scrname+ where+ scrname = "balance sheet balances"+ ropts' = (_rsReportOpts $ reportspec_ $ uoCliOpts $ aopts ui){balanceaccum_=Historical}++bsHandle :: BrickEvent Name AppEvent -> EventM Name UIState ()+bsHandle = asHandle . dbgui "bsHandle"
Hledger/UI/ErrorScreen.hs view
@@ -6,7 +6,10 @@ {-# OPTIONS_GHC -Wno-incomplete-record-updates #-} module Hledger.UI.ErrorScreen- (errorScreen+ (esNew+ ,esUpdate+ ,esDraw+ ,esHandle ,uiCheckBalanceAssertions ,uiReloadJournal ,uiReloadJournalIfChanged@@ -29,32 +32,19 @@ import Hledger.UI.UITypes import Hledger.UI.UIState import Hledger.UI.UIUtils+import Hledger.UI.UIScreens 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{..}+esDraw UIState{aScreen=ES ESS{..} ,aMode=mode } = case mode of- Help -> [helpDialog copts, maincontent]- -- Minibuffer e -> [minibuffer e, maincontent]+ Help -> [helpDialog, maincontent] _ -> [maincontent] where maincontent = Widget Greedy Greedy $ do- render $ defaultLayout toplabel bottomlabel $ withAttr (attrName "error") $ str $ esError+ render $ defaultLayout toplabel bottomlabel $ withAttr (attrName "error") $ str $ _essError where toplabel = withAttr (attrName "border" <> attrName "bold") (str "Oops. Please fix this problem then press g to reload")@@ -79,7 +69,7 @@ esHandle ev = do ui0 <- get' case ui0 of- ui@UIState{aScreen=ErrorScreen{..}+ ui@UIState{aScreen=ES ESS{..} ,aopts=UIOpts{uoCliOpts=copts} ,ajournal=j ,aMode=mode@@ -93,14 +83,14 @@ _ -> helpHandle ev _ -> do- let d = copts^.rsDay+ d <- liftIO getCurrentDay 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+ (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] ->@@ -159,12 +149,15 @@ ej <- let copts' = enableForecastPreservingPeriod ui copts in runExceptT $ journalReload copts'+ -- dbg1IO "uiReloadJournal before reload" (map tdescription $ jtxns $ ajournal ui) return $ case ej of- Right j -> regenerateScreens j d ui+ Right j ->+ -- dbg1 "uiReloadJournal after reload" (map tdescription $ jtxns 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+ UIState{aScreen=ES _} -> ui{aScreen=esNew err}+ _ -> pushScreen (esNew 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@@ -183,20 +176,20 @@ 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+ Left err -> case aScreen ui of+ ES _ -> ui{aScreen=esNew err}+ _ -> pushScreen (esNew 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}+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+ UIState{aScreen=ES sst} -> ui{aScreen=ES sst{_essError=err}}+ _ -> pushScreen (esNew err) ui
+ Hledger/UI/IncomestatementScreen.hs view
@@ -0,0 +1,29 @@+-- The income statement accounts screen, like the accounts screen but restricted to income statement accounts.++module Hledger.UI.IncomestatementScreen+ (isNew+ ,isUpdate+ ,isDraw+ ,isHandle+ )+where++import Brick++import Hledger+import Hledger.Cli hiding (mode, progname, prognameandversion)+import Hledger.UI.UIOptions+import Hledger.UI.UITypes+import Hledger.UI.UIUtils+import Hledger.UI.UIScreens+import Hledger.UI.AccountsScreen (asHandle, asDrawHelper)+++isDraw :: UIState -> [Widget Name]+isDraw ui = dbgui "isDraw" $ asDrawHelper ui ropts' scrname+ where+ scrname = "income statement changes"+ ropts' = (_rsReportOpts $ reportspec_ $ uoCliOpts $ aopts ui){balanceaccum_=PerPeriod}++isHandle :: BrickEvent Name AppEvent -> EventM Name UIState ()+isHandle = asHandle . dbgui "isHandle"
Hledger/UI/Main.hs view
@@ -1,13 +1,12 @@--- TODO: brick 1 support--- https://hackage.haskell.org/package/brick-1.0/changelog {-|-hledger-ui - a hledger add-on providing a curses-style interface.+hledger-ui - a hledger add-on providing an efficient TUI. Copyright (c) 2007-2015 Simon Michael <simon@joyful.com> Released under GPL version 3 or later. -} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MultiWayIf #-} module Hledger.UI.Main where @@ -15,28 +14,38 @@ import Control.Concurrent (threadDelay) import Control.Concurrent.Async (withAsync) import Control.Monad (forM_, void, when)+import Data.Bifunctor (first)+import Data.Function ((&)) import Data.List (find) import Data.List.Extra (nubSort)+import qualified Data.Map as M (elems) import Data.Maybe (fromMaybe) import qualified Data.Text as T import Graphics.Vty (mkVty, Mode (Mouse), Vty (outputIface), Output (setMode)) import Lens.Micro ((^.)) import System.Directory (canonicalizePath)+import System.Environment (withProgName) import System.FilePath (takeDirectory)-import System.FSNotify (Event(Modified), isPollingManager, watchDir, withManager)-import Brick-+import System.FSNotify (Event(Modified), watchDir, withManager, EventIsDirectory (IsFile))+import Brick hiding (bsDraw) import qualified Brick.BChan as BC import Hledger import Hledger.Cli hiding (progname,prognameandversion)+import Hledger.UI.Theme import Hledger.UI.UIOptions import Hledger.UI.UITypes-import Hledger.UI.Theme+import Hledger.UI.UIState (uiState, getDepth)+import Hledger.UI.UIUtils (dbguiEv, showScreenStack, showScreenSelection)+import Hledger.UI.MenuScreen import Hledger.UI.AccountsScreen+import Hledger.UI.BalancesheetScreen+import Hledger.UI.IncomestatementScreen import Hledger.UI.RegisterScreen-import Hledger.UI.UIUtils (dlogUiTrace)+import Hledger.UI.TransactionScreen+import Hledger.UI.ErrorScreen + ---------------------------------------------------------------------- newChan :: IO (BC.BChan a)@@ -47,7 +56,11 @@ main :: IO ()-main = do+main = withProgName "hledger-ui.log" $ do -- force Hledger.Utils.Debug.* to log to hledger-ui.log+ traceLogAtIO 1 "\n\n\n\n==== hledger-ui start"+ dbg1IO "args" progArgs+ dbg1IO "debugLevel" debugLevel+ opts@UIOpts{uoCliOpts=copts@CliOpts{inputopts_=iopts,rawopts_=rawopts}} <- getHledgerUIOpts -- when (debug_ $ cliopts_ opts) $ printf "%s\n" prognameandversion >> printf "opts: %s\n" (show opts) @@ -63,8 +76,8 @@ _ -> withJournalDo copts' (runBrickUi opts) runBrickUi :: UIOpts -> Journal -> IO ()-runBrickUi uopts@UIOpts{uoCliOpts=copts@CliOpts{inputopts_=_iopts,reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}} j =- dlogUiTrace "========= runBrickUi" $ do+runBrickUi uopts0@UIOpts{uoCliOpts=copts@CliOpts{inputopts_=_iopts,reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}} j =+ do let today = copts^.rsDay @@ -105,15 +118,16 @@ -- There is also a freeform text area for extra query terms (/ key). -- It's cleaner and less conflicting to keep the former out of the latter. - uopts' = uopts{+ uopts = uopts0{ uoCliOpts=copts{ reportspec_=rspec{ _rsQuery=filteredQuery $ _rsQuery rspec, -- query with depth/date parts removed _rsReportOpts=ropts{- depth_ =queryDepth $ _rsQuery rspec, -- query's depth part- period_=periodfromoptsandargs, -- query's date part- no_elide_=True, -- avoid squashing boring account names, for a more regular tree (unlike hledger)- empty_=not $ empty_ ropts -- show zero items by default, hide them with -E (unlike hledger)+ depth_ = queryDepth $ _rsQuery rspec, -- query's depth part+ period_ = periodfromoptsandargs, -- query's date part+ no_elide_ = True, -- avoid squashing boring account names, for a more regular tree (unlike hledger)+ empty_ = not $ empty_ ropts, -- show zero items by default, hide them with -E (unlike hledger)+ declared_ = True -- always show declared accounts even if unused } } }@@ -125,51 +139,76 @@ filteredQuery q = simplifyQuery $ And [queryFromFlags ropts, filtered q] where filtered = filterQuery (\x -> not $ queryIsDepth x || queryIsDate x) - (scr, prevscrs) = case uoRegister uopts' of- Nothing -> (accountsScreen, [])- -- with --register, start on the register screen, and also put- -- the accounts screen on the prev screens stack so you can exit- -- to that as usual.- Just apat -> (rsSetAccount acct False registerScreen, [ascr'])- where- acct = fromMaybe (error' $ "--register "++apat++" did not match any account") -- PARTIAL:- . firstMatch $ journalAccountNamesDeclaredOrImplied j- firstMatch = case toRegexCI $ T.pack apat of- Right re -> find (regexMatchText re)- Left _ -> const Nothing- -- Initialising the accounts screen is awkward, requiring- -- another temporary UIState value..- ascr' = aScreen $- asInit today True- UIState{- astartupopts=uopts'- ,aopts=uopts'- ,ajournal=j- ,aScreen=asSetSelectedAccount acct accountsScreen- ,aPrevScreens=[]- ,aMode=Normal- }+ -- Choose the initial screen to display.+ -- We like to show the balance sheet accounts screen by default,+ -- but that can change eg if we can't detect any accounts for it,+ -- or if an account query has been provided at startup,+ -- or if a specific screen has been requested by command line flag.+ -- Whichever is the initial screen, we also set up a stack of previous screens,+ -- as if you had navigated down to it from the top.+ -- (remember, the previous screens list is ordered nearest/lowest first)+ rawopts = rawopts_ $ uoCliOpts $ uopts+ (prevscrs, currscr) =+ dbg1With (showScreenStack "initial" showScreenSelection . uncurry2 (uiState defuiopts nulljournal)) $+ if+ | boolopt "menu" rawopts -> ([], menuscr)+ | boolopt "all" rawopts -> ([msSetSelectedScreen 0 menuscr], allacctsscr)+ | boolopt "bs" rawopts -> ([menuscr], bsacctsscr)+ | boolopt "is" rawopts -> ([msSetSelectedScreen 2 menuscr], isacctsscr) - ui =- (sInit scr) today True $- UIState{- astartupopts=uopts'- ,aopts=uopts'- ,ajournal=j- ,aScreen=scr- ,aPrevScreens=prevscrs- ,aMode=Normal- }+ -- With --register=ACCT, the initial screen stack is:+ -- menu screen, with ACCTSSCR selected+ -- ACCTSSCR (the accounts screen containing ACCT), with ACCT selected+ -- register screen for ACCT+ | Just apat <- uoRegister uopts ->+ let+ -- the account being requested+ acct = fromMaybe (error' $ "--register "++apat++" did not match any account") -- PARTIAL:+ . firstMatch $ journalAccountNamesDeclaredOrImplied j+ where+ firstMatch = case toRegexCI $ T.pack apat of+ Right re -> find (regexMatchText re)+ Left _ -> const Nothing - brickapp :: App UIState AppEvent Name- brickapp = App {- appStartEvent = return ()- , appAttrMap = const $ fromMaybe defaultTheme $ getTheme =<< uoTheme uopts'- , appChooseCursor = showFirstCursor- , appHandleEvent = \ev -> do ui' <- get; sHandle (aScreen ui') ev- , appDraw = \ui' -> sDraw (aScreen ui') ui'- }+ -- the register screen for acct+ regscr = + rsSetAccount acct False $+ rsNew uopts today j acct forceinclusive+ where+ forceinclusive = case getDepth ui of+ Just de -> accountNameLevel acct >= de+ Nothing -> False + -- The accounts screen containing acct.+ -- Keep these selidx values synced with the menu items in msNew.+ (acctsscr, selidx) =+ case journalAccountType j acct of+ Just t | isBalanceSheetAccountType t -> (bsacctsscr, 1)+ Just t | isIncomeStatementAccountType t -> (isacctsscr, 2)+ _ -> (allacctsscr,0)+ & first (asSetSelectedAccount acct)++ -- the menu screen+ menuscr' = msSetSelectedScreen selidx menuscr+ in ([acctsscr, menuscr'], regscr)++ -- No balance sheet accounts detected, or an initial account query specified:+ | not hasbsaccts || hasacctquery -> ([msSetSelectedScreen 0 menuscr], allacctsscr)++ | otherwise -> ([menuscr], bsacctsscr)++ where+ hasbsaccts = any (`elem` accttypes) [Asset, Liability, Equity]+ where accttypes = M.elems $ jaccounttypes j+ hasacctquery = matchesQuery queryIsAcct $ _rsQuery rspec+ menuscr = msNew+ allacctsscr = asNew uopts today j Nothing+ bsacctsscr = bsNew uopts today j Nothing+ isacctsscr = isNew uopts today j Nothing++ ui = uiState uopts j prevscrs currscr+ app = brickApp (uoTheme uopts)+ -- print (length (show ui)) >> exitSuccess -- show any debug output to this point & quit let @@ -179,10 +218,10 @@ setMode (outputIface v) Mouse True return v - if not (uoWatch uopts')+ if not (uoWatch uopts) then do vty <- makevty- void $ customMain vty makevty Nothing brickapp ui+ void $ customMain vty makevty Nothing app ui else do -- a channel for sending misc. events to the app@@ -214,7 +253,6 @@ -- with Debounce at the default 1ms it clears transient errors itself -- but gets tied up for ages withManager $ \mgr -> do- dbg1IO "fsnotify using polling ?" $ isPollingManager mgr files <- mapM (canonicalizePath . fst) $ jfiles j let directories = nubSort $ map takeDirectory files dbg1IO "files" files@@ -225,7 +263,7 @@ d -- predicate: ignore changes not involving our files (\case- Modified f _ False -> f `elem` files+ Modified f _ IsFile -> f `elem` files -- Added f _ -> f `elem` files -- Removed f _ -> f `elem` files -- we don't handle adding/removing journal files right now@@ -242,4 +280,37 @@ -- and start the app. Must be inside the withManager block. (XXX makevty too ?) vty <- makevty- void $ customMain vty makevty (Just eventChan) brickapp ui+ void $ customMain vty makevty (Just eventChan) app ui++brickApp :: Maybe String -> App UIState AppEvent Name+brickApp mtheme = App {+ appStartEvent = return ()+ , appAttrMap = const $ fromMaybe defaultTheme $ getTheme =<< mtheme+ , appChooseCursor = showFirstCursor+ , appHandleEvent = uiHandle+ , appDraw = uiDraw+ }++uiHandle :: BrickEvent Name AppEvent -> EventM Name UIState ()+uiHandle ev = do+ dbguiEv $ "\n==== " ++ show ev+ ui <- get+ case aScreen ui of+ MS _ -> msHandle ev+ AS _ -> asHandle ev+ BS _ -> bsHandle ev+ IS _ -> isHandle ev+ RS _ -> rsHandle ev+ TS _ -> tsHandle ev+ ES _ -> esHandle ev++uiDraw :: UIState -> [Widget Name]+uiDraw ui =+ case aScreen ui of+ MS _ -> msDraw ui+ AS _ -> asDraw ui+ BS _ -> bsDraw ui+ IS _ -> isDraw ui+ RS _ -> rsDraw ui+ TS _ -> tsDraw ui+ ES _ -> esDraw ui
+ Hledger/UI/MenuScreen.hs view
@@ -0,0 +1,273 @@+-- The menu screen, showing other screens available in hledger-ui.++{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Hledger.UI.MenuScreen+ (msNew+ ,msUpdate+ ,msDraw+ ,msHandle+ ,msSetSelectedScreen+ )+where++import Brick+import Brick.Widgets.List+import Control.Monad+import Control.Monad.IO.Class (liftIO)+import Data.Maybe+import qualified Data.Text as T+import Data.Time.Calendar (Day)+import qualified Data.Vector as V+import Data.Vector ((!?))+import Graphics.Vty (Event(..),Key(..),Modifier(..), Button (BLeft, BScrollDown, BScrollUp))+import Lens.Micro.Platform+import System.Console.ANSI+import System.FilePath (takeFileName)++import Hledger+import Hledger.Cli hiding (mode, progname, prognameandversion)+import Hledger.UI.UIOptions+import Hledger.UI.UITypes+import Hledger.UI.UIState+import Hledger.UI.UIUtils+import Hledger.UI.UIScreens+import Hledger.UI.ErrorScreen (uiReloadJournal, uiCheckBalanceAssertions, uiReloadJournalIfChanged)+import Hledger.UI.Editor (runIadd, runEditor, endPosition)+import Brick.Widgets.Edit (getEditContents, handleEditorEvent)+++msDraw :: UIState -> [Widget Name]+msDraw UIState{aopts=_uopts@UIOpts{uoCliOpts=copts@CliOpts{reportspec_=_rspec}}+ ,ajournal=j+ ,aScreen=MS sst+ ,aMode=mode+ } = dbgui "msDraw" $+ case mode of+ Help -> [helpDialog, maincontent]+ _ -> [maincontent]+ where+ maincontent = Widget Greedy Greedy $ do+ render $ defaultLayout toplabel bottomlabel $ renderList msDrawItem True (sst ^. mssList)+ where+ toplabel =+ withAttr (attrName "border" <> attrName "filename") files+ <+> (if ignore_assertions_ . balancingopts_ $ inputopts_ copts+ then withAttr (attrName "border" <> attrName "query") (str " ignoring balance assertions")+ else str "")+ where+ files = case journalFilePaths j of+ [] -> str ""+ f:_ -> str $ takeFileName f+ -- [f,_:[]] -> (withAttr ("border" <> "bold") $ str $ takeFileName f) <+> str " (& 1 included file)"+ -- f:fs -> (withAttr ("border" <> "bold") $ str $ takeFileName f) <+> str (" (& " ++ show (length fs) ++ " included files)")++ bottomlabel = case mode of+ Minibuffer label ed -> minibuffer label ed+ _ -> quickhelp+ where+ quickhelp = borderKeysStr' [+ ("?", str "help")+-- ,("RIGHT", str "register")+ -- ,("t", renderToggle (tree_ ropts) "list" "tree")+ -- ,("t", str "tree")+ -- ,("l", str "list")+ -- ,("-+", str "depth")+ -- ,("H", renderToggle (not ishistorical) "end-bals" "changes")+ -- ,("F", renderToggle1 (isJust . forecast_ $ inputopts_ copts) "forecast")+ --,("/", "filter")+ --,("DEL", "unfilter")+ --,("ESC", "cancel/top")+ ,("a", str "add")+-- ,("g", "reload")+ ,("q", str "quit")+ ]++msDraw _ = dbgui "msDraw" $ errorWrongScreenType "draw function" -- PARTIAL:++-- msDrawItem :: (Int,Int) -> Bool -> MenuScreenItem -> Widget Name+-- msDrawItem (_acctwidth, _balwidth) _selected MenuScreenItem{..} =+msDrawItem :: Bool -> MenuScreenItem -> Widget Name+msDrawItem _selected MenuScreenItem{..} =+ Widget Greedy Fixed $ do+ render $ txt msItemScreenName++-- XXX clean up like asHandle+msHandle :: BrickEvent Name AppEvent -> EventM Name UIState ()+msHandle ev = do+ ui0 <- get'+ dbguiEv "msHandle"+ case ui0 of+ ui@UIState{+ aopts=UIOpts{uoCliOpts=copts}+ ,ajournal=j+ ,aMode=mode+ ,aScreen=MS sst+ } -> do+ let+ -- save the currently selected account, in case we leave this screen and lose the selection+ mselscr = case listSelectedElement $ _mssList sst of+ Just (_, MenuScreenItem{..}) -> Just msItemScreen+ Nothing -> Nothing+ nonblanks = V.takeWhile (not . T.null . msItemScreenName) $ listElements $ _mssList sst+ lastnonblankidx = max 0 (length nonblanks - 1)+ d <- liftIO getCurrentDay++ case mode of+ Minibuffer _ ed ->+ case ev of+ VtyEvent (EvKey KEsc []) -> put' $ closeMinibuffer ui+ VtyEvent (EvKey KEnter []) -> put' $ regenerateScreens j d $+ case setFilter s $ closeMinibuffer ui of+ Left bad -> showMinibuffer "Cannot compile regular expression" (Just bad) ui+ Right ui' -> ui'+ where s = chomp $ unlines $ map strip $ getEditContents ed+ VtyEvent (EvKey (KChar 'l') [MCtrl]) -> redraw+ VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui+ VtyEvent e -> do+ ed' <- nestEventM' ed $ handleEditorEvent (VtyEvent e)+ put' ui{aMode=Minibuffer "filter" ed'}+ AppEvent _ -> return ()+ MouseDown{} -> return ()+ MouseUp{} -> return ()++ Help ->+ case ev of+ -- VtyEvent (EvKey (KChar 'q') []) -> halt+ VtyEvent (EvKey (KChar 'l') [MCtrl]) -> redraw+ VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui+ _ -> helpHandle ev++ Normal ->+ case ev of+ VtyEvent (EvKey (KChar 'q') []) -> halt+ -- EvKey (KChar 'l') [MCtrl] -> do+ VtyEvent (EvKey KEsc []) -> put' $ resetScreens d ui+ VtyEvent (EvKey (KChar c) []) | c == '?' -> put' $ setMode Help ui+ -- XXX AppEvents currently handled only in Normal mode+ -- XXX be sure we don't leave unconsumed events piling up+ AppEvent (DateChange old _) | isStandardPeriod p && p `periodContainsDate` old ->+ 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'+ VtyEvent (EvKey (KChar 'I') []) -> put' $ uiCheckBalanceAssertions d (toggleIgnoreBalanceAssertions ui)+ VtyEvent (EvKey (KChar 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add 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 'B') []) -> put' $ regenerateScreens j d $ toggleConversionOp ui+-- VtyEvent (EvKey (KChar 'V') []) -> put' $ regenerateScreens j d $ toggleValue ui+-- VtyEvent (EvKey (KChar '0') []) -> put' $ regenerateScreens j d $ setDepth (Just 0) ui+-- VtyEvent (EvKey (KChar '1') []) -> put' $ regenerateScreens j d $ setDepth (Just 1) ui+-- VtyEvent (EvKey (KChar '2') []) -> put' $ regenerateScreens j d $ setDepth (Just 2) ui+-- VtyEvent (EvKey (KChar '3') []) -> put' $ regenerateScreens j d $ setDepth (Just 3) ui+-- VtyEvent (EvKey (KChar '4') []) -> put' $ regenerateScreens j d $ setDepth (Just 4) ui+-- VtyEvent (EvKey (KChar '5') []) -> put' $ regenerateScreens j d $ setDepth (Just 5) ui+-- VtyEvent (EvKey (KChar '6') []) -> put' $ regenerateScreens j d $ setDepth (Just 6) ui+-- VtyEvent (EvKey (KChar '7') []) -> put' $ regenerateScreens j d $ setDepth (Just 7) ui+-- VtyEvent (EvKey (KChar '8') []) -> put' $ regenerateScreens j d $ setDepth (Just 8) ui+-- VtyEvent (EvKey (KChar '9') []) -> put' $ regenerateScreens j d $ setDepth (Just 9) ui+-- VtyEvent (EvKey (KChar '-') []) -> put' $ regenerateScreens j d $ decDepth ui+-- VtyEvent (EvKey (KChar '_') []) -> put' $ regenerateScreens j d $ decDepth ui+-- VtyEvent (EvKey (KChar c) []) | c `elem` ['+','='] -> put' $ regenerateScreens j d $ incDepth ui+-- VtyEvent (EvKey (KChar 'T') []) -> put' $ regenerateScreens j d $ setReportPeriod (DayPeriod d) ui++-- -- display mode/query toggles+-- VtyEvent (EvKey (KChar 'H') []) -> modify' (regenerateScreens j d . toggleHistorical) >> msCenterAndContinue+-- VtyEvent (EvKey (KChar 't') []) -> modify' (regenerateScreens j d . toggleTree) >> msCenterAndContinue+-- VtyEvent (EvKey (KChar c) []) | c `elem` ['z','Z'] -> modify' (regenerateScreens j d . toggleEmpty) >> msCenterAndContinue+-- VtyEvent (EvKey (KChar 'R') []) -> modify' (regenerateScreens j d . toggleReal) >> msCenterAndContinue+-- VtyEvent (EvKey (KChar 'U') []) -> modify' (regenerateScreens j d . toggleUnmarked) >> msCenterAndContinue+-- VtyEvent (EvKey (KChar 'P') []) -> modify' (regenerateScreens j d . togglePending) >> msCenterAndContinue+-- VtyEvent (EvKey (KChar 'C') []) -> modify' (regenerateScreens j d . toggleCleared) >> msCenterAndContinue+-- VtyEvent (EvKey (KChar 'F') []) -> modify' (regenerateScreens j d . toggleForecast d)++ -- VtyEvent (EvKey (KDown) [MShift]) -> put' $ regenerateScreens j d $ shrinkReportPeriod d ui+ -- VtyEvent (EvKey (KUp) [MShift]) -> put' $ regenerateScreens j d $ growReportPeriod d ui+ -- VtyEvent (EvKey (KRight) [MShift]) -> put' $ regenerateScreens j d $ nextReportPeriod journalspan ui+ -- VtyEvent (EvKey (KLeft) [MShift]) -> put' $ regenerateScreens j d $ previousReportPeriod journalspan ui+ VtyEvent (EvKey (KChar '/') []) -> put' $ regenerateScreens j d $ showMinibuffer "filter" Nothing ui+ VtyEvent (EvKey k []) | k `elem` [KBS, KDel] -> (put' $ regenerateScreens j d $ resetFilter ui)++ VtyEvent (EvKey (KChar 'l') [MCtrl]) -> scrollSelectionToMiddle (_mssList sst) >> redraw+ VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui++ -- RIGHT enters selected screen if there is one+ VtyEvent e | e `elem` moveRightEvents+ , not $ isBlankElement $ listSelectedElement (_mssList sst) ->+ msEnterScreen d (fromMaybe Accounts mselscr) ui++ -- MouseDown is sometimes duplicated, https://github.com/jtdaugherty/brick/issues/347+ -- just use it to move the selection+ MouseDown _n BLeft _mods Location{loc=(_x,y)} | not $ (=="") clickedname -> do+ put' ui{aScreen=MS sst} -- XXX does this do anything ?+ where+ item = listElements (_mssList sst) !? y+ clickedname = maybe "" msItemScreenName item+ -- mclickedscr = msItemScreen <$> item+ -- and on MouseUp, enter the subscreen+ MouseUp _n (Just BLeft) Location{loc=(_x,y)} -> -- | not $ (=="") clickedname ->+ case mclickedscr of+ Just scr -> msEnterScreen d scr ui+ Nothing -> return ()+ where+ item = listElements (_mssList sst) !? y+ -- clickedname = maybe "" msItemScreenName item+ mclickedscr = msItemScreen <$> item++ -- when selection is at the last item, DOWN scrolls instead of moving, until maximally scrolled+ VtyEvent e | e `elem` moveDownEvents, isBlankElement mnextelement -> do+ vScrollBy (viewportScroll $ (_mssList sst)^.listNameL) 1+ where mnextelement = listSelectedElement $ listMoveDown (_mssList sst)++ -- mouse scroll wheel scrolls the viewport up or down to its maximum extent,+ -- pushing the selection when necessary.+ MouseDown name btn _mods _loc | btn `elem` [BScrollUp, BScrollDown] -> do+ let scrollamt = if btn==BScrollUp then -1 else 1+ list' <- nestEventM' (_mssList sst) $ listScrollPushingSelection name (msListSize (_mssList sst)) scrollamt+ put' ui{aScreen=MS sst{_mssList=list'}}++ -- if page down or end leads to a blank padding item, stop at last non-blank+ VtyEvent e@(EvKey k []) | k `elem` [KPageDown, KEnd] -> do+ l <- nestEventM' (_mssList sst) $ handleListEvent e+ if isBlankElement $ listSelectedElement l+ then do+ let l' = listMoveTo lastnonblankidx l+ scrollSelectionToMiddle l'+ put' ui{aScreen=MS sst{_mssList=l'}}+ else+ put' ui{aScreen=MS sst{_mssList=l}}++ -- fall through to the list's event handler (handles up/down)+ VtyEvent e -> do+ list' <- nestEventM' (_mssList sst) $ handleListEvent (normaliseMovementKeys e)+ put' ui{aScreen=MS $ sst & mssList .~ list'}++ MouseDown{} -> return ()+ MouseUp{} -> return ()+ AppEvent _ -> return ()++ _ -> dbguiEv "msHandle" >> errorWrongScreenType "event handler"++msEnterScreen :: Day -> ScreenName -> UIState -> EventM Name UIState ()+msEnterScreen d scrname ui@UIState{ajournal=j, aopts=uopts} = do+ dbguiEv "msEnterScreen"+ let+ scr = case scrname of+ Accounts -> asNew uopts d j Nothing+ Balancesheet -> bsNew uopts d j Nothing+ Incomestatement -> isNew uopts d j Nothing+ put' $ pushScreen scr ui++-- | Set the selected list item on the menu screen. Has no effect on other screens.+msSetSelectedScreen :: Int -> Screen -> Screen+msSetSelectedScreen selidx (MS mss@MSS{_mssList=l}) = MS mss{_mssList=listMoveTo selidx l}+msSetSelectedScreen _ s = s++isBlankElement mel = ((msItemScreenName . snd) <$> mel) == Just ""++msListSize = V.length . V.takeWhile ((/="").msItemScreenName) . listElements+
Hledger/UI/RegisterScreen.hs view
@@ -7,149 +7,50 @@ {-# OPTIONS_GHC -Wno-incomplete-record-updates #-} module Hledger.UI.RegisterScreen- (registerScreen- ,rsHandle- ,rsSetAccount- ,rsCenterSelection- )+(rsNew+,rsUpdate+,rsDraw+,rsHandle+,rsSetAccount+,rsCenterSelection+) where import Control.Monad import Control.Monad.IO.Class (liftIO)+import Data.Bifunctor (bimap, Bifunctor (second)) import Data.List import Data.Maybe import qualified Data.Text as T-import Data.Time.Calendar import qualified Data.Vector as V+import Data.Vector ((!?)) import Graphics.Vty (Event(..),Key(..),Modifier(..), Button (BLeft, BScrollDown, BScrollUp)) import Brick import Brick.Widgets.List hiding (reverse) import Brick.Widgets.Edit import Lens.Micro.Platform-import Safe import System.Console.ANSI - import Hledger import Hledger.Cli hiding (mode, progname,prognameandversion) import Hledger.UI.UIOptions--- import Hledger.UI.Theme import Hledger.UI.UITypes import Hledger.UI.UIState import Hledger.UI.UIUtils+import Hledger.UI.UIScreens import Hledger.UI.Editor-import Hledger.UI.TransactionScreen-import Hledger.UI.ErrorScreen-import Data.Vector ((!?))--registerScreen :: Screen-registerScreen = RegisterScreen{- sInit = rsInit- ,sDraw = rsDraw- ,sHandle = rsHandle- ,rsList = list RegisterList V.empty 1- ,rsAccount = ""- ,rsForceInclusive = False- }--rsSetAccount :: AccountName -> Bool -> Screen -> Screen-rsSetAccount a forceinclusive scr@RegisterScreen{} =- scr{rsAccount=replaceHiddenAccountsNameWith "*" a, rsForceInclusive=forceinclusive}-rsSetAccount _ _ scr = scr--rsInit :: Day -> Bool -> UIState -> UIState-rsInit d reset ui@UIState{aopts=_uopts@UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}}, ajournal=j, aScreen=s@RegisterScreen{..}} =- dlogUiTrace "rsInit 1" $- ui{aScreen=s{rsList=newitems'}}- where- -- gather arguments and queries- -- XXX temp- inclusive = tree_ ropts || rsForceInclusive- thisacctq = Acct $ (if inclusive then accountNameToAccountRegex else accountNameToAccountOnlyRegex) rsAccount-- -- adjust the report options and regenerate the ReportSpec, 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- }- wd = whichDate ropts'- rspec' = reportSpecSetFutureAndForecast d (forecast_ $ inputopts_ copts) .- either (error "rsInit: adjusting the query for register, should not have failed") id $ -- PARTIAL:- updateReportSpec ropts' rspec{_rsDay=d}- 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-- -- generate pre-rendered list items. This 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- -- _ -> "<split>" -- should do this if accounts field width < 30- ,rsItemChangeAmount = showamt change- ,rsItemBalanceAmount = showamt bal- ,rsItemTransaction = t- }- where showamt = showMixedAmountB oneLine{displayMaxWidth=Just 32}- -- 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 List- newitems = list RegisterList (V.fromList $ displayitems ++ blankitems) 1-- -- decide which transaction is selected:- -- if reset is true, the last (latest) transaction;- -- 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.- newitems' = listMoveTo newselidx newitems- where- newselidx =- case (reset, listSelectedElement rsList) 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- endidx = max 0 $ length displayitems - 1--rsInit _ _ _ = dlogUiTrace "rsInit 2" $ errorWrongScreenType "init function" -- PARTIAL:+import Hledger.UI.ErrorScreen (uiReloadJournal, uiCheckBalanceAssertions, uiReloadJournalIfChanged) rsDraw :: UIState -> [Widget Name] rsDraw UIState{aopts=_uopts@UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec}}- ,aScreen=RegisterScreen{..}+ ,aScreen=RS RSS{..} ,aMode=mode- } = dlogUiTrace "rsDraw 1" $+ } = dbgui "rsDraw 1" $ case mode of- Help -> [helpDialog copts, maincontent]- -- Minibuffer e -> [minibuffer e, maincontent]+ Help -> [helpDialog, maincontent] _ -> [maincontent] where- displayitems = V.toList $ rsList ^. listElementsL+ displayitems = V.toList $ listElements $ _rssList maincontent = Widget Greedy Greedy $ do -- calculate column widths, based on current available width c <- getContext@@ -191,7 +92,7 @@ acctswidth = maxdescacctswidth - descwidth colwidths = (datewidth,descwidth,acctswidth,changewidth,balwidth) - render $ defaultLayout toplabel bottomlabel $ renderList (rsDrawItem colwidths) True rsList+ render $ defaultLayout toplabel bottomlabel $ renderList (rsDrawItem colwidths) True _rssList where ropts = _rsReportOpts rspec@@ -199,7 +100,7 @@ -- inclusive = tree_ ropts || rsForceInclusive toplabel =- withAttr (attrName "border" <> attrName "bold") (str $ T.unpack $ replaceHiddenAccountsNameWith "All" rsAccount)+ withAttr (attrName "border" <> attrName "bold") (str $ T.unpack $ replaceHiddenAccountsNameWith "All" _rssAccount) -- <+> withAttr ("border" <> "query") (str $ if inclusive then "" else " exclusive") <+> togglefilters <+> str " transactions"@@ -222,11 +123,11 @@ ] of [] -> str "" fs -> withAttr (attrName "border" <> attrName "query") (str $ " " ++ intercalate ", " fs)- cur = str $ case rsList ^. listSelectedL of+ cur = str $ case listSelected _rssList of Nothing -> "-" Just i -> show (i + 1) total = str $ show $ length nonblanks- nonblanks = V.takeWhile (not . T.null . rsItemDate) $ rsList^.listElementsL+ nonblanks = V.takeWhile (not . T.null . rsItemDate) $ listElements $ _rssList -- query = query_ $ reportopts_ $ cliopts_ opts @@ -251,7 +152,7 @@ -- ,("q", "quit") ] -rsDraw _ = dlogUiTrace "rsDraw 2" $ errorWrongScreenType "draw function" -- PARTIAL:+rsDraw _ = dbgui "rsDraw 2" $ errorWrongScreenType "draw function" -- PARTIAL: rsDrawItem :: (Int,Int,Int,Int,Int) -> Bool -> RegisterScreenItem -> Widget Name rsDrawItem (datewidth,descwidth,acctswidth,changewidth,balwidth) selected RegisterScreenItem{..} =@@ -278,23 +179,30 @@ sel | selected = (<> attrName "selected") | otherwise = id +-- XXX clean up like asHandle rsHandle :: BrickEvent Name AppEvent -> EventM Name UIState () rsHandle ev = do ui0 <- get'- dlogUiTraceM "rsHandle 1"+ dbguiEv "rsHandle 1" case ui0 of ui@UIState{- aScreen=scr@RegisterScreen{..}+ aScreen=RS sst@RSS{..} ,aopts=UIOpts{uoCliOpts=copts} ,ajournal=j ,aMode=mode } -> do+ d <- liftIO getCurrentDay let- d = copts^.rsDay journalspan = journalDateSpan False j- nonblanks = V.takeWhile (not . T.null . rsItemDate) $ rsList^.listElementsL+ nonblanks = V.takeWhile (not . T.null . rsItemDate) $ listElements $ _rssList lastnonblankidx = max 0 (length nonblanks - 1)-+ numberedtxns = zipWith (curry (second rsItemTransaction)) [(1::Integer)..] (V.toList nonblanks)+ -- the transactions being shown and the currently selected or last transaction, if any:+ mtxns :: Maybe ([NumberedTransaction], NumberedTransaction)+ mtxns = case numberedtxns of+ [] -> Nothing+ nts@(_:_) -> Just (nts, maybe (last nts) (bimap ((+1).fromIntegral) rsItemTransaction) $+ listSelectedElement _rssList) -- PARTIAL: last won't fail case mode of Minibuffer _ ed -> case ev of@@ -326,19 +234,23 @@ VtyEvent (EvKey (KChar 'q') []) -> halt VtyEvent (EvKey KEsc []) -> put' $ resetScreens d ui VtyEvent (EvKey (KChar c) []) | c == '?' -> put' $ setMode Help ui++ -- AppEvents arrive in --watch mode, see AccountsScreen AppEvent (DateChange old _) | isStandardPeriod p && p `periodContainsDate` old -> put' $ regenerateScreens j d $ setReportPeriod (DayPeriod d) ui where p = reportPeriod ui- e | e `elem` [VtyEvent (EvKey (KChar 'g') []), AppEvent FileChange] ->++ e | e `elem` [AppEvent FileChange, VtyEvent (EvKey (KChar 'g') [])] -> liftIO (uiReloadJournal copts d ui) >>= put'+ VtyEvent (EvKey (KChar 'I') []) -> put' $ uiCheckBalanceAssertions d (toggleIgnoreBalanceAssertions ui) VtyEvent (EvKey (KChar 'a') []) -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add copts j >> uiReloadJournalIfChanged copts d j ui VtyEvent (EvKey (KChar 'A') []) -> suspendAndResume $ void (runIadd (journalFilePath j)) >> uiReloadJournalIfChanged 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 where- (pos,f) = case listSelectedElement rsList of+ (pos,f) = case listSelectedElement _rssList of Nothing -> (endPosition, journalFilePath j) Just (_, RegisterScreenItem{ rsItemTransaction=Transaction{tsourcepos=(SourcePos f' l c,_)}}) -> (Just (unPos l, Just $ unPos c),f')@@ -361,73 +273,85 @@ VtyEvent (EvKey (KRight) [MShift]) -> put' $ regenerateScreens j d $ nextReportPeriod journalspan ui VtyEvent (EvKey (KLeft) [MShift]) -> put' $ regenerateScreens j d $ previousReportPeriod journalspan ui VtyEvent (EvKey k []) | k `elem` [KBS, KDel] -> (put' $ regenerateScreens j d $ resetFilter ui)- VtyEvent (EvKey (KChar 'l') [MCtrl]) -> scrollSelectionToMiddle rsList >> redraw+ VtyEvent (EvKey (KChar 'l') [MCtrl]) -> scrollSelectionToMiddle _rssList >> redraw VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui -- exit screen on LEFT VtyEvent e | e `elem` moveLeftEvents -> put' $ popScreen ui -- or on a click in the app's left margin. This is a VtyEvent since not in a clickable widget. VtyEvent (EvMouseUp x _y (Just BLeft)) | x==0 -> put' $ popScreen ui- -- or on clicking a blank list item.- MouseUp _ (Just BLeft) Location{loc=(_,y)} | clickeddate == "" -> put' $ popScreen ui- where clickeddate = maybe "" rsItemDate $ listElements rsList !? y -- enter transaction screen on RIGHT VtyEvent e | e `elem` moveRightEvents ->- case listSelectedElement rsList of- Just _ -> put' $ screenEnter d transactionScreen{tsAccount=rsAccount} ui- Nothing -> put' ui+ case mtxns of Nothing -> return (); Just (nts, nt) -> rsEnterTransactionScreen _rssAccount nts nt ui -- or on transaction click -- MouseDown is sometimes duplicated, https://github.com/jtdaugherty/brick/issues/347 -- just use it to move the selection MouseDown _n BLeft _mods Location{loc=(_x,y)} | not $ (=="") clickeddate -> do- put' $ ui{aScreen=scr{rsList=listMoveTo y rsList}}- where clickeddate = maybe "" rsItemDate $ listElements rsList !? y+ put' $ ui{aScreen=RS sst{_rssList=listMoveTo y _rssList}}+ where clickeddate = maybe "" rsItemDate $ listElements _rssList !? y -- and on MouseUp, enter the subscreen MouseUp _n (Just BLeft) Location{loc=(_x,y)} | not $ (=="") clickeddate -> do- put' $ screenEnter d transactionScreen{tsAccount=rsAccount} ui- where clickeddate = maybe "" rsItemDate $ listElements rsList !? y+ case mtxns of Nothing -> return (); Just (nts, nt) -> rsEnterTransactionScreen _rssAccount nts nt ui+ where clickeddate = maybe "" rsItemDate $ listElements _rssList !? y -- when selection is at the last item, DOWN scrolls instead of moving, until maximally scrolled VtyEvent e | e `elem` moveDownEvents, isBlankElement mnextelement -> do- vScrollBy (viewportScroll $ rsList ^. listNameL) 1- where mnextelement = listSelectedElement $ listMoveDown rsList+ vScrollBy (viewportScroll $ listName $ _rssList) 1+ where mnextelement = listSelectedElement $ listMoveDown _rssList -- mouse scroll wheel scrolls the viewport up or down to its maximum extent, -- pushing the selection when necessary. MouseDown name btn _mods _loc | btn `elem` [BScrollUp, BScrollDown] -> do let scrollamt = if btn==BScrollUp then -1 else 1- list' <- nestEventM' rsList $ listScrollPushingSelection name (rsListSize rsList) scrollamt- put' ui{aScreen=scr{rsList=list'}}+ list' <- nestEventM' _rssList $ listScrollPushingSelection name (rsListSize _rssList) scrollamt+ put' ui{aScreen=RS sst{_rssList=list'}} -- if page down or end leads to a blank padding item, stop at last non-blank VtyEvent e@(EvKey k []) | k `elem` [KPageDown, KEnd] -> do- l <- nestEventM' rsList $ handleListEvent e+ l <- nestEventM' _rssList $ handleListEvent e if isBlankElement $ listSelectedElement l then do let l' = listMoveTo lastnonblankidx l scrollSelectionToMiddle l'- put' ui{aScreen=scr{rsList=l'}}+ put' ui{aScreen=RS sst{_rssList=l'}} else- put' ui{aScreen=scr{rsList=l}}+ put' ui{aScreen=RS sst{_rssList=l}} -- fall through to the list's event handler (handles other [pg]up/down events) VtyEvent e -> do let e' = normaliseMovementKeys e- newitems <- nestEventM' rsList $ handleListEvent e'- put' ui{aScreen=scr{rsList=newitems}}+ newitems <- nestEventM' _rssList $ handleListEvent e'+ put' ui{aScreen=RS sst{_rssList=newitems}} MouseDown{} -> return () MouseUp{} -> return () AppEvent _ -> return () - _ -> dlogUiTrace "rsHandle 2" $ errorWrongScreenType "event handler"+ _ -> dbgui "rsHandle 2" $ errorWrongScreenType "event handler" isBlankElement mel = ((rsItemDate . snd) <$> mel) == Just "" +rsListSize = V.length . V.takeWhile ((/="").rsItemDate) . listElements++rsSetAccount :: AccountName -> Bool -> Screen -> Screen+rsSetAccount a forceinclusive (RS st@RSS{}) =+ RS st{_rssAccount=replaceHiddenAccountsNameWith "*" a, _rssForceInclusive=forceinclusive}+rsSetAccount _ _ st = st++-- | Scroll the selected item to the middle of the screen, when on the register screen.+-- No effect on other screens. rsCenterSelection :: UIState -> EventM Name UIState UIState-rsCenterSelection ui = do- scrollSelectionToMiddle $ rsList $ aScreen ui+rsCenterSelection ui@UIState{aScreen=RS sst} = do+ scrollSelectionToMiddle $ _rssList sst return ui -- ui is unchanged, but this makes the function more chainable+rsCenterSelection ui = return ui -rsListSize = V.length . V.takeWhile ((/="").rsItemDate) . listElements+rsEnterTransactionScreen :: AccountName -> [NumberedTransaction] -> NumberedTransaction -> UIState -> EventM Name UIState ()+rsEnterTransactionScreen acct nts nt ui = do+ dbguiEv "rsEnterTransactionScreen"+ put' $+ pushScreen (tsNew acct nts nt)+ ui++
Hledger/UI/TransactionScreen.hs view
@@ -6,7 +6,10 @@ {-# OPTIONS_GHC -Wno-incomplete-record-updates #-} module Hledger.UI.TransactionScreen-( transactionScreen+(tsNew+,tsUpdate+,tsDraw+,tsHandle ) where import Control.Monad@@ -14,79 +17,32 @@ import Data.List import Data.Maybe import qualified Data.Text as T-import Data.Time.Calendar (Day)-import qualified Data.Vector as V import Graphics.Vty (Event(..),Key(..),Modifier(..), Button (BLeft))-import Lens.Micro ((^.)) import Brick-import Brick.Widgets.List (listElementsL, listMoveTo, listSelectedElement)+import Brick.Widgets.List (listMoveTo) import Hledger import Hledger.Cli hiding (mode, prices, progname,prognameandversion) import Hledger.UI.UIOptions--- import Hledger.UI.Theme import Hledger.UI.UITypes import Hledger.UI.UIState import Hledger.UI.UIUtils+import Hledger.UI.UIScreens import Hledger.UI.Editor-import Hledger.UI.ErrorScreen import Brick.Widgets.Edit (editorText, renderEditor)--transactionScreen :: Screen-transactionScreen = TransactionScreen{- sInit = tsInit- ,sDraw = tsDraw- ,sHandle = tsHandle- ,tsTransaction = (1,nulltransaction)- ,tsTransactions = [(1,nulltransaction)]- ,tsAccount = ""- }--tsInit :: Day -> Bool -> UIState -> UIState-tsInit _d _reset ui@UIState{aopts=UIOpts{}- ,ajournal=_j- ,aScreen=s@TransactionScreen{tsTransaction=(_,t),tsTransactions=nts}- ,aPrevScreens=prevscreens- } =- ui{aScreen=s{tsTransaction=(i',t'),tsTransactions=nts'}}- where- i' = maybe 0 (toInteger . (+1)) . elemIndex t' $ map snd nts'- -- If the previous screen was RegisterScreen, use the listed and selected items as- -- the transactions. Otherwise, use the provided transaction and list.- (t',nts') = case prevscreens of- RegisterScreen{rsList=xs}:_ -> (seltxn, zip [1..] $ map rsItemTransaction nonblanks)- where- seltxn = maybe nulltransaction (rsItemTransaction . snd) $ listSelectedElement xs- nonblanks = V.toList . V.takeWhile (not . T.null . rsItemDate) $ xs ^. listElementsL- _ -> (t, nts)-tsInit _ _ _ = errorWrongScreenType "init function" -- PARTIAL:---- Render a transaction suitably for the transaction screen.-showTxn :: ReportOpts -> ReportSpec -> Journal -> Transaction -> T.Text-showTxn ropts rspec j t =- showTransactionOneLineAmounts- $ maybe id (transactionApplyValuation prices styles periodlast (_rsDay rspec)) (value_ ropts)- $ maybe id (transactionToCost styles) (conversionop_ ropts) t- -- (if real_ ropts then filterTransactionPostings (Real True) else id) -- filter postings by --real- where- prices = journalPriceOracle (infer_prices_ ropts) j- styles = journalCommodityStyles j- periodlast =- fromMaybe (error' "TransactionScreen: expected a non-empty journal") $ -- PARTIAL: shouldn't happen- reportPeriodOrJournalLastDay rspec j+import Hledger.UI.ErrorScreen (uiReloadJournalIfChanged, uiCheckBalanceAssertions) tsDraw :: UIState -> [Widget Name] tsDraw UIState{aopts=UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}} ,ajournal=j- ,aScreen=TransactionScreen{tsTransaction=(i,t')- ,tsTransactions=nts- ,tsAccount=acct- }+ ,aScreen=TS TSS{_tssTransaction=(i,t')+ ,_tssTransactions=nts+ ,_tssAccount=acct+ } ,aMode=mode } = case mode of- Help -> [helpDialog copts, maincontent]- -- Minibuffer e -> [minibuffer e, maincontent]+ Help -> [helpDialog, maincontent] _ -> [maincontent] where maincontent = Widget Greedy Greedy $ render $ defaultLayout toplabel bottomlabel txneditor@@ -141,15 +97,29 @@ tsDraw _ = errorWrongScreenType "draw function" -- PARTIAL: +-- Render a transaction suitably for the transaction screen.+showTxn :: ReportOpts -> ReportSpec -> Journal -> Transaction -> T.Text+showTxn ropts rspec j t =+ showTransactionOneLineAmounts+ $ maybe id (transactionApplyValuation prices styles periodlast (_rsDay rspec)) (value_ ropts)+ $ maybe id (transactionToCost styles) (conversionop_ ropts) t+ -- (if real_ ropts then filterTransactionPostings (Real True) else id) -- filter postings by --real+ where+ prices = journalPriceOracle (infer_prices_ ropts) j+ styles = journalCommodityStyles j+ periodlast =+ fromMaybe (error' "TransactionScreen: expected a non-empty journal") $ -- PARTIAL: shouldn't happen+ reportPeriodOrJournalLastDay rspec j+ tsHandle :: BrickEvent Name AppEvent -> EventM Name UIState () tsHandle ev = do ui0 <- get' case ui0 of- ui@UIState{aScreen=TransactionScreen{tsTransaction=(i,t), tsTransactions=nts}- ,aopts=UIOpts{uoCliOpts=copts@CliOpts{reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}}- ,ajournal=j- ,aMode=mode- } ->+ ui@UIState{aScreen=TS TSS{_tssTransaction=(i,t), _tssTransactions=nts}+ ,aopts=UIOpts{uoCliOpts=copts}+ ,ajournal=j+ ,aMode=mode+ } -> case mode of Help -> case ev of@@ -159,8 +129,8 @@ _ -> helpHandle ev _ -> do+ d <- liftIO getCurrentDay let- d = copts^.rsDay (iprev,tprev) = maybe (i,t) ((i-1),) $ lookup (i-1) nts (inext,tnext) = maybe (i,t) ((i+1),) $ lookup (i+1) nts case ev of@@ -179,7 +149,7 @@ -- plog (if e == AppEvent FileChange then "file change" else "manual reload") "" `seq` return () ej <- liftIO . runExceptT $ journalReload copts case ej of- Left err -> put' $ screenEnter d errorScreen{esError=err} ui+ Left err -> put' $ pushScreen (esNew err) ui Right j' -> put' $ regenerateScreens j' d ui VtyEvent (EvKey (KChar 'I') []) -> put' $ uiCheckBalanceAssertions d (toggleIgnoreBalanceAssertions ui) @@ -198,9 +168,6 @@ VtyEvent e | e `elem` moveLeftEvents -> put' . popScreen $ tsSelect i t ui -- Probably not necessary to tsSelect here, but it's safe. -- or on a click in the app's left margin. VtyEvent (EvMouseUp x _y (Just BLeft)) | x==0 -> put' . popScreen $ tsSelect i t ui- -- or on clicking the blank area below the transaction.- MouseUp _ (Just BLeft) Location{loc=(_,y)} | y+1 > numentrylines -> put' . popScreen $ tsSelect i t ui- where numentrylines = length (T.lines $ showTxn ropts rspec j t) - 1 VtyEvent (EvKey (KChar 'l') [MCtrl]) -> redraw VtyEvent (EvKey (KChar 'z') [MCtrl]) -> suspend ui@@ -209,12 +176,14 @@ _ -> errorWrongScreenType "event handler" -- | Select a new transaction and update the previous register screen-tsSelect i t ui@UIState{aScreen=s@TransactionScreen{}} = case aPrevScreens ui of+tsSelect :: Integer -> Transaction -> UIState -> UIState+tsSelect i t ui@UIState{aScreen=TS sst} = case aPrevScreens ui of x:xs -> ui'{aPrevScreens=rsSelect i x : xs} [] -> ui'- where ui' = ui{aScreen=s{tsTransaction=(i,t)}}+ where ui' = ui{aScreen=TS sst{_tssTransaction=(i,t)}} tsSelect _ _ ui = ui -- | Select the nth item on the register screen.-rsSelect i scr@RegisterScreen{..} = scr{rsList=listMoveTo (fromInteger $ i-1) rsList}+rsSelect :: Integer -> Screen -> Screen+rsSelect i (RS sst@RSS{..}) = RS sst{_rssList=listMoveTo (fromInteger $ i-1) _rssList} rsSelect _ scr = scr
Hledger/UI/UIOptions.hs view
@@ -36,7 +36,11 @@ -- flagNone ["debug-ui"] (setboolopt "rules-file") "run with no terminal output, showing console" flagNone ["watch","w"] (setboolopt "watch") "watch for data and date changes and reload automatically" ,flagReq ["theme"] (\s opts -> Right $ setopt "theme" s opts) "THEME" ("use this custom display theme ("++intercalate ", " themeNames++")")- ,flagReq ["register"] (\s opts -> Right $ setopt "register" s opts) "ACCTREGEX" "start in the (first) matched account's register"+ ,flagNone ["menu"] (setboolopt "menu") "start in the menu screen"+ ,flagNone ["all"] (setboolopt "all") "start in the all accounts screen"+ ,flagNone ["bs"] (setboolopt "bs") "start in the balance sheet accounts screen"+ ,flagNone ["is"] (setboolopt "is") "start in the income statement accounts screen"+ ,flagReq ["register"] (\s opts -> Right $ setopt "register" s opts) "ACCTREGEX" "start in the (first matched) account's register" ,flagNone ["change"] (setboolopt "change") "show period balances (changes) at startup instead of historical balances" -- ,flagNone ["cumulative"] (setboolopt "cumulative")@@ -52,8 +56,8 @@ --uimode :: Mode RawOpts uimode = (mode "hledger-ui" (setopt "command" "ui" def)- "browse accounts, postings and entries in a full-window curses interface"- (argsFlag "[PATTERNS]") []){+ "browse accounts, postings and entries in a full-window TUI"+ (argsFlag "[--menu|--all|--bs|--is|--register=ACCT] [QUERY]") []){ modeGroupFlags = Group { groupUnnamed = uiflags ,groupHidden = hiddenflags@@ -61,7 +65,7 @@ ,groupNamed = [(generalflagsgroup1)] } ,modeHelpSuffix=[- -- "Reads your ~/.hledger.journal file, or another specified by $LEDGER_FILE or -f, and starts the full-window curses ui."+ -- "Reads your ~/.hledger.journal file, or another specified by $LEDGER_FILE or -f, and starts the full-window TUI." ] } @@ -101,7 +105,7 @@ --getHledgerUIOpts = processArgs uimode >>= return >>= rawOptsToUIOpts getHledgerUIOpts = do args <- getArgs >>= expandArgsAt- let args' = replaceNumericFlags args+ let args' = replaceNumericFlags $ ensureDebugHasArg args let cmdargopts = either usageError id $ process uimode args' rawOptsToUIOpts cmdargopts
+ Hledger/UI/UIScreens.hs view
@@ -0,0 +1,329 @@+-- | 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+ -- _ -> "<split>" -- 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"+
Hledger/UI/UIState.hs view
@@ -1,27 +1,78 @@ {- | UIState operations. -} -{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}- module Hledger.UI.UIState+(uiState+,uiShowStatus+,setFilter+,setMode+,setReportPeriod+,showMinibuffer+,closeMinibuffer+,toggleCleared+,toggleConversionOp+,toggleIgnoreBalanceAssertions+,toggleEmpty+,toggleForecast+,toggleHistorical+,togglePending+,toggleUnmarked+,toggleReal+,toggleTree+,setTree+,setList+,toggleValue+,reportPeriod+,shrinkReportPeriod+,growReportPeriod+,nextReportPeriod+,previousReportPeriod+,resetReportPeriod+,moveReportPeriodToDate+,getDepth+,setDepth+,decDepth+,incDepth+,resetDepth+,popScreen+,pushScreen+,enableForecastPreservingPeriod+,resetFilter+,resetScreens+,regenerateScreens+) where import Brick.Widgets.Edit import Data.Bifunctor (first) import Data.Foldable (asum) import Data.Either (fromRight)-import Data.List ((\\), foldl', sort)+import Data.List ((\\), sort) import Data.Maybe (fromMaybe) import Data.Semigroup (Max(..)) import qualified Data.Text as T import Data.Text.Zipper (gotoEOL) import Data.Time.Calendar (Day) import Lens.Micro ((^.), over, set)+import Safe import Hledger import Hledger.Cli.CliOptions import Hledger.UI.UITypes+import Hledger.UI.UIOptions (UIOpts)+import Hledger.UI.UIScreens (screenUpdate) +-- | Make an initial UI state with the given options, journal,+-- parent screen stack if any, and starting screen.+uiState :: UIOpts -> Journal -> [Screen] -> Screen -> UIState+uiState uopts j prevscrs scr = UIState {+ astartupopts = uopts+ ,aopts = uopts+ ,ajournal = j+ ,aMode = Normal+ ,aScreen = scr+ ,aPrevScreens = prevscrs+ }+ -- | Toggle between showing only unmarked items or all items. toggleUnmarked :: UIState -> UIState toggleUnmarked = over statuses (toggleStatus1 Unmarked)@@ -66,7 +117,7 @@ -- pressing Y after first or second step starts new cycle: -- [u] P [p] -- [pc] P [p]--- toggleStatus2 s ss+-- toggleStatus s ss -- | ss == [s] = complement [s] -- | ss == complement [s] = [] -- | otherwise = [s] -- XXX assume only three values@@ -218,10 +269,10 @@ . set empty__ True -- set period PeriodAll . set rsQuery Any . set rsQueryOpts [] --- | Reset all options state to exactly what it was at startup--- (preserving any command-line options/arguments).-resetOpts :: UIState -> UIState-resetOpts ui@UIState{astartupopts} = ui{aopts=astartupopts}+-- -- | Reset all options state to exactly what it was at startup+-- -- (preserving any command-line options/arguments).+-- resetOpts :: UIState -> UIState+-- resetOpts ui@UIState{astartupopts} = ui{aopts=astartupopts} resetDepth :: UIState -> UIState resetDepth = updateReportDepth (const Nothing)@@ -278,22 +329,6 @@ setMode :: Mode -> UIState -> UIState setMode m ui = ui{aMode=m} --- | Regenerate the content for the current and previous screens, from a new journal and current date.-regenerateScreens :: Journal -> Day -> UIState -> UIState-regenerateScreens j d ui@UIState{aScreen=s,aPrevScreens=ss} =- -- XXX clumsy due to entanglement of UIState and Screen.- -- sInit operates only on an appstate's current screen, so- -- remove all the screens from the appstate and then add them back- -- one at a time, regenerating as we go.- let- frst:rest = reverse $ s:ss :: [Screen]- ui0 = ui{ajournal=j, aScreen=frst, aPrevScreens=[]} :: UIState-- ui1 = (sInit frst) d False ui0 :: UIState- ui2 = foldl' (\ui' s' -> (sInit s') d False $ pushScreen s' ui') ui1 rest :: UIState- in- ui2- pushScreen :: Screen -> UIState -> UIState pushScreen scr ui = ui{aPrevScreens=(aScreen ui:aPrevScreens ui) ,aScreen=scr@@ -303,18 +338,20 @@ popScreen ui@UIState{aPrevScreens=s:ss} = ui{aScreen=s, aPrevScreens=ss} popScreen ui = ui +-- | Reset options to their startup values, discard screen navigation history,+-- and return to the top screen, regenerating it with the startup options +-- and the provided reporting date. resetScreens :: Day -> UIState -> UIState-resetScreens d ui@UIState{aScreen=s,aPrevScreens=ss} =- (sInit topscreen) d True $- resetOpts $- closeMinibuffer ui{aScreen=topscreen, aPrevScreens=[]}+resetScreens d ui@UIState{astartupopts=origopts, ajournal=j, aScreen=s,aPrevScreens=ss} =+ ui{aopts=origopts, aPrevScreens=[], aScreen=topscreen', aMode=Normal} where- topscreen = case ss of _:_ -> last ss- [] -> s+ topscreen' = screenUpdate origopts d j $ lastDef s ss --- | Enter a new screen, saving the old screen & state in the--- navigation history and initialising the new screen's state.-screenEnter :: Day -> Screen -> UIState -> UIState-screenEnter d scr ui = (sInit scr) d True $- pushScreen scr- ui+-- | Given a new journal and reporting date, save the new journal in the ui state,+-- 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.+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/UITypes.hs view
@@ -36,6 +36,7 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE EmptyDataDeriving #-} module Hledger.UI.UITypes where @@ -43,10 +44,9 @@ -- import GHC.IO (unsafePerformIO) import Data.Text (Text) import Data.Time.Calendar (Day)-import Brick import Brick.Widgets.List (List) import Brick.Widgets.Edit (Editor)-import Lens.Micro.Platform+import Lens.Micro.Platform (makeLenses) import Text.Show.Functions () -- import the Show instance for functions. Warning, this also re-exports it @@ -54,21 +54,29 @@ import Hledger.Cli (HasCliOpts(..)) import Hledger.UI.UIOptions +data AppEvent =+ FileChange -- one of the Journal's files has been added/modified/removed+ | DateChange Day Day -- the current date has changed since last checked (with the old and new values)+ deriving (Eq, Show)+ -- | hledger-ui's application state. This holds one or more stateful screens. -- As you navigate through screens, the old ones are saved in a stack. -- The app can be in one of several modes: normal screen operation, -- showing a help dialog, entering data in the minibuffer etc. data UIState = UIState {- astartupopts :: UIOpts -- ^ the command-line options and query arguments specified at startup- ,aopts :: UIOpts -- ^ the command-line options and query arguments currently in effect- ,ajournal :: Journal -- ^ the journal being viewed- ,aPrevScreens :: [Screen] -- ^ previously visited screens, most recent first- ,aScreen :: Screen -- ^ the currently active screen- ,aMode :: Mode -- ^ the currently active mode+ -- unchanging:+ astartupopts :: UIOpts -- ^ the command-line options and query arguments specified at program start+ -- can change while program runs:+ ,aopts :: UIOpts -- ^ the command-line options and query arguments currently in effect+ ,ajournal :: Journal -- ^ the journal being viewed (can change with --watch)+ ,aPrevScreens :: [Screen] -- ^ previously visited screens, most recent first (XXX silly, reverse these)+ ,aScreen :: Screen -- ^ the currently active screen+ ,aMode :: Mode -- ^ the currently active mode on the current screen } deriving (Show) --- | The mode modifies the screen's rendering and event handling.--- It resets to Normal when entering a new screen.+-- | Any screen can be in one of several modes, which modifies +-- its rendering and event handling.+-- The mode resets to Normal when entering a new screen. data Mode = Normal | Help@@ -78,10 +86,11 @@ -- Ignore the editor when comparing Modes. instance Eq (Editor l n) where _ == _ = True --- Unique names required for widgets, viewports, cursor locations etc.+-- Unique names required for brick widgets, viewports, cursor locations etc. data Name = HelpDialog | MinibufferEditor+ | MenuList | AccountsViewport | AccountsList | RegisterViewport@@ -89,61 +98,150 @@ | TransactionEditor deriving (Ord, Show, Eq) -data AppEvent =- FileChange -- one of the Journal's files has been added/modified/removed- | DateChange Day Day -- the current date has changed since last checked (with the old and new values)- deriving (Eq, Show)+-- Unique names for screens the user can navigate to from the menu.+data ScreenName =+ Accounts+ | Balancesheet+ | Incomestatement+ deriving (Ord, Show, Eq) --- | hledger-ui screen types & instances.+----------------------------------------------------------------------------------------------------+-- | hledger-ui screen types, v1, "one screen = one module"+-- These types aimed for maximum decoupling of modules and ease of adding more screens.+-- A new screen requires+-- 1. a new constructor in the Screen type, +-- 2. a new module implementing init/draw/handle functions, +-- 3. a call from any other screen which enters it. -- Each screen type has generically named initialisation, draw, and event handling functions, -- and zero or more uniquely named screen state fields, which hold the data for a particular -- instance of this screen. Note the latter create partial functions, which means that some invalid -- cases need to be handled, and also that their lenses are traversals, not single-value getters.+-- data Screen =+-- AccountsScreen {+-- sInit :: Day -> Bool -> UIState -> UIState -- ^ function to initialise or update this screen's state+-- ,sDraw :: UIState -> [Widget Name] -- ^ brick renderer for this screen+-- ,sHandle :: BrickEvent Name AppEvent -> EventM Name UIState () -- ^ brick event handler for this screen+-- -- state fields.These ones have lenses:+-- ,_asList :: List Name AccountsScreenItem -- ^ list widget showing account names & balances+-- ,_asSelectedAccount :: AccountName -- ^ a backup of the account name from the list widget's selected item (or "")+-- }+-- | RegisterScreen {+-- sInit :: Day -> Bool -> UIState -> UIState+-- ,sDraw :: UIState -> [Widget Name]+-- ,sHandle :: BrickEvent Name AppEvent -> EventM Name UIState ()+-- --+-- ,rsList :: List Name RegisterScreenItem -- ^ list widget showing transactions affecting this account+-- ,rsAccount :: AccountName -- ^ the account this register is for+-- ,rsForceInclusive :: Bool -- ^ should this register always include subaccount transactions,+-- -- even when in flat mode ? (ie because entered from a+-- -- depth-clipped accounts screen item)+-- }+-- | TransactionScreen {+-- sInit :: Day -> Bool -> UIState -> UIState+-- ,sDraw :: UIState -> [Widget Name]+-- ,sHandle :: BrickEvent Name AppEvent -> EventM Name UIState ()+-- --+-- ,tsTransaction :: NumberedTransaction -- ^ the transaction we are currently viewing, and its position in the list+-- ,tsTransactions :: [NumberedTransaction] -- ^ list of transactions we can step through+-- ,tsAccount :: AccountName -- ^ the account whose register we entered this screen from+-- }+-- | ErrorScreen {+-- sInit :: Day -> Bool -> UIState -> UIState+-- ,sDraw :: UIState -> [Widget Name]+-- ,sHandle :: BrickEvent Name AppEvent -> EventM Name UIState ()+-- --+-- ,esError :: String -- ^ error message to show+-- }+-- deriving (Show)++----------------------------------------------------------------------------------------------------+-- | hledger-ui screen types, v2, "more parts, but simpler parts"+-- These types aim to be more restrictive, allowing fewer invalid states, and easier to inspect+-- and debug. The screen types store only state, not behaviour (functions), and there is no longer+-- a circular dependency between UIState and Screen.+-- A new screen requires+-- 1. a new constructor in the Screen type+-- 2. a new screen state type if needed+-- 3. a new case in toAccountsLikeScreen if needed+-- 4. new cases in the uiDraw and uiHandle functions+-- 5. new constructor and updater functions in UIScreens, and a new case in screenUpdate+-- 6. a new module implementing draw and event-handling functions+-- 7. a call from any other screen which enters it (eg the menu screen, a new case in msEnterScreen)+-- 8. if it appears on the main menu: a new menu item in msNew++-- cf https://github.com/jtdaugherty/brick/issues/379#issuecomment-1192000374+-- | The various screens which a user can navigate to in hledger-ui,+-- along with any screen-specific parameters or data influencing what they display.+-- (The separate state types add code noise but seem to reduce partial code/invalid data a bit.) data Screen =- AccountsScreen {- sInit :: Day -> Bool -> UIState -> UIState -- ^ function to initialise or update this screen's state- ,sDraw :: UIState -> [Widget Name] -- ^ brick renderer for this screen- ,sHandle :: BrickEvent Name AppEvent -> EventM Name UIState () -- ^ brick event handler for this screen- -- state fields.These ones have lenses:- ,_asList :: List Name AccountsScreenItem -- ^ list widget showing account names & balances- ,_asSelectedAccount :: AccountName -- ^ a backup of the account name from the list widget's selected item (or "")- }- | RegisterScreen {- sInit :: Day -> Bool -> UIState -> UIState- ,sDraw :: UIState -> [Widget Name]- ,sHandle :: BrickEvent Name AppEvent -> EventM Name UIState ()- --- ,rsList :: List Name RegisterScreenItem -- ^ list widget showing transactions affecting this account- ,rsAccount :: AccountName -- ^ the account this register is for- ,rsForceInclusive :: Bool -- ^ should this register always include subaccount transactions,- -- even when in flat mode ? (ie because entered from a- -- depth-clipped accounts screen item)- }- | TransactionScreen {- sInit :: Day -> Bool -> UIState -> UIState- ,sDraw :: UIState -> [Widget Name]- ,sHandle :: BrickEvent Name AppEvent -> EventM Name UIState ()- --- ,tsTransaction :: NumberedTransaction -- ^ the transaction we are currently viewing, and its position in the list- ,tsTransactions :: [NumberedTransaction] -- ^ list of transactions we can step through- ,tsAccount :: AccountName -- ^ the account whose register we entered this screen from- }- | ErrorScreen {- sInit :: Day -> Bool -> UIState -> UIState- ,sDraw :: UIState -> [Widget Name]- ,sHandle :: BrickEvent Name AppEvent -> EventM Name UIState ()- --- ,esError :: String -- ^ error message to show- }+ MS MenuScreenState+ | AS AccountsScreenState+ | BS AccountsScreenState+ | IS AccountsScreenState+ | RS RegisterScreenState+ | TS TransactionScreenState+ | ES ErrorScreenState deriving (Show)--- XXX check for ideas: https://github.com/jtdaugherty/brick/issues/379#issuecomment-1191993357 --- | Error message to use in case statements adapting to the different Screen shapes.-errorWrongScreenType :: String -> a-errorWrongScreenType lbl =- -- unsafePerformIO $ threadDelay 2000000 >> -- delay to allow console output to be seen- error' (unwords [lbl, "called with wrong screen type, should not happen"])+-- | A subset of the screens which reuse the account screen's state and logic.+-- Such Screens can be converted to and from this more restrictive type+-- for cleaner code.+data AccountsLikeScreen = ALS (AccountsScreenState -> Screen) AccountsScreenState+ deriving (Show) +toAccountsLikeScreen :: Screen -> Maybe AccountsLikeScreen+toAccountsLikeScreen scr = case scr of+ AS ass -> Just $ ALS AS ass+ BS ass -> Just $ ALS BS ass+ IS ass -> Just $ ALS IS ass+ _ -> Nothing++fromAccountsLikeScreen :: AccountsLikeScreen -> Screen+fromAccountsLikeScreen (ALS scons ass) = scons ass++data MenuScreenState = MSS {+ -- view data:+ _mssList :: List Name MenuScreenItem -- ^ list widget showing screen names+ ,_mssUnused :: () -- ^ dummy field to silence warning+} deriving (Show)++-- Used for the accounts screen and similar screens.+data AccountsScreenState = ASS {+ -- screen parameters:+ _assSelectedAccount :: AccountName -- ^ a copy of the account name from the list's selected item (or "")+ -- view data derived from options, reporting date, journal, and screen parameters:+ ,_assList :: List Name AccountsScreenItem -- ^ list widget showing account names & balances+} deriving (Show)++data RegisterScreenState = RSS {+ -- screen parameters:+ _rssAccount :: AccountName -- ^ the account this register is for+ ,_rssForceInclusive :: Bool -- ^ should this register always include subaccount transactions,+ -- even when in flat mode ? (ie because entered from a+ -- depth-clipped accounts screen item)+ -- view data derived from options, reporting date, journal, and screen parameters:+ ,_rssList :: List Name RegisterScreenItem -- ^ list widget showing transactions affecting this account+} deriving (Show)++data TransactionScreenState = TSS {+ -- screen parameters:+ _tssAccount :: AccountName -- ^ the account whose register we entered this screen from+ ,_tssTransactions :: [NumberedTransaction] -- ^ the transactions in that register, which we can step through+ ,_tssTransaction :: NumberedTransaction -- ^ the currently displayed transaction, and its position in the list+} deriving (Show)++data ErrorScreenState = ESS {+ -- screen parameters:+ _essError :: String -- ^ error message to show+ ,_essUnused :: () -- ^ dummy field to silence warning+} deriving (Show)++-- | An item in the menu screen's list of screens.+data MenuScreenItem = MenuScreenItem {+ msItemScreenName :: Text -- ^ screen display name+ ,msItemScreen :: ScreenName -- ^ an internal name we can use to find the corresponding screen+ } deriving (Show)+ -- | An item in the accounts screen's list of accounts and balances. data AccountsScreenItem = AccountsScreenItem { asItemIndentLevel :: Int -- ^ indent level@@ -166,13 +264,28 @@ type NumberedTransaction = (Integer, Transaction) +-- These TH calls must come after most of the types above.+-- Fields named _foo produce lenses named foo.+-- XXX foo fields producing fooL lenses would be preferable+makeLenses ''MenuScreenState+makeLenses ''AccountsScreenState+makeLenses ''RegisterScreenState+makeLenses ''TransactionScreenState+makeLenses ''ErrorScreenState++----------------------------------------------------------------------------------------------------++-- | Error message to use in case statements adapting to the different Screen shapes.+errorWrongScreenType :: String -> a+errorWrongScreenType lbl =+ -- unsafePerformIO $ threadDelay 2000000 >> -- delay to allow console output to be seen+ error' (unwords [lbl, "called with wrong screen type, should not happen"])+ -- dummy monoid instance needed make lenses work with List fields not common across constructors --instance Monoid (List n a) -- where -- mempty = list "" V.empty 1 -- XXX problem in 0.7, every list requires a unique Name--- mappend l1 l2 = l1 & listElementsL .~ (l1^.listElementsL <> l2^.listElementsL)--makeLenses ''Screen+-- mappend l1 l = l1 & listElementsL .~ (l1^.listElementsL <> l^.listElementsL) uioptslens f ui = (\x -> ui{aopts=x}) <$> f (aopts ui) @@ -193,3 +306,4 @@ instance HasReportOpts UIState where reportOpts = uioptslens.reportOpts+
Hledger/UI/UIUtils.hs view
@@ -3,6 +3,7 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE LambdaCase #-} module Hledger.UI.UIUtils ( borderDepthStr@@ -28,12 +29,19 @@ ,modify' ,suspend ,redraw+ ,reportSpecAddQuery ,reportSpecSetFutureAndForecast ,listScrollPushingSelection- ,dlogUiTrace- ,dlogUiTraceM- ,uiDebugLevel+ ,dbgui+ ,dbguiIO+ ,dbguiEv+ ,dbguiScreensEv+ ,showScreenId+ ,showScreenRegisterDescriptions+ ,showScreenSelection+ ,mapScreens ,uiNumBlankItems+ ,showScreenStack ) where @@ -42,12 +50,12 @@ import Brick.Widgets.Border.Style import Brick.Widgets.Dialog import Brick.Widgets.Edit-import Brick.Widgets.List (List, listSelectedL, listNameL, listItemHeightL, listSelected, listMoveDown, listMoveUp, GenericList)+import Brick.Widgets.List (List, listSelectedL, listNameL, listItemHeightL, listSelected, listMoveDown, listMoveUp, GenericList, listElements) import Control.Monad.IO.Class import Data.Bifunctor (second) import Data.List import qualified Data.Text as T-import Data.Time (Day, addDays)+import Data.Time (addDays) import Graphics.Vty (Event(..),Key(..),Modifier(..),Vty(..),Color,Attr,currentAttr,refresh, displayBounds -- ,Output(displayBounds,mkDisplayContext),DisplayContext(..)@@ -55,38 +63,63 @@ import Lens.Micro.Platform import Hledger-import Hledger.Cli (CliOpts)+-- import Hledger.Cli.CliOptions (CliOpts(reportspec_)) import Hledger.Cli.DocFiles+-- import Hledger.UI.UIOptions (UIOpts(uoCliOpts)) import Hledger.UI.UITypes-import Hledger.UI.UIState +import Data.Vector (Vector)+import qualified Data.Vector as V+ -- | On posix platforms, send the system STOP signal to suspend the -- current program. On windows, does nothing.+-- (Though, currently hledger-ui is not built on windows.) #ifdef mingw32_HOST_OS suspendSignal :: IO () suspendSignal = return () #else import System.Posix.Signals-import Data.Vector (Vector) suspendSignal :: IO () suspendSignal = raiseSignal sigSTOP #endif -- Debug logging for UI state changes.+-- A good place to log things of interest while debugging, see commented examples below. get' = do- x <- get- dlogUiTraceM $ "getting state: " ++ (head $ lines $ pshow $ aScreen x)- return x+ ui <- get+ dbguiEv $ "getting state: " ++ + showScreenStack "" showScreenSelection ui+ -- (head $ lines $ pshow $ aScreen x)+ -- ++ " " ++ (show $ map tdescription $ jtxns $ ajournal x)+ -- dbguiEv $ ("query: "++) $ pshow' $ x & aopts & uoCliOpts & reportspec_ & _rsQuery+ -- dbguiScreensEv "getting" showScreenId x+ -- dbguiScreensEv "getting, with register descriptions" showScreenRegisterDescriptions x+ return ui -put' x = do- dlogUiTraceM $ "putting state: " ++ (head $ lines $ pshow $ aScreen x)- put x+put' ui = do+ dbguiEv $ "putting state: " +++ showScreenStack "" showScreenSelection ui+ -- (head $ lines $ pshow $ aScreen x)+ -- ++ " " ++ (show $ map tdescription $ jtxns $ ajournal x)+ -- dbguiEv $ ("query: "++) $ pshow' $ x & aopts & uoCliOpts & reportspec_ & _rsQuery+ -- dbguiScreensEv "putting" showScreenId x+ -- dbguiScreensEv "putting, with register descriptions" showScreenRegisterDescriptions x+ put ui modify' f = do- x <- get- let x' = f x- dlogUiTraceM $ "modifying state: " ++ (head $ lines $ pshow $ aScreen x')+ ui <- get+ let ui' = f ui+ dbguiEv $ "getting state: " ++ (showScreenStack "" showScreenSelection ui)+ dbguiEv $ "putting state: " ++ (showScreenStack "" showScreenSelection ui')+ -- (head $ lines $ pshow $ aScreen x')+ -- ++ " " ++ (show $ map tdescription $ jtxns $ ajournal x')+ -- dbguiEv $ ("from: "++) $ pshow' $ x & aopts & uoCliOpts & reportspec_ & _rsQuery+ -- dbguiEv $ ("to: "++) $ pshow' $ x' & aopts & uoCliOpts & reportspec_ & _rsQuery+ -- dbguiScreensEv "getting" showScreenId x+ -- dbguiScreensEv "putting" showScreenId x'+ -- dbguiScreensEv "getting, with register descriptions" showScreenRegisterDescriptions x+ -- dbguiScreensEv "putting, with register descriptions" showScreenRegisterDescriptions x' modify f -- | On posix platforms, suspend the program using the STOP signal,@@ -105,18 +138,18 @@ defaultLayout toplabel bottomlabel = topBottomBorderWithLabels (str " "<+>toplabel<+>str " ") (str " "<+>bottomlabel<+>str " ") . margin 1 0 Nothing- -- topBottomBorderWithLabel2 label .+ -- topBottomBorderWithLabel label . -- padLeftRight 1 -- XXX should reduce inner widget's width by 2, but doesn't -- "the layout adjusts... if you use the core combinators" -- | Draw the help dialog, called when help mode is active.-helpDialog :: CliOpts -> Widget Name-helpDialog _copts =+helpDialog :: Widget Name+helpDialog = Widget Fixed Fixed $ do c <- getContext render $ withDefAttr (attrName "help") $- renderDialog (dialog (Just "Help (LEFT/ESC/?/q to close help)") Nothing (c^.availWidthL)) $ -- (Just (0,[("ok",())]))+ renderDialog (dialog (Just $ str "Help (LEFT/ESC/?/q to close help)") Nothing (c^.availWidthL)) $ -- (Just (0,[("ok",())])) padTop (Pad 0) $ padLeft (Pad 1) $ padRight (Pad 1) $ vBox [ hBox [@@ -192,7 +225,7 @@ helpHandle :: BrickEvent Name AppEvent -> EventM Name UIState () helpHandle ev = do ui <- get- let ui' = setMode Normal ui+ let ui' = ui{aMode=Normal} case ev of VtyEvent e | e `elem` closeHelpEvents -> put' ui' VtyEvent (EvKey (KChar 'p') []) -> suspendAndResume (runPagerForTopic "hledger-ui" Nothing >> return ui')@@ -250,7 +283,8 @@ -- temporary shenanigans: --- | Convert the special account name "*" (from balance report with depth limit 0) to something clearer.+-- | Replace the special account names "*" and "..." (from balance reports with depth limit 0)+-- to something clearer. replaceHiddenAccountsNameWith :: AccountName -> AccountName -> AccountName replaceHiddenAccountsNameWith anew a | a == hiddenAccountsName = anew | a == "*" = anew@@ -295,8 +329,8 @@ hBorderWithLabel (withAttr (attrName "border") bottomlabel) ---- XXX should be equivalent to the above, but isn't (page down goes offscreen)---_topBottomBorderWithLabel2 :: Widget Name -> Widget Name -> Widget Name---_topBottomBorderWithLabel2 label = \wrapped ->+--_topBottomBorderWithLabel :: Widget Name -> Widget Name -> Widget Name+--_topBottomBorderWithLabel label = \wrapped -> -- let debugmsg = "" -- in hBorderWithLabel (label <+> str debugmsg) -- <=>@@ -309,7 +343,7 @@ -- thickness, using the current background colour or the specified -- colour. -- XXX May disrupt border style of inner widgets.--- XXX Should reduce the available size visible to inner widget, but doesn't seem to (cf rsDraw2).+-- XXX Should reduce the available size visible to inner widget, but doesn't seem to (cf rsDraw). margin :: Int -> Int -> Maybe Color -> Widget Name -> Widget Name margin h v mcolour w = Widget Greedy Greedy $ do ctx <- getContext@@ -358,11 +392,11 @@ toprow = dbg4 "toprow" $ max 0 (selectedrow - (itemsperpage `div` 2)) -- assuming ViewportScroll's row offset is measured in list items not screen rows setTop (viewportScroll $ list^.listNameL) toprow --- arrow keys vi keys emacs keys+-- arrow keys vi keys emacs keys enter key moveUpEvents = [EvKey KUp [] , EvKey (KChar 'k') [], EvKey (KChar 'p') [MCtrl]] moveDownEvents = [EvKey KDown [] , EvKey (KChar 'j') [], EvKey (KChar 'n') [MCtrl]] moveLeftEvents = [EvKey KLeft [] , EvKey (KChar 'h') [], EvKey (KChar 'b') [MCtrl]]-moveRightEvents = [EvKey KRight [], EvKey (KChar 'l') [], EvKey (KChar 'f') [MCtrl]]+moveRightEvents = [EvKey KRight [], EvKey (KChar 'l') [], EvKey (KChar 'f') [MCtrl], EvKey KEnter []] normaliseMovementKeys ev | ev `elem` moveUpEvents = EvKey KUp []@@ -371,11 +405,16 @@ | ev `elem` moveRightEvents = EvKey KRight [] | otherwise = ev --- | Update the ReportSpec's query to exclude future transactions (later than the given day)+-- | Restrict the ReportSpec's query by adding the given additional query.+reportSpecAddQuery :: Query -> ReportSpec -> ReportSpec+reportSpecAddQuery q rspec =+ rspec{_rsQuery=simplifyQuery $ And [_rsQuery rspec, q]}++-- | Update the ReportSpec's query to exclude future transactions (later than its "today" date) -- and forecast transactions (generated by --forecast), if the given forecast DateSpan is Nothing, -- and include them otherwise.-reportSpecSetFutureAndForecast :: Day -> Maybe DateSpan -> ReportSpec -> ReportSpec-reportSpecSetFutureAndForecast d fcast rspec =+reportSpecSetFutureAndForecast :: Maybe DateSpan -> ReportSpec -> ReportSpec+reportSpecSetFutureAndForecast fcast rspec = rspec{_rsQuery=simplifyQuery $ And [_rsQuery rspec, periodq, excludeforecastq fcast]} where periodq = Date . periodAsDateSpan . period_ $ _rsReportOpts rspec@@ -383,7 +422,7 @@ excludeforecastq (Just _) = Any excludeforecastq Nothing = -- not:date:tomorrow- not:tag:generated-transaction And [- Not (Date $ DateSpan (Just $ addDays 1 d) Nothing)+ Not (Date $ DateSpan (Just $ addDays 1 $ _rsDay rspec) Nothing) ,Not generatedTransactionTag ] @@ -408,19 +447,80 @@ _ -> return list _ -> return list --- | Log a string to ./debug.log before returning the second argument,--- if the global debug level is at or above a standard hledger-ui debug level.+-- | A debug logging helper for hledger-ui code: at any debug level >= 1,+-- logs the string to hledger-ui.log before returning the second argument. -- Uses unsafePerformIO.-dlogUiTrace :: String -> a -> a-dlogUiTrace = dlogTraceAt uiDebugLevel+dbgui :: String -> a -> a+dbgui = traceLogAt 1 --- | Like dlogUiTrace, but within the hledger-ui brick event handler monad.-dlogUiTraceM :: String -> EventM Name UIState ()-dlogUiTraceM s = dlogUiTrace s $ return ()+-- | Like dbgui, but convenient to use in IO.+dbguiIO :: String -> IO ()+dbguiIO = traceLogAtIO 1 --- | Log hledger-ui events at this debug level.-uiDebugLevel :: Int-uiDebugLevel = 2+-- | Like dbgui, but convenient to use in EventM handlers.+dbguiEv :: String -> EventM Name s ()+dbguiEv s = dbgui s $ return ()++-- | Like dbguiEv, but log a compact view of the current screen stack.+-- See showScreenStack.+-- To just log the stack: @dbguiScreensEv "" showScreenId ui@+dbguiScreensEv :: String -> (Screen -> String) -> UIState -> EventM Name UIState ()+dbguiScreensEv postfix showscr ui = dbguiEv $ showScreenStack postfix showscr ui++-- Render a compact labelled view of the current screen stack,+-- adding the given postfix to the label (can be empty),+-- from the topmost screen to the currently-viewed screen,+-- with each screen rendered by the given rendering function.+-- Useful for inspecting states across the whole screen stack.+-- Some screen rendering functions are +-- @showScreenId@, @showScreenSelection@, @showScreenRegisterDescriptions@.+--+-- Eg to just show the stack: @showScreenStack "" showScreenId ui@+--+-- To to show the stack plus selected item indexes: @showScreenStack "" showScreenSelection ui@+--+showScreenStack :: String -> (Screen -> String) -> UIState -> String+showScreenStack postfix showscr ui = concat [+ "screen stack"+ ,if null postfix then "" else ", " ++ postfix+ ,": "+ ,unwords $ mapScreens showscr ui+ ]++-- | Run a function on each screen in a UIState's screen "stack",+-- from topmost screen down to currently-viewed screen.+mapScreens :: (Screen -> a) -> UIState -> [a]+mapScreens f UIState{aPrevScreens, aScreen} = map f $ reverse $ aScreen : aPrevScreens++-- Show a screen's compact id (first letter of its constructor).+showScreenId :: Screen -> String+showScreenId = \case+ MS _ -> "M" -- menu+ AS _ -> "A" -- all accounts+ BS _ -> "B" -- bs accounts+ IS _ -> "I" -- is accounts+ RS _ -> "R" -- menu+ TS _ -> "T" -- transaction+ ES _ -> "E" -- error++-- Show a screen's compact id, plus for register screens, the transaction descriptions.+showScreenRegisterDescriptions :: Screen -> String+showScreenRegisterDescriptions scr = case scr of+ RS sst -> ((showScreenId scr ++ ":") ++) $ -- menu+ intercalate "," $ map (T.unpack . rsItemDescription) $+ takeWhile (not . T.null . rsItemDate) $ V.toList $ listElements $ _rssList sst+ _ -> showScreenId scr++-- Show a screen's compact id, plus index of its selected list item if any.+showScreenSelection :: Screen -> String+showScreenSelection = \case+ MS MSS{_mssList} -> "M" ++ (maybe "" show $ listSelected _mssList) -- menu+ AS ASS{_assList} -> "A" ++ (maybe "" show $ listSelected _assList) -- all accounts+ BS ASS{_assList} -> "B" ++ (maybe "" show $ listSelected _assList) -- bs accounts+ IS ASS{_assList} -> "I" ++ (maybe "" show $ listSelected _assList) -- is accounts+ RS RSS{_rssList} -> "R" ++ (maybe "" show $ listSelected _rssList) -- menu+ TS _ -> "T" -- transaction+ ES _ -> "E" -- error -- | How many blank items to add to lists to fill the full window height. uiNumBlankItems :: Int
hledger-ui.1 view
@@ -1,5 +1,5 @@ -.TH "HLEDGER-UI" "1" "September 2022" "hledger-ui-1.27 " "hledger User Manuals"+.TH "HLEDGER-UI" "1" "December 2022" "hledger-ui-1.28 " "hledger User Manuals" @@ -7,7 +7,7 @@ .PP hledger-ui is a terminal interface (TUI) for the hledger accounting tool.-This manual is for hledger-ui 1.27.+This manual is for hledger-ui 1.28. .SH SYNOPSIS .PP \f[C]hledger-ui [OPTIONS] [QUERYARGS]\f[R]@@ -53,6 +53,18 @@ \f[B]\f[CB]--theme=default|terminal|greenterm\f[B]\f[R] use this custom display theme .TP+\f[B]\f[CB]--menu\f[B]\f[R]+start in the menu screen+.TP+\f[B]\f[CB]--all\f[B]\f[R]+start in the all accounts screen+.TP+\f[B]\f[CB]--bs\f[B]\f[R]+start in the balance sheet accounts screen+.TP+\f[B]\f[CB]--is\f[B]\f[R]+start in the income statement accounts screen+.TP \f[B]\f[CB]--register=ACCTREGEX\f[B]\f[R] start in the (first) matched account\[aq]s register screen .TP@@ -228,8 +240,7 @@ .IP \[bu] 2 Click on list items to go deeper .IP \[bu] 2-Click on the left margin (column 0), or the blank area at bottom of-screen, to go back.+Click on the left margin (column 0) to go back. .SH KEYS .PP Keyboard gives more control.@@ -240,14 +251,14 @@ \f[C]LEFT\f[R], or \f[C]q\f[R]) to close it. The following keys work on most screens: .PP-The cursor keys navigate: \f[C]RIGHT\f[R] goes deeper, \f[C]LEFT\f[R]-returns to the previous screen,+The cursor keys navigate: \f[C]RIGHT\f[R] or \f[C]ENTER\f[R] goes+deeper, \f[C]LEFT\f[R] returns to the previous screen, \f[C]UP\f[R]/\f[C]DOWN\f[R]/\f[C]PGUP\f[R]/\f[C]PGDN\f[R]/\f[C]HOME\f[R]/\f[C]END\f[R] move up and down through lists. Emacs-style (\f[C]CTRL-p\f[R]/\f[C]CTRL-n\f[R]/\f[C]CTRL-f\f[R]/\f[C]CTRL-b\f[R])-movement keys are also supported (but not vi-style keys, since-hledger-1.19, sorry!).+and VI-style (\f[C]k\f[R],\f[C]j\f[R],\f[C]l\f[R],\f[C]h\f[R]) movement+keys are also supported. A tip: movement speed is limited by your keyboard repeat rate, to move faster you may want to adjust it. (If you\[aq]re on a mac, the karabiner app is one way to do that.)@@ -335,18 +346,57 @@ .PP Additional screen-specific keys are described below. .SH SCREENS-.SS Accounts screen .PP-This is normally the first screen displayed.-It lists accounts and their balances, like hledger\[aq]s balance-command.-By default, it shows all accounts and their latest ending balances-(including the balances of subaccounts).-Accounts which have been declared with an account directive are also-listed, even if not yet used (except for empty parent accounts).-If you specify a query on the command line, it shows just the matched-accounts and the balances from matched transactions.+hledger-ui shows several different screens, described below.+It shows the \[dq]Balance sheet accounts\[dq] screen to start with,+except in the following situations:+.IP \[bu] 2+If no asset/liability/equity accounts can be detected, or if an account+query has been given on the command line, it starts in the \[dq]All+accounts\[dq] screen.+.IP \[bu] 2+If a starting screen is specified with --menu/--all/--bs/--is/--register+on the command line, it starts in that screen. .PP+From any screen you can press \f[C]LEFT\f[R] or \f[C]ESC\f[R] to+navigate back to the top level \[dq]Menu\[dq] screen.+.SS Menu+.PP+The top-most screen.+From here you can navigate to three accounts screens:+.SS All accounts+.PP+This screen shows all accounts (possibly filtered by a query), and their+end balances on the date shown in the title bar (or their balance+changes in the period shown in the title bar, toggleable with+\f[C]H\f[R]).+It is like the \f[C]hledger balance\f[R] command.+.SS Balance sheet accounts+.PP+This screen shows asset, liability and equity accounts, if these can be+detected (see account types).+It always shows end balances.+It is like the \f[C]hledger balancesheetequity\f[R] command.+.SS Income statement accounts+.PP+This screen shows revenue and expense accounts.+It always shows balance changes.+It is like the \f[C]hledger incomestatement\f[R] command.+.PP+All of these accounts screens work in much the same way:+.PP+They show accounts which have been posted to by transactions, as well as+accounts which have been declared with an account directive (except for+empty parent accounts).+.PP+If you specify a query on the command line or with \f[C]/\f[R] in the+app, they show just the matched accounts, and the balances from matched+transactions.+.PP+hledger-ui shows accounts with zero balances by default (unlike+command-line hledger).+To hide these, press \f[C]z\f[R] to toggle nonzero mode.+.PP Account names are shown as a flat list by default; press \f[C]t\f[R] to toggle tree mode. In list mode, account balances are exclusive of subaccounts, except@@ -363,7 +413,7 @@ or press \f[C]ESCAPE\f[R]. .PP \f[C]H\f[R] toggles between showing historical balances or period-balances.+balances (on the \[dq]All accounts\[dq] screen). Historical balances (the default) are ending balances at the end of the report period, taking into account all transactions before that date (filtered by the filter query if any), including transactions before the@@ -384,12 +434,9 @@ .PP \f[C]R\f[R] toggles real mode, in which virtual postings are ignored. .PP-\f[C]z\f[R] toggles nonzero mode, in which only accounts with nonzero-balances are shown (hledger-ui shows zero items by default, unlike-command-line hledger).-.PP-Press \f[C]RIGHT\f[R] to view an account\[aq]s transactions register.-.SS Register screen+Press \f[C]RIGHT\f[R] to view an account\[aq]s register screen, Or,+\f[C]LEFT\f[R] to see the menu screen.+.SS Register .PP This screen shows the transactions affecting a particular account, like a check register.@@ -435,7 +482,7 @@ command-line hledger). .PP Press \f[C]RIGHT\f[R] to view the selected transaction in detail.-.SS Transaction screen+.SS Transaction .PP This screen shows a single transaction, as a general journal entry, similar to hledger\[aq]s print command and journal format@@ -455,7 +502,7 @@ The #N number preceding them is the transaction\[aq]s position within the complete unfiltered journal, which is a more stable id (at least until the next reload).-.SS Error screen+.SS Error .PP This screen will appear if there is a problem, such as a parse error, when you press g to reload.@@ -486,34 +533,31 @@ This leaves more mental bandwidth for your accounting. Of course you can still interact with hledger-ui when needed, eg to toggle cleared mode, or to explore the history.-.SS Watch mode limitations .PP-There are situations in which it won\[aq]t work, ie the display will not-update when you save a change (because the underlying \f[C]inotify\f[R]-library does not support it).-Here are some that we know of:-.IP \[bu] 2-Certain editors: saving with \f[C]gedit\f[R], and perhaps any Gnome-application, won\[aq]t be detected (#1617).-Jetbrains IDEs, such as IDEA, also may not work (#911).-.IP \[bu] 2-Certain unusual filesystems might not be supported.-(All the usual ones on unix, mac and windows are supported.)+Here are some current limitations to be aware of: .PP-In such cases, the workaround is to switch to the hledger-ui window and-press \f[C]g\f[R] each time you want it to reload.-(Actually, see #1617 for another workaround, and let us know if it works-for you.)+Changes might not be detected with certain editors, possibly including+Jetbrains IDEs, \f[C]gedit\f[R], other Gnome applications; or on certain+unusual filesystems.+(#1617, #911).+To work around, reload manually by pressing \f[C]g\f[R] in the+hledger-ui window.+(Or see #1617 for another workaround, and let us know if it works for+you.) .PP-If you leave \f[C]hledger-ui --watch\f[R] running for days, on certain-platforms (?), perhaps with many transactions in your journal (?),-perhaps with large numbers of other files present (?), you may see it-gradually using more and more memory and CPU over time, as seen in-\f[C]top\f[R] or Activity Monitor or Task Manager.+CPU and memory usage can sometimes gradually increase, if+\f[C]hledger-ui --watch\f[R] is left running for days.+(Possibly correlated with certain platforms, many transactions, and/or+large numbers of other files present).+To work around, \f[C]q\f[R]uit and restart it, or (where supported)+suspend (\f[C]CTRL-z\f[R]) and restart it (\f[C]fg\f[R]).+.SS Debug output .PP-A workaround is to \f[C]q\f[R]uit and restart it, or to suspend it-(\f[C]CTRL-z\f[R]) and restart it (\f[C]fg\f[R]) if your shell supports-that.+You can add \f[C]--debug[=N]\f[R] to the command line to log debug+output.+This will be logged to the file \f[C]hledger-ui.log\f[R] in the current+directory.+N ranges from 1 (least output, the default) to 9 (maximum output). .SH ENVIRONMENT .PP \f[B]COLUMNS\f[R] The screen width to use.
hledger-ui.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.34.4.+-- This file has been generated from package.yaml by hpack version 0.35.0. -- -- see: https://github.com/sol/hpack name: hledger-ui-version: 1.27.1+version: 1.28 synopsis: Curses-style terminal interface for the hledger accounting system description: A simple curses-style terminal user interface for the hledger accounting system. It can be a more convenient way to browse your accounts than the CLI.@@ -49,13 +49,17 @@ other-modules: Hledger.UI Hledger.UI.AccountsScreen+ Hledger.UI.BalancesheetScreen Hledger.UI.Editor Hledger.UI.ErrorScreen+ Hledger.UI.IncomestatementScreen Hledger.UI.Main+ Hledger.UI.MenuScreen Hledger.UI.RegisterScreen Hledger.UI.Theme Hledger.UI.TransactionScreen Hledger.UI.UIOptions+ Hledger.UI.UIScreens Hledger.UI.UIState Hledger.UI.UITypes Hledger.UI.UIUtils@@ -63,13 +67,12 @@ 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.27.1"+ cpp-options: -DVERSION="1.28" build-depends: ansi-terminal >=0.9 , async- , base >=4.14 && <4.17- , breakpoint- , brick >=1.0+ , base >=4.14 && <4.18+ , brick >=1.5 , cmdargs >=0.8 , containers >=0.5.9 , data-default@@ -77,10 +80,10 @@ , doclayout >=0.3 && <0.5 , extra >=1.6.3 , filepath- , fsnotify >=0.2.1.2 && <0.4- , hledger >=1.27.1 && <1.28- , hledger-lib >=1.27.1 && <1.28- , megaparsec >=7.0.0 && <9.3+ , fsnotify ==0.4.*+ , hledger ==1.28.*+ , hledger-lib ==1.28.*+ , megaparsec >=7.0.0 && <9.4 , microlens >=0.4 , microlens-platform >=0.2.3.1 , mtl >=2.2.1@@ -94,10 +97,10 @@ , unix , vector , vty >=5.15+ default-language: Haskell2010 if os(windows) buildable: False else buildable: True if flag(threaded) ghc-options: -threaded- default-language: Haskell2010
hledger-ui.info view
@@ -12,7 +12,7 @@ ************* hledger-ui is a terminal interface (TUI) for the hledger accounting-tool. This manual is for hledger-ui 1.27.+tool. This manual is for hledger-ui 1.28. 'hledger-ui [OPTIONS] [QUERYARGS]' 'hledger ui -- [OPTIONS] [QUERYARGS]'@@ -68,6 +68,18 @@ '--theme=default|terminal|greenterm' use this custom display theme+'--menu'++ start in the menu screen+'--all'++ start in the all accounts screen+'--bs'++ start in the balance sheet accounts screen+'--is'++ start in the income statement accounts screen '--register=ACCTREGEX' start in the (first) matched account's register screen@@ -250,8 +262,7 @@ * Use mouse wheel or trackpad to scroll up and down * Click on list items to go deeper- * Click on the left margin (column 0), or the blank area at bottom of- screen, to go back.+ * Click on the left margin (column 0) to go back. File: hledger-ui.info, Node: KEYS, Next: SCREENS, Prev: MOUSE, Up: Top@@ -266,13 +277,13 @@ 'ESCAPE', or 'LEFT', or 'q') to close it. The following keys work on most screens: - The cursor keys navigate: 'RIGHT' goes deeper, 'LEFT' returns to the-previous screen, 'UP'/'DOWN'/'PGUP'/'PGDN'/'HOME'/'END' move up and down-through lists. Emacs-style ('CTRL-p'/'CTRL-n'/'CTRL-f'/'CTRL-b')-movement keys are also supported (but not vi-style keys, since-hledger-1.19, sorry!). A tip: movement speed is limited by your-keyboard repeat rate, to move faster you may want to adjust it. (If-you're on a mac, the karabiner app is one way to do that.)+ The cursor keys navigate: 'RIGHT' or 'ENTER' goes deeper, 'LEFT'+returns to the previous screen, 'UP'/'DOWN'/'PGUP'/'PGDN'/'HOME'/'END'+move up and down through lists. Emacs-style+('CTRL-p'/'CTRL-n'/'CTRL-f'/'CTRL-b') and VI-style ('k','j','l','h')+movement keys are also supported. A tip: movement speed is limited by+your keyboard repeat rate, to move faster you may want to adjust it.+(If you're on a mac, the karabiner app is one way to do that.) With shift pressed, the cursor keys adjust the report period, limiting the transactions to be shown (by default, all are shown).@@ -352,27 +363,82 @@ 4 SCREENS ********* +hledger-ui shows several different screens, described below. It shows+the "Balance sheet accounts" screen to start with, except in the+following situations:++ * If no asset/liability/equity accounts can be detected, or if an+ account query has been given on the command line, it starts in the+ "All accounts" screen.++ * If a starting screen is specified with -menu/-all/-bs/-is/-register+ on the command line, it starts in that screen.++ From any screen you can press 'LEFT' or 'ESC' to navigate back to the+top level "Menu" screen.+ * Menu: -* Accounts screen::-* Register screen::-* Transaction screen::-* Error screen::+* Menu::+* All accounts::+* Balance sheet accounts::+* Income statement accounts::+* Register::+* Transaction::+* Error:: -File: hledger-ui.info, Node: Accounts screen, Next: Register screen, Up: SCREENS+File: hledger-ui.info, Node: Menu, Next: All accounts, Up: SCREENS -4.1 Accounts screen-===================+4.1 Menu+======== -This is normally the first screen displayed. It lists accounts and-their balances, like hledger's balance command. By default, it shows-all accounts and their latest ending balances (including the balances of-subaccounts). Accounts which have been declared with an account-directive are also listed, even if not yet used (except for empty parent-accounts). If you specify a query on the command line, it shows just-the matched accounts and the balances from matched transactions.+The top-most screen. From here you can navigate to three accounts+screens: ++File: hledger-ui.info, Node: All accounts, Next: Balance sheet accounts, Prev: Menu, Up: SCREENS++4.2 All accounts+================++This screen shows all accounts (possibly filtered by a query), and their+end balances on the date shown in the title bar (or their balance+changes in the period shown in the title bar, toggleable with 'H'). It+is like the 'hledger balance' command.+++File: hledger-ui.info, Node: Balance sheet accounts, Next: Income statement accounts, Prev: All accounts, Up: SCREENS++4.3 Balance sheet accounts+==========================++This screen shows asset, liability and equity accounts, if these can be+detected (see account types). It always shows end balances. It is like+the 'hledger balancesheetequity' command.+++File: hledger-ui.info, Node: Income statement accounts, Next: Register, Prev: Balance sheet accounts, Up: SCREENS++4.4 Income statement accounts+=============================++This screen shows revenue and expense accounts. It always shows balance+changes. It is like the 'hledger incomestatement' command.++ All of these accounts screens work in much the same way:++ They show accounts which have been posted to by transactions, as well+as accounts which have been declared with an account directive (except+for empty parent accounts).++ If you specify a query on the command line or with '/' in the app,+they show just the matched accounts, and the balances from matched+transactions.++ hledger-ui shows accounts with zero balances by default (unlike+command-line hledger). To hide these, press 'z' to toggle nonzero mode.+ Account names are shown as a flat list by default; press 't' to toggle tree mode. In list mode, account balances are exclusive of subaccounts, except where subaccounts are hidden by a depth limit (see@@ -385,15 +451,16 @@ To remove the depth limit, set it higher than the maximum account depth, or press 'ESCAPE'. - 'H' toggles between showing historical balances or period balances.-Historical balances (the default) are ending balances at the end of the-report period, taking into account all transactions before that date-(filtered by the filter query if any), including transactions before the-start of the report period. In other words, historical balances are-what you would see on a bank statement for that account (unless-disturbed by a filter query). Period balances ignore transactions-before the report start date, so they show the change in balance during-the report period. They are more useful eg when viewing a time log.+ 'H' toggles between showing historical balances or period balances+(on the "All accounts" screen). Historical balances (the default) are+ending balances at the end of the report period, taking into account all+transactions before that date (filtered by the filter query if any),+including transactions before the start of the report period. In other+words, historical balances are what you would see on a bank statement+for that account (unless disturbed by a filter query). Period balances+ignore transactions before the report start date, so they show the+change in balance during the report period. They are more useful eg+when viewing a time log. 'U' toggles filtering by unmarked status, including or excluding unmarked postings in the balances. Similarly, 'P' toggles pending@@ -404,17 +471,14 @@ 'R' toggles real mode, in which virtual postings are ignored. - 'z' toggles nonzero mode, in which only accounts with nonzero-balances are shown (hledger-ui shows zero items by default, unlike-command-line hledger).-- Press 'RIGHT' to view an account's transactions register.+ Press 'RIGHT' to view an account's register screen, Or, 'LEFT' to see+the menu screen. -File: hledger-ui.info, Node: Register screen, Next: Transaction screen, Prev: Accounts screen, Up: SCREENS+File: hledger-ui.info, Node: Register, Next: Transaction, Prev: Income statement accounts, Up: SCREENS -4.2 Register screen-===================+4.5 Register+============ This screen shows the transactions affecting a particular account, like a check register. Each line represents one transaction and shows:@@ -457,10 +521,10 @@ Press 'RIGHT' to view the selected transaction in detail. -File: hledger-ui.info, Node: Transaction screen, Next: Error screen, Prev: Register screen, Up: SCREENS+File: hledger-ui.info, Node: Transaction, Next: Error, Prev: Register, Up: SCREENS -4.3 Transaction screen-======================+4.6 Transaction+=============== This screen shows a single transaction, as a general journal entry, similar to hledger's print command and journal format@@ -481,10 +545,10 @@ reload). -File: hledger-ui.info, Node: Error screen, Prev: Transaction screen, Up: SCREENS+File: hledger-ui.info, Node: Error, Prev: Transaction, Up: SCREENS -4.4 Error screen-================+4.7 Error+========= This screen will appear if there is a problem, such as a parse error, when you press g to reload. Once you have fixed the problem, press g@@ -500,10 +564,10 @@ * Menu: * Watch mode::-* Watch mode limitations::+* Debug output:: -File: hledger-ui.info, Node: Watch mode, Next: Watch mode limitations, Up: TIPS+File: hledger-ui.info, Node: Watch mode, Next: Debug output, Up: TIPS 5.1 Watch mode ==============@@ -525,35 +589,29 @@ hledger-ui when needed, eg to toggle cleared mode, or to explore the history. --File: hledger-ui.info, Node: Watch mode limitations, Prev: Watch mode, Up: TIPS--5.2 Watch mode limitations-==========================--There are situations in which it won't work, ie the display will not-update when you save a change (because the underlying 'inotify' library-does not support it). Here are some that we know of:+ Here are some current limitations to be aware of: - * Certain editors: saving with 'gedit', and perhaps any Gnome- application, won't be detected (#1617). Jetbrains IDEs, such as- IDEA, also may not work (#911).+ Changes might not be detected with certain editors, possibly+including Jetbrains IDEs, 'gedit', other Gnome applications; or on+certain unusual filesystems. (#1617, #911). To work around, reload+manually by pressing 'g' in the hledger-ui window. (Or see #1617 for+another workaround, and let us know if it works for you.) - * Certain unusual filesystems might not be supported. (All the usual- ones on unix, mac and windows are supported.)+ CPU and memory usage can sometimes gradually increase, if 'hledger-ui+--watch' is left running for days. (Possibly correlated with certain+platforms, many transactions, and/or large numbers of other files+present). To work around, 'q'uit and restart it, or (where supported)+suspend ('CTRL-z') and restart it ('fg'). - In such cases, the workaround is to switch to the hledger-ui window-and press 'g' each time you want it to reload. (Actually, see #1617 for-another workaround, and let us know if it works for you.)++File: hledger-ui.info, Node: Debug output, Prev: Watch mode, Up: TIPS - If you leave 'hledger-ui --watch' running for days, on certain-platforms (?), perhaps with many transactions in your journal (?),-perhaps with large numbers of other files present (?), you may see it-gradually using more and more memory and CPU over time, as seen in 'top'-or Activity Monitor or Task Manager.+5.2 Debug output+================ - A workaround is to 'q'uit and restart it, or to suspend it ('CTRL-z')-and restart it ('fg') if your shell supports that.+You can add '--debug[=N]' to the command line to log debug output. This+will be logged to the file 'hledger-ui.log' in the current directory. N+ranges from 1 (least output, the default) to 9 (maximum output). File: hledger-ui.info, Node: ENVIRONMENT, Next: FILES, Prev: TIPS, Up: Top@@ -642,32 +700,38 @@ Node: Top221 Node: OPTIONS1654 Ref: #options1752-Node: MOUSE6634-Ref: #mouse6729-Node: KEYS7011-Ref: #keys7104-Node: SCREENS11190-Ref: #screens11288-Node: Accounts screen11378-Ref: #accounts-screen11506-Node: Register screen13845-Ref: #register-screen14000-Node: Transaction screen15984-Ref: #transaction-screen16142-Node: Error screen17012-Ref: #error-screen17134-Node: TIPS17378-Ref: #tips17477-Node: Watch mode17529-Ref: #watch-mode17646-Node: Watch mode limitations18396-Ref: #watch-mode-limitations18537-Node: ENVIRONMENT19673-Ref: #environment19784-Node: FILES21169-Ref: #files21268-Node: BUGS21481-Ref: #bugs21558+Node: MOUSE6836+Ref: #mouse6931+Node: KEYS7168+Ref: #keys7261+Node: SCREENS11336+Ref: #screens11434+Node: Menu12120+Ref: #menu12212+Node: All accounts12289+Ref: #all-accounts12428+Node: Balance sheet accounts12679+Ref: #balance-sheet-accounts12859+Node: Income statement accounts13047+Ref: #income-statement-accounts13229+Node: Register15649+Ref: #register15786+Node: Transaction17770+Ref: #transaction17893+Node: Error18763+Ref: #error18857+Node: TIPS19101+Ref: #tips19200+Node: Watch mode19242+Ref: #watch-mode19349+Node: Debug output20805+Ref: #debug-output20916+Node: ENVIRONMENT21128+Ref: #environment21239+Node: FILES22624+Ref: #files22723+Node: BUGS22936+Ref: #bugs23013 End Tag Table
hledger-ui.txt view
@@ -5,7 +5,7 @@ NAME hledger-ui is a terminal interface (TUI) for the hledger accounting- tool. This manual is for hledger-ui 1.27.+ tool. This manual is for hledger-ui 1.28. SYNOPSIS hledger-ui [OPTIONS] [QUERYARGS]@@ -47,6 +47,14 @@ --theme=default|terminal|greenterm use this custom display theme + --menu start in the menu screen++ --all start in the all accounts screen++ --bs start in the balance sheet accounts screen++ --is start in the income statement accounts screen+ --register=ACCTREGEX start in the (first) matched account's register screen @@ -219,88 +227,87 @@ o Click on list items to go deeper - o Click on the left margin (column 0), or the blank area at bottom of- screen, to go back.+ o Click on the left margin (column 0) to go back. KEYS Keyboard gives more control. - ? shows a help dialog listing all keys. (Some of these also appear in+ ? shows a help dialog listing all keys. (Some of these also appear in the quick help at the bottom of each screen.) Press ? again (or ESCAPE, or LEFT, or q) to close it. The following keys work on most screens: - The cursor keys navigate: RIGHT goes deeper, LEFT returns to the previ-- ous screen, UP/DOWN/PGUP/PGDN/HOME/END move up and down through lists.- Emacs-style (CTRL-p/CTRL-n/CTRL-f/CTRL-b) movement keys are also sup-- ported (but not vi-style keys, since hledger-1.19, sorry!). A tip:- movement speed is limited by your keyboard repeat rate, to move faster- you may want to adjust it. (If you're on a mac, the karabiner app is- one way to do that.)+ The cursor keys navigate: RIGHT or ENTER goes deeper, LEFT returns to+ the previous screen, UP/DOWN/PGUP/PGDN/HOME/END move up and down+ through lists. Emacs-style (CTRL-p/CTRL-n/CTRL-f/CTRL-b) and VI-style+ (k,j,l,h) movement keys are also supported. A tip: movement speed is+ limited by your keyboard repeat rate, to move faster you may want to+ adjust it. (If you're on a mac, the karabiner app is one way to do+ that.) - With shift pressed, the cursor keys adjust the report period, limiting- the transactions to be shown (by default, all are shown). SHIFT-- DOWN/UP steps downward and upward through these standard report period- durations: year, quarter, month, week, day. Then, SHIFT-LEFT/RIGHT- moves to the previous/next period. T sets the report period to today.- With the -w/--watch option, when viewing a "current" period (the cur-+ With shift pressed, the cursor keys adjust the report period, limiting+ the transactions to be shown (by default, all are shown). SHIFT-+ DOWN/UP steps downward and upward through these standard report period+ durations: year, quarter, month, week, day. Then, SHIFT-LEFT/RIGHT+ moves to the previous/next period. T sets the report period to today.+ With the -w/--watch option, when viewing a "current" period (the cur- rent day, week, month, quarter, or year), the period will move automat-- ically to track the current date. To set a non-standard period, you+ ically to track the current date. To set a non-standard period, you can use / and a date: query. - / lets you set a general filter query limiting the data shown, using- the same query terms as in hledger and hledger-web. While editing the- query, you can use CTRL-a/e/d/k, BS, cursor keys; press ENTER to set+ / lets you set a general filter query limiting the data shown, using+ the same query terms as in hledger and hledger-web. While editing the+ query, you can use CTRL-a/e/d/k, BS, cursor keys; press ENTER to set it, or ESCAPEto cancel. There are also keys for quickly adjusting some- common filters like account depth and transaction status (see below).+ common filters like account depth and transaction status (see below). BACKSPACE or DELETE removes all filters, showing all transactions. - As mentioned above, by default hledger-ui hides future transactions -+ As mentioned above, by default hledger-ui hides future transactions - both ordinary transactions recorded in the journal, and periodic trans-- actions generated by rule. F toggles forecast mode, in which+ actions generated by rule. F toggles forecast mode, in which future/forecasted transactions are shown. - ESCAPE resets the UI state and jumps back to the top screen, restoring- the app's initial state at startup. Or, it cancels minibuffer data+ ESCAPE resets the UI state and jumps back to the top screen, restoring+ the app's initial state at startup. Or, it cancels minibuffer data entry or the help dialog. CTRL-l redraws the screen and centers the selection if possible (selec-- tions near the top won't be centered, since we don't scroll above the+ tions near the top won't be centered, since we don't scroll above the top). - g reloads from the data file(s) and updates the current screen and any- previous screens. (With large files, this could cause a noticeable+ g reloads from the data file(s) and updates the current screen and any+ previous screens. (With large files, this could cause a noticeable pause.) - I toggles balance assertion checking. Disabling balance assertions+ I toggles balance assertion checking. Disabling balance assertions temporarily can be useful for troubleshooting. - a runs command-line hledger's add command, and reloads the updated+ a runs command-line hledger's add command, and reloads the updated file. This allows some basic data entry. - A is like a, but runs the hledger-iadd tool, which provides a terminal- interface. This key will be available if hledger-iadd is installed in+ A is like a, but runs the hledger-iadd tool, which provides a terminal+ interface. This key will be available if hledger-iadd is installed in $path. - E runs $HLEDGER_UI_EDITOR, or $EDITOR, or a default (emacsclient -a ""- -nw) on the journal file. With some editors (emacs, vi), the cursor- will be positioned at the current transaction when invoked from the- register and transaction screens, and at the error location (if possi-+ E runs $HLEDGER_UI_EDITOR, or $EDITOR, or a default (emacsclient -a ""+ -nw) on the journal file. With some editors (emacs, vi), the cursor+ will be positioned at the current transaction when invoked from the+ register and transaction screens, and at the error location (if possi- ble) when invoked from the error screen. - B toggles cost mode, showing amounts in their transaction price's com-+ B toggles cost mode, showing amounts in their transaction price's com- modity (like toggling the -B/--cost flag). - V toggles value mode, showing amounts' current market value in their- default valuation commodity (like toggling the -V/--market flag).- Note, "current market value" means the value on the report end date if- specified, otherwise today. To see the value on another date, you can- temporarily set that as the report end date. Eg: to see a transaction- as it was valued on july 30, go to the accounts or register screen,+ V toggles value mode, showing amounts' current market value in their+ default valuation commodity (like toggling the -V/--market flag).+ Note, "current market value" means the value on the report end date if+ specified, otherwise today. To see the value on another date, you can+ temporarily set that as the report end date. Eg: to see a transaction+ as it was valued on july 30, go to the accounts or register screen, press /, and add date:-7/30 to the query. At most one of cost or value mode can be active at once. - There's not yet any visual reminder when cost or value mode is active;+ There's not yet any visual reminder when cost or value mode is active; for now pressing b b v should reliably reset to normal mode. q quits the application.@@ -308,36 +315,74 @@ Additional screen-specific keys are described below. SCREENS- Accounts screen- This is normally the first screen displayed. It lists accounts and- their balances, like hledger's balance command. By default, it shows- all accounts and their latest ending balances (including the balances- of subaccounts). Accounts which have been declared with an account- directive are also listed, even if not yet used (except for empty par-- ent accounts). If you specify a query on the command line, it shows- just the matched accounts and the balances from matched transactions.+ hledger-ui shows several different screens, described below. It shows+ the "Balance sheet accounts" screen to start with, except in the fol-+ lowing situations: - Account names are shown as a flat list by default; press t to toggle- tree mode. In list mode, account balances are exclusive of subac-- counts, except where subaccounts are hidden by a depth limit (see- below). In tree mode, all account balances are inclusive of subac-+ o If no asset/liability/equity accounts can be detected, or if an+ account query has been given on the command line, it starts in the+ "All accounts" screen.++ o If a starting screen is specified with --menu/--all/--bs/--is/--reg-+ ister on the command line, it starts in that screen.++ From any screen you can press LEFT or ESC to navigate back to the top+ level "Menu" screen.++ Menu+ The top-most screen. From here you can navigate to three accounts+ screens:++ All accounts+ This screen shows all accounts (possibly filtered by a query), and+ their end balances on the date shown in the title bar (or their balance+ changes in the period shown in the title bar, toggleable with H). It+ is like the hledger balance command.++ Balance sheet accounts+ This screen shows asset, liability and equity accounts, if these can be+ detected (see account types). It always shows end balances. It is+ like the hledger balancesheetequity command.++ Income statement accounts+ This screen shows revenue and expense accounts. It always shows bal-+ ance changes. It is like the hledger incomestatement command.++ All of these accounts screens work in much the same way:++ They show accounts which have been posted to by transactions, as well+ as accounts which have been declared with an account directive (except+ for empty parent accounts).++ If you specify a query on the command line or with / in the app, they+ show just the matched accounts, and the balances from matched transac-+ tions.++ hledger-ui shows accounts with zero balances by default (unlike com-+ mand-line hledger). To hide these, press z to toggle nonzero mode.++ Account names are shown as a flat list by default; press t to toggle+ tree mode. In list mode, account balances are exclusive of subac-+ counts, except where subaccounts are hidden by a depth limit (see+ below). In tree mode, all account balances are inclusive of subac- counts. - To see less detail, press a number key, 1 to 9, to set a depth limit.+ To see less detail, press a number key, 1 to 9, to set a depth limit. Or use - to decrease and +/= to increase the depth limit. 0 shows even- less detail, collapsing all accounts to a single total. To remove the- depth limit, set it higher than the maximum account depth, or press+ less detail, collapsing all accounts to a single total. To remove the+ depth limit, set it higher than the maximum account depth, or press ESCAPE. - H toggles between showing historical balances or period balances. His-- torical balances (the default) are ending balances at the end of the- report period, taking into account all transactions before that date- (filtered by the filter query if any), including transactions before- the start of the report period. In other words, historical balances- are what you would see on a bank statement for that account (unless- disturbed by a filter query). Period balances ignore transactions- before the report start date, so they show the change in balance during- the report period. They are more useful eg when viewing a time log.+ H toggles between showing historical balances or period balances (on+ the "All accounts" screen). Historical balances (the default) are end-+ ing balances at the end of the report period, taking into account all+ transactions before that date (filtered by the filter query if any),+ including transactions before the start of the report period. In other+ words, historical balances are what you would see on a bank statement+ for that account (unless disturbed by a filter query). Period balances+ ignore transactions before the report start date, so they show the+ change in balance during the report period. They are more useful eg+ when viewing a time log. U toggles filtering by unmarked status, including or excluding unmarked postings in the balances. Similarly, P toggles pending postings, and C@@ -347,119 +392,110 @@ R toggles real mode, in which virtual postings are ignored. - z toggles nonzero mode, in which only accounts with nonzero balances- are shown (hledger-ui shows zero items by default, unlike command-line- hledger).-- Press RIGHT to view an account's transactions register.+ Press RIGHT to view an account's register screen, Or, LEFT to see the+ menu screen. - Register screen+ Register This screen shows the transactions affecting a particular account, like a check register. Each line represents one transaction and shows: - o the other account(s) involved, in abbreviated form. (If there are- both real and virtual postings, it shows only the accounts affected+ o the other account(s) involved, in abbreviated form. (If there are+ both real and virtual postings, it shows only the accounts affected by real postings.) - o the overall change to the current account's balance; positive for an+ o the overall change to the current account's balance; positive for an inflow to this account, negative for an outflow. o the running historical total or period total for the current account,- after the transaction. This can be toggled with H. Similar to the- accounts screen, the historical total is affected by transactions- (filtered by the filter query) before the report start date, while+ after the transaction. This can be toggled with H. Similar to the+ accounts screen, the historical total is affected by transactions+ (filtered by the filter query) before the report start date, while the period total is not. If the historical total is not disturbed by- a filter query, it will be the running historical balance you would+ a filter query, it will be the running historical balance you would see on a bank register for the current account. - Transactions affecting this account's subaccounts will be included in+ Transactions affecting this account's subaccounts will be included in the register if the accounts screen is in tree mode, or if it's in list- mode but this account has subaccounts which are not shown due to a- depth limit. In other words, the register always shows the transac-- tions contributing to the balance shown on the accounts screen. Tree+ mode but this account has subaccounts which are not shown due to a+ depth limit. In other words, the register always shows the transac-+ tions contributing to the balance shown on the accounts screen. Tree mode/list mode can be toggled with t here also. - U toggles filtering by unmarked status, showing or hiding unmarked+ U toggles filtering by unmarked status, showing or hiding unmarked transactions. Similarly, P toggles pending transactions, and C toggles- cleared transactions. (By default, transactions with all statuses are- shown; if you activate one or two status filters, only those transac-+ cleared transactions. (By default, transactions with all statuses are+ shown; if you activate one or two status filters, only those transac- tions are shown; and if you activate all three, the filter is removed.) R toggles real mode, in which virtual postings are ignored. - z toggles nonzero mode, in which only transactions posting a nonzero- change are shown (hledger-ui shows zero items by default, unlike com-+ z toggles nonzero mode, in which only transactions posting a nonzero+ change are shown (hledger-ui shows zero items by default, unlike com- mand-line hledger). Press RIGHT to view the selected transaction in detail. - Transaction screen- This screen shows a single transaction, as a general journal entry,- similar to hledger's print command and journal format (hledger_jour-+ Transaction+ This screen shows a single transaction, as a general journal entry,+ similar to hledger's print command and journal format (hledger_jour- nal(5)). - The transaction's date(s) and any cleared flag, transaction code,- description, comments, along with all of its account postings are- shown. Simple transactions have two postings, but there can be more+ The transaction's date(s) and any cleared flag, transaction code,+ description, comments, along with all of its account postings are+ shown. Simple transactions have two postings, but there can be more (or in certain cases, fewer). - UP and DOWN will step through all transactions listed in the previous- account register screen. In the title bar, the numbers in parentheses- show your position within that account register. They will vary+ UP and DOWN will step through all transactions listed in the previous+ account register screen. In the title bar, the numbers in parentheses+ show your position within that account register. They will vary depending on which account register you came from (remember most trans- actions appear in multiple account registers). The #N number preceding them is the transaction's position within the complete unfiltered jour- nal, which is a more stable id (at least until the next reload). - Error screen- This screen will appear if there is a problem, such as a parse error,- when you press g to reload. Once you have fixed the problem, press g+ Error+ This screen will appear if there is a problem, such as a parse error,+ when you press g to reload. Once you have fixed the problem, press g again to reload and resume normal operation. (Or, you can press escape to cancel the reload attempt.) TIPS Watch mode- One of hledger-ui's best features is the auto-reloading -w/--watch- mode. With this flag, it will update the display automatically when-+ One of hledger-ui's best features is the auto-reloading -w/--watch+ mode. With this flag, it will update the display automatically when- ever changes are saved to the data files. - This is very useful when reconciling. A good workflow is to have your- bank's online register open in a browser window, for reference; the- journal file open in an editor window; and hledger-ui in watch mode in+ This is very useful when reconciling. A good workflow is to have your+ bank's online register open in a browser window, for reference; the+ journal file open in an editor window; and hledger-ui in watch mode in a terminal window, eg: $ hledger-ui --watch --register checking -C - As you mark things cleared in the editor, you can see the effect imme-- diately without having to context switch. This leaves more mental- bandwidth for your accounting. Of course you can still interact with- hledger-ui when needed, eg to toggle cleared mode, or to explore the+ As you mark things cleared in the editor, you can see the effect imme-+ diately without having to context switch. This leaves more mental+ bandwidth for your accounting. Of course you can still interact with+ hledger-ui when needed, eg to toggle cleared mode, or to explore the history. - Watch mode limitations- There are situations in which it won't work, ie the display will not- update when you save a change (because the underlying inotify library- does not support it). Here are some that we know of:-- o Certain editors: saving with gedit, and perhaps any Gnome applica-- tion, won't be detected (#1617). Jetbrains IDEs, such as IDEA, also- may not work (#911).-- o Certain unusual filesystems might not be supported. (All the usual- ones on unix, mac and windows are supported.)+ Here are some current limitations to be aware of: - In such cases, the workaround is to switch to the hledger-ui window and- press g each time you want it to reload. (Actually, see #1617 for- another workaround, and let us know if it works for you.)+ Changes might not be detected with certain editors, possibly including+ Jetbrains IDEs, gedit, other Gnome applications; or on certain unusual+ filesystems. (#1617, #911). To work around, reload manually by press-+ ing g in the hledger-ui window. (Or see #1617 for another workaround,+ and let us know if it works for you.) - If you leave hledger-ui --watch running for days, on certain platforms- (?), perhaps with many transactions in your journal (?), perhaps with- large numbers of other files present (?), you may see it gradually- using more and more memory and CPU over time, as seen in top or Activ-- ity Monitor or Task Manager.+ CPU and memory usage can sometimes gradually increase, if hledger-ui+ --watch is left running for days. (Possibly correlated with certain+ platforms, many transactions, and/or large numbers of other files+ present). To work around, quit and restart it, or (where supported)+ suspend (CTRL-z) and restart it (fg). - A workaround is to quit and restart it, or to suspend it (CTRL-z) and- restart it (fg) if your shell supports that.+ Debug output+ You can add --debug[=N] to the command line to log debug output. This+ will be logged to the file hledger-ui.log in the current directory. N+ ranges from 1 (least output, the default) to 9 (maximum output). ENVIRONMENT COLUMNS The screen width to use. Default: the full terminal width.@@ -468,17 +504,17 @@ On unix computers, the default value is: ~/.hledger.journal. - A more typical value is something like ~/finance/YYYY.journal, where- ~/finance is a version-controlled finance directory and YYYY is the- current year. Or, ~/finance/current.journal, where current.journal is+ A more typical value is something like ~/finance/YYYY.journal, where+ ~/finance is a version-controlled finance directory and YYYY is the+ current year. Or, ~/finance/current.journal, where current.journal is a symbolic link to YYYY.journal. - The usual way to set this permanently is to add a command to one of+ The usual way to set this permanently is to add a command to one of your shell's startup files (eg ~/.profile): export LEDGER_FILE=~/finance/current.journal` - On some Mac computers, there is a more thorough way to set environment+ On some Mac computers, there is a more thorough way to set environment variables, that will also affect applications started from the GUI (eg, Emacs started from a dock icon): In ~/.MacOSX/environment.plist, add an entry like:@@ -489,24 +525,24 @@ For this to take effect you might need to killall Dock, or reboot. - On Windows computers, the default value is probably C:\Users\YOUR-- NAME\.hledger.journal. You can change this by running a command like- this in a powershell window (let us know if you need to be an Adminis-+ On Windows computers, the default value is probably C:\Users\YOUR-+ NAME\.hledger.journal. You can change this by running a command like+ this in a powershell window (let us know if you need to be an Adminis- trator, and if this persists across a reboot): > setx LEDGER_FILE "C:\Users\MyUserName\finance\2021.journal" - Or, change it in settings: see https://www.java.com/en/down-+ Or, change it in settings: see https://www.java.com/en/down- load/help/path.html. FILES- Reads data from one or more files in hledger journal, timeclock, time-- dot, or CSV format specified with -f, or $LEDGER_FILE, or- $HOME/.hledger.journal (on windows, perhaps+ Reads data from one or more files in hledger journal, timeclock, time-+ dot, or CSV format specified with -f, or $LEDGER_FILE, or+ $HOME/.hledger.journal (on windows, perhaps C:/Users/USER/.hledger.journal). BUGS- The need to precede options with -- when invoked from hledger is awk-+ The need to precede options with -- when invoked from hledger is awk- ward. -f- doesn't work (hledger-ui can't read from stdin).@@ -514,13 +550,13 @@ -V affects only the accounts screen. When you press g, the current and all previous screens are regenerated,- which may cause a noticeable pause with large files. Also there is no+ which may cause a noticeable pause with large files. Also there is no visual indication that this is in progress. - --watch is not yet fully robust. It works well for normal usage, but- many file changes in a short time (eg saving the file thousands of- times with an editor macro) can cause problems at least on OSX. Symp-- toms include: unresponsive UI, periodic resetting of the cursor posi-+ --watch is not yet fully robust. It works well for normal usage, but+ many file changes in a short time (eg saving the file thousands of+ times with an editor macro) can cause problems at least on OSX. Symp-+ toms include: unresponsive UI, periodic resetting of the cursor posi- tion, momentary display of parse errors, high CPU usage eventually sub- siding, and possibly a small but persistent build-up of CPU usage until the program is restarted.@@ -531,7 +567,7 @@ REPORTING BUGS- Report bugs at http://bugs.hledger.org (or on the #hledger IRC channel+ Report bugs at http://bugs.hledger.org (or on the #hledger IRC channel or hledger mail list) @@ -549,4 +585,4 @@ -hledger-ui-1.27 September 2022 HLEDGER-UI(1)+hledger-ui-1.28 December 2022 HLEDGER-UI(1)