File: AccountsScreen.hs

package info (click to toggle)
haskell-hledger-ui 1.32.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, trixie
  • size: 420 kB
  • sloc: haskell: 2,443; makefile: 5
file content (405 lines) | stat: -rw-r--r-- 20,718 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
-- The accounts screen, showing accounts and balances like the CLI balance command.

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}

module Hledger.UI.AccountsScreen
 (asNew
 ,asUpdate
 ,asDraw
 ,asDrawHelper
 ,asHandle
 ,handleHelpMode
 ,handleMinibufferMode
 ,asHandleNormalMode
 ,enterRegisterScreen
 ,asSetSelectedAccount
 )
where

import Brick
import Brick.Widgets.List
import Brick.Widgets.Edit
import Control.Monad
import Control.Monad.IO.Class (liftIO)
import Data.List hiding (reverse)
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 Text.DocLayout (realLength)

import Hledger
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.ErrorScreen (uiReloadJournal, uiCheckBalanceAssertions, uiReloadJournalIfChanged)
import Hledger.UI.RegisterScreen (rsCenterSelection)
import Data.Either (fromRight)
import Control.Arrow ((>>>))
import Safe (headDef)


asDraw :: UIState -> [Widget Name]
asDraw ui = dbgui "asDraw" $ asDrawHelper ui ropts' scrname
  where
    ropts' = _rsReportOpts $ reportspec_ $ uoCliOpts $ aopts ui
    scrname = "account " ++ if ishistorical then "balances" else "changes"
      where ishistorical = balanceaccum_ ropts' == Historical

-- | 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

            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

            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

            -- XXX how to minimise the balance column's jumping around as you change the depth limit ?

            colwidths | shortfall <= 0 = (preferredacctwidth, preferredbalwidth)
                      | otherwise      = (adjustedacctwidth, adjustedbalwidth)

          render $ defaultLayout toplabel bottomlabel $ renderList (asDrawItem colwidths) True (ass ^. assList)

          where
            ishistorical = balanceaccum_ ropts == Historical

            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

            bottomlabel = case mode of
                            Minibuffer label ed -> minibuffer label ed
                            _                   -> quickhelp
              where
                quickhelp = borderKeysStr' [
                   ("LEFT", str "back")
                  -- ,("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")
                  ,("?", str "help")
                  -- ,("q", str "quit")
                  ]

asDrawItem :: (Int,Int) -> Bool -> AccountsScreenItem -> Widget Name
asDrawItem (acctwidth, balwidth) selected AccountsScreenItem{..} =
  Widget Greedy Fixed $ do
    -- c <- getContext
      -- let showitem = intercalate "\n" . balanceReportItemAsText defreportopts fmt
    render $
      txt (fitText (Just acctwidth) (Just acctwidth) True True $ T.replicate (asItemIndentLevel) " " <> asItemDisplayAccountName) <+>
      txt balspace <+>
      splitAmounts balBuilder
      where
        balBuilder = maybe mempty showamt asItemMixedAmount
        showamt = showMixedAmountB oneLine{displayMinWidth=Just balwidth, displayMaxWidth=Just balwidth}
        balspace = T.replicate (2 + balwidth - wbWidth balBuilder) " "
        splitAmounts = foldr1 (<+>) . intersperse (str ", ") . map renderamt . T.splitOn ", " . wbToText
        renderamt :: T.Text -> Widget Name
        renderamt a | T.any (=='-') a = withAttr (sel $ attrName "list" <> attrName "balance" <> attrName "negative") $ txt a
                    | otherwise       = withAttr (sel $ attrName "list" <> attrName "balance" <> attrName "positive") $ txt a
        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
  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

-- | 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"

  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

  case ev of

    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

    -- 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'

    -- 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

    -- 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)

    -- 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

    -- 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)

    -- 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

    -- 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

    -- 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: 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}

    -- 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'}}

    -- 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 = rsNew uopts d j acct isdepthclipped
      where
        isdepthclipped = case getDepth ui of
                          Just de -> accountNameLevel acct >= de
                          Nothing -> False
    ui1 = pushScreen regscr ui
  rsCenterSelection ui1 >>= put'

-- | 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

-- | 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

isBlankItem mitem = ((asItemAccountName . snd) <$> mitem) == Just ""

asListSize = V.length . V.takeWhile ((/="").asItemAccountName) . listElements