1{- | UIState operations. -} 2 3{-# LANGUAGE NamedFieldPuns #-} 4{-# LANGUAGE OverloadedStrings #-} 5{-# LANGUAGE RecordWildCards #-} 6 7module Hledger.UI.UIState 8where 9 10import Brick.Widgets.Edit 11import Data.List 12import Data.Text.Zipper (gotoEOL) 13import Data.Time.Calendar (Day) 14import Data.Maybe (fromMaybe) 15 16import Hledger 17import Hledger.Cli.CliOptions 18import Hledger.UI.UITypes 19import Hledger.UI.UIOptions 20 21-- | Toggle between showing only unmarked items or all items. 22toggleUnmarked :: UIState -> UIState 23toggleUnmarked ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 24 ui{aopts=uopts{cliopts_=copts{reportopts_=reportOptsToggleStatusSomehow Unmarked copts ropts}}} 25 26-- | Toggle between showing only pending items or all items. 27togglePending :: UIState -> UIState 28togglePending ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 29 ui{aopts=uopts{cliopts_=copts{reportopts_=reportOptsToggleStatusSomehow Pending copts ropts}}} 30 31-- | Toggle between showing only cleared items or all items. 32toggleCleared :: UIState -> UIState 33toggleCleared ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 34 ui{aopts=uopts{cliopts_=copts{reportopts_=reportOptsToggleStatusSomehow Cleared copts ropts}}} 35 36-- TODO testing different status toggle styles 37 38-- | Generate zero or more indicators of the status filters currently active, 39-- which will be shown comma-separated as part of the indicators list. 40uiShowStatus :: CliOpts -> [Status] -> [String] 41uiShowStatus copts ss = 42 case style of 43 -- in style 2, instead of "Y, Z" show "not X" 44 Just 2 | length ss == numstatuses-1 45 -> map (("not "++). showstatus) $ sort $ complement ss -- should be just one 46 _ -> map showstatus $ sort ss 47 where 48 numstatuses = length [minBound..maxBound::Status] 49 style = maybeposintopt "status-toggles" $ rawopts_ copts 50 showstatus Cleared = "cleared" 51 showstatus Pending = "pending" 52 showstatus Unmarked = "unmarked" 53 54reportOptsToggleStatusSomehow :: Status -> CliOpts -> ReportOpts -> ReportOpts 55reportOptsToggleStatusSomehow s copts ropts = 56 case maybeposintopt "status-toggles" $ rawopts_ copts of 57 Just 2 -> reportOptsToggleStatus2 s ropts 58 Just 3 -> reportOptsToggleStatus3 s ropts 59-- Just 4 -> reportOptsToggleStatus4 s ropts 60-- Just 5 -> reportOptsToggleStatus5 s ropts 61 _ -> reportOptsToggleStatus1 s ropts 62 63-- 1 UPC toggles only X/all 64reportOptsToggleStatus1 s ropts@ReportOpts{statuses_=ss} 65 | ss == [s] = ropts{statuses_=[]} 66 | otherwise = ropts{statuses_=[s]} 67 68-- 2 UPC cycles X/not-X/all 69-- repeatedly pressing X cycles: 70-- [] U [u] 71-- [u] U [pc] 72-- [pc] U [] 73-- pressing Y after first or second step starts new cycle: 74-- [u] P [p] 75-- [pc] P [p] 76reportOptsToggleStatus2 s ropts@ReportOpts{statuses_=ss} 77 | ss == [s] = ropts{statuses_=complement [s]} 78 | ss == complement [s] = ropts{statuses_=[]} 79 | otherwise = ropts{statuses_=[s]} -- XXX assume only three values 80 81-- 3 UPC toggles each X 82reportOptsToggleStatus3 s ropts@ReportOpts{statuses_=ss} 83 | s `elem` ss = ropts{statuses_=filter (/= s) ss} 84 | otherwise = ropts{statuses_=simplifyStatuses (s:ss)} 85 86-- 4 upc sets X, UPC sets not-X 87--reportOptsToggleStatus4 s ropts@ReportOpts{statuses_=ss} 88-- | s `elem` ss = ropts{statuses_=filter (/= s) ss} 89-- | otherwise = ropts{statuses_=simplifyStatuses (s:ss)} 90-- 91-- 5 upc toggles X, UPC toggles not-X 92--reportOptsToggleStatus5 s ropts@ReportOpts{statuses_=ss} 93-- | s `elem` ss = ropts{statuses_=filter (/= s) ss} 94-- | otherwise = ropts{statuses_=simplifyStatuses (s:ss)} 95 96-- | Given a list of unique enum values, list the other possible values of that enum. 97complement :: (Bounded a, Enum a, Eq a) => [a] -> [a] 98complement = ([minBound..maxBound] \\) 99 100-- 101 102-- | Toggle between showing all and showing only nonempty (more precisely, nonzero) items. 103toggleEmpty :: UIState -> UIState 104toggleEmpty ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 105 ui{aopts=uopts{cliopts_=copts{reportopts_=toggleEmpty ropts}}} 106 where 107 toggleEmpty ropts = ropts{empty_=not $ empty_ ropts} 108 109-- | Show primary amounts, not cost or value. 110clearCostValue :: UIState -> UIState 111clearCostValue ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 112 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{value_ = plog "clearing value mode" Nothing}}}} 113 114-- | Toggle between showing the primary amounts or costs. 115toggleCost :: UIState -> UIState 116toggleCost ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 117 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{value_ = valuationToggleCost $ value_ ropts}}}} 118 119-- | Toggle between showing primary amounts or default valuation. 120toggleValue :: UIState -> UIState 121toggleValue ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 122 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{ 123 value_ = plog "toggling value mode to" $ valuationToggleValue $ value_ ropts}}}} 124 125-- | Basic toggling of -B/cost, for hledger-ui. 126valuationToggleCost :: Maybe ValuationType -> Maybe ValuationType 127valuationToggleCost (Just (AtCost _)) = Nothing 128valuationToggleCost _ = Just $ AtCost Nothing 129 130-- | Basic toggling of -V, for hledger-ui. 131valuationToggleValue :: Maybe ValuationType -> Maybe ValuationType 132valuationToggleValue (Just (AtDefault _)) = Nothing 133valuationToggleValue _ = Just $ AtDefault Nothing 134 135-- | Set hierarchic account tree mode. 136setTree :: UIState -> UIState 137setTree ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 138 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{accountlistmode_=ALTree}}}} 139 140-- | Set flat account list mode. 141setList :: UIState -> UIState 142setList ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 143 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{accountlistmode_=ALFlat}}}} 144 145-- | Toggle between flat and tree mode. If current mode is unspecified/default, assume it's flat. 146toggleTree :: UIState -> UIState 147toggleTree ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 148 ui{aopts=uopts{cliopts_=copts{reportopts_=toggleTreeMode ropts}}} 149 where 150 toggleTreeMode ropts 151 | accountlistmode_ ropts == ALTree = ropts{accountlistmode_=ALFlat} 152 | otherwise = ropts{accountlistmode_=ALTree} 153 154-- | Toggle between historical balances and period balances. 155toggleHistorical :: UIState -> UIState 156toggleHistorical ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 157 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{balancetype_=b}}}} 158 where 159 b | balancetype_ ropts == HistoricalBalance = PeriodChange 160 | otherwise = HistoricalBalance 161 162-- | Toggle hledger-ui's "forecast mode". In forecast mode, periodic 163-- transactions (generated by periodic rules) are enabled (as with 164-- hledger --forecast), and also future transactions in general 165-- (including non-periodic ones) are displayed. In normal mode, all 166-- future transactions (periodic or not) are suppressed (unlike 167-- command-line hledger). 168-- 169-- After toggling this, we do a full reload of the journal from disk 170-- to make it take effect; currently that's done in the callers (cf 171-- AccountsScreen, RegisterScreen) where it's easier. This is 172-- overkill, probably we should just hide/show the periodic 173-- transactions with a query for their special tag. 174-- 175toggleForecast :: Day -> UIState -> UIState 176toggleForecast d ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 177 ui{aopts=uopts{cliopts_=copts'}} 178 where 179 copts' = copts{reportopts_=ropts{forecast_=forecast'}} 180 forecast' = 181 case forecast_ ropts of 182 Just _ -> Nothing 183 Nothing -> Just $ fromMaybe nulldatespan $ forecastPeriodFromRawOpts d $ rawopts_ copts 184 185-- | Toggle between showing all and showing only real (non-virtual) items. 186toggleReal :: UIState -> UIState 187toggleReal ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 188 ui{aopts=uopts{cliopts_=copts{reportopts_=toggleReal ropts}}} 189 where 190 toggleReal ropts = ropts{real_=not $ real_ ropts} 191 192-- | Toggle the ignoring of balance assertions. 193toggleIgnoreBalanceAssertions :: UIState -> UIState 194toggleIgnoreBalanceAssertions ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{inputopts_=iopts}}} = 195 ui{aopts=uopts{cliopts_=copts{inputopts_=iopts{ignore_assertions_=not $ ignore_assertions_ iopts}}}} 196 197-- | Step through larger report periods, up to all. 198growReportPeriod :: Day -> UIState -> UIState 199growReportPeriod _d ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 200 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{period_=periodGrow $ period_ ropts}}}} 201 202-- | Step through smaller report periods, down to a day. 203shrinkReportPeriod :: Day -> UIState -> UIState 204shrinkReportPeriod d ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 205 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{period_=periodShrink d $ period_ ropts}}}} 206 207-- | Step the report start/end dates to the next period of same duration, 208-- remaining inside the given enclosing span. 209nextReportPeriod :: DateSpan -> UIState -> UIState 210nextReportPeriod enclosingspan ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts@ReportOpts{period_=p}}}} = 211 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{period_=periodNextIn enclosingspan p}}}} 212 213-- | Step the report start/end dates to the next period of same duration, 214-- remaining inside the given enclosing span. 215previousReportPeriod :: DateSpan -> UIState -> UIState 216previousReportPeriod enclosingspan ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts@ReportOpts{period_=p}}}} = 217 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{period_=periodPreviousIn enclosingspan p}}}} 218 219-- | If a standard report period is set, step it forward/backward if needed so that 220-- it encloses the given date. 221moveReportPeriodToDate :: Day -> UIState -> UIState 222moveReportPeriodToDate d ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts@ReportOpts{period_=p}}}} = 223 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{period_=periodMoveTo d p}}}} 224 225-- | Get the report period. 226reportPeriod :: UIState -> Period 227reportPeriod UIState{aopts=UIOpts{cliopts_=CliOpts{reportopts_=ReportOpts{period_=p}}}} = 228 p 229 230-- | Set the report period. 231setReportPeriod :: Period -> UIState -> UIState 232setReportPeriod p ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 233 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{period_=p}}}} 234 235-- | Clear any report period limits. 236resetReportPeriod :: UIState -> UIState 237resetReportPeriod = setReportPeriod PeriodAll 238 239-- | Apply a new filter query. 240setFilter :: String -> UIState -> UIState 241setFilter s ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 242 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{query_=s}}}} 243 244-- | Reset some filters & toggles. 245resetFilter :: UIState -> UIState 246resetFilter ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 247 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{ 248 empty_=True 249 ,statuses_=[] 250 ,real_=False 251 ,query_="" 252 --,period_=PeriodAll 253 }}}} 254 255-- | Reset all options state to exactly what it was at startup 256-- (preserving any command-line options/arguments). 257resetOpts :: UIState -> UIState 258resetOpts ui@UIState{astartupopts} = ui{aopts=astartupopts} 259 260resetDepth :: UIState -> UIState 261resetDepth ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} = 262 ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=Nothing}}}} 263 264-- | Get the maximum account depth in the current journal. 265maxDepth :: UIState -> Int 266maxDepth UIState{ajournal=j} = maximum $ map accountNameLevel $ journalAccountNames j 267 268-- | Decrement the current depth limit towards 0. If there was no depth limit, 269-- set it to one less than the maximum account depth. 270decDepth :: UIState -> UIState 271decDepth ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts@ReportOpts{..}}}} 272 = ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=dec depth_}}}} 273 where 274 dec (Just d) = Just $ max 0 (d-1) 275 dec Nothing = Just $ maxDepth ui - 1 276 277-- | Increment the current depth limit. If this makes it equal to the 278-- the maximum account depth, remove the depth limit. 279incDepth :: UIState -> UIState 280incDepth ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts@ReportOpts{..}}}} 281 = ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=inc depth_}}}} 282 where 283 inc (Just d) | d < (maxDepth ui - 1) = Just $ d+1 284 inc _ = Nothing 285 286-- | Set the current depth limit to the specified depth, or remove the depth limit. 287-- Also remove the depth limit if the specified depth is greater than the current 288-- maximum account depth. If the specified depth is negative, reset the depth limit 289-- to whatever was specified at uiartup. 290setDepth :: Maybe Int -> UIState -> UIState 291setDepth mdepth ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} 292 = ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=mdepth'}}}} 293 where 294 mdepth' = case mdepth of 295 Nothing -> Nothing 296 Just d | d < 0 -> depth_ ropts 297 | d >= maxDepth ui -> Nothing 298 | otherwise -> mdepth 299 300getDepth :: UIState -> Maybe Int 301getDepth UIState{aopts=UIOpts{cliopts_=CliOpts{reportopts_=ropts}}} = depth_ ropts 302 303-- | Open the minibuffer, setting its content to the current query with the cursor at the end. 304showMinibuffer :: UIState -> UIState 305showMinibuffer ui = setMode (Minibuffer e) ui 306 where 307 e = applyEdit gotoEOL $ editor MinibufferEditor (Just 1) oldq 308 oldq = query_ $ reportopts_ $ cliopts_ $ aopts ui 309 310-- | Close the minibuffer, discarding any edit in progress. 311closeMinibuffer :: UIState -> UIState 312closeMinibuffer = setMode Normal 313 314setMode :: Mode -> UIState -> UIState 315setMode m ui = ui{aMode=m} 316 317-- | Regenerate the content for the current and previous screens, from a new journal and current date. 318regenerateScreens :: Journal -> Day -> UIState -> UIState 319regenerateScreens j d ui@UIState{aScreen=s,aPrevScreens=ss} = 320 -- XXX clumsy due to entanglement of UIState and Screen. 321 -- sInit operates only on an appstate's current screen, so 322 -- remove all the screens from the appstate and then add them back 323 -- one at a time, regenerating as we go. 324 let 325 first:rest = reverse $ s:ss :: [Screen] 326 ui0 = ui{ajournal=j, aScreen=first, aPrevScreens=[]} :: UIState 327 328 ui1 = (sInit first) d False ui0 :: UIState 329 ui2 = foldl' (\ui s -> (sInit s) d False $ pushScreen s ui) ui1 rest :: UIState 330 in 331 ui2 332 333pushScreen :: Screen -> UIState -> UIState 334pushScreen scr ui = ui{aPrevScreens=(aScreen ui:aPrevScreens ui) 335 ,aScreen=scr 336 } 337 338popScreen :: UIState -> UIState 339popScreen ui@UIState{aPrevScreens=s:ss} = ui{aScreen=s, aPrevScreens=ss} 340popScreen ui = ui 341 342resetScreens :: Day -> UIState -> UIState 343resetScreens d ui@UIState{aScreen=s,aPrevScreens=ss} = 344 (sInit topscreen) d True $ 345 resetOpts $ 346 closeMinibuffer ui{aScreen=topscreen, aPrevScreens=[]} 347 where 348 topscreen = case ss of _:_ -> last ss 349 [] -> s 350 351-- | Enter a new screen, saving the old screen & state in the 352-- navigation history and initialising the new screen's state. 353screenEnter :: Day -> Screen -> UIState -> UIState 354screenEnter d scr ui = (sInit scr) d True $ 355 pushScreen scr 356 ui 357 358