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