mirror of
https://github.com/simonmichael/hledger.git
synced 2024-12-27 12:24:43 +03:00
ui: refactor: AppState -> UIState, cleanups
This commit is contained in:
parent
0851851ea9
commit
47a8eb53c8
@ -21,7 +21,7 @@ import qualified Data.Text as T
|
||||
import Data.Time.Calendar (Day)
|
||||
import System.FilePath (takeFileName)
|
||||
import qualified Data.Vector as V
|
||||
import Graphics.Vty as Vty
|
||||
import Graphics.Vty
|
||||
import Brick
|
||||
-- import Brick.Widgets.Center
|
||||
import Brick.Widgets.List
|
||||
@ -37,6 +37,7 @@ import Hledger.Cli hiding (progname,prognameandversion,green)
|
||||
import Hledger.UI.UIOptions
|
||||
-- import Hledger.UI.Theme
|
||||
import Hledger.UI.UITypes
|
||||
import Hledger.UI.UIState
|
||||
import Hledger.UI.UIUtils
|
||||
import Hledger.UI.RegisterScreen
|
||||
import Hledger.UI.ErrorScreen
|
||||
@ -50,13 +51,13 @@ accountsScreen = AccountsScreen{
|
||||
,_asSelectedAccount = ""
|
||||
}
|
||||
|
||||
asInit :: Day -> Bool -> AppState -> AppState
|
||||
asInit d reset st@AppState{
|
||||
asInit :: Day -> Bool -> UIState -> UIState
|
||||
asInit d reset ui@UIState{
|
||||
aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}},
|
||||
ajournal=j,
|
||||
aScreen=s@AccountsScreen{}
|
||||
} =
|
||||
st{aopts=uopts', aScreen=s & asList .~ newitems'}
|
||||
ui{aopts=uopts', aScreen=s & asList .~ newitems'}
|
||||
where
|
||||
newitems = list (Name "accounts") (V.fromList displayitems) 1
|
||||
|
||||
@ -103,8 +104,8 @@ asInit d reset st@AppState{
|
||||
|
||||
asInit _ _ _ = error "init function called with wrong screen type, should not happen"
|
||||
|
||||
asDraw :: AppState -> [Widget]
|
||||
asDraw AppState{aopts=UIOpts{cliopts_=CliOpts{reportopts_=ropts}}
|
||||
asDraw :: UIState -> [Widget]
|
||||
asDraw UIState{aopts=UIOpts{cliopts_=CliOpts{reportopts_=ropts}}
|
||||
,ajournal=j
|
||||
,aScreen=s@AccountsScreen{}
|
||||
,aMode=mode
|
||||
@ -230,8 +231,8 @@ asDrawItem (acctwidth, balwidth) selected AccountsScreenItem{..} =
|
||||
sel | selected = (<> "selected")
|
||||
| otherwise = id
|
||||
|
||||
asHandle :: AppState -> Vty.Event -> EventM (Next AppState)
|
||||
asHandle st'@AppState{
|
||||
asHandle :: UIState -> Event -> EventM (Next UIState)
|
||||
asHandle ui0@UIState{
|
||||
aScreen=scr@AccountsScreen{..}
|
||||
,aopts=UIOpts{cliopts_=copts}
|
||||
,ajournal=j
|
||||
@ -247,62 +248,62 @@ asHandle st'@AppState{
|
||||
selacct = case listSelectedElement $ scr ^. asList of
|
||||
Just (_, AccountsScreenItem{..}) -> asItemAccountName
|
||||
Nothing -> scr ^. asSelectedAccount
|
||||
st = st'{aScreen=scr & asSelectedAccount .~ selacct}
|
||||
ui = ui0{aScreen=scr & asSelectedAccount .~ selacct}
|
||||
|
||||
case mode of
|
||||
Minibuffer ed ->
|
||||
case ev of
|
||||
EvKey KEsc [] -> continue $ stCloseMinibuffer st
|
||||
EvKey KEnter [] -> continue $ regenerateScreens j d $ stFilter s $ stCloseMinibuffer st
|
||||
EvKey KEsc [] -> continue $ closeMinibuffer ui
|
||||
EvKey KEnter [] -> continue $ regenerateScreens j d $ setFilter s $ closeMinibuffer ui
|
||||
where s = chomp $ unlines $ getEditContents ed
|
||||
ev -> do ed' <- handleEvent ev ed
|
||||
continue $ st{aMode=Minibuffer ed'}
|
||||
continue $ ui{aMode=Minibuffer ed'}
|
||||
|
||||
Help ->
|
||||
case ev of
|
||||
EvKey (KChar 'q') [] -> halt st
|
||||
_ -> helpHandle st ev
|
||||
EvKey (KChar 'q') [] -> halt ui
|
||||
_ -> helpHandle ui ev
|
||||
|
||||
Normal ->
|
||||
case ev of
|
||||
EvKey (KChar 'q') [] -> halt st
|
||||
EvKey (KChar 'q') [] -> halt ui
|
||||
-- EvKey (KChar 'l') [MCtrl] -> do
|
||||
EvKey KEsc [] -> continue $ resetScreens d st
|
||||
EvKey k [] | k `elem` [KChar 'h', KChar '?'] -> continue $ setMode Help st
|
||||
EvKey (KChar 'g') [] -> liftIO (stReloadJournalIfChanged copts d j st) >>= continue
|
||||
EvKey (KChar 'a') [] -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add copts j >> stReloadJournalIfChanged copts d j st
|
||||
EvKey (KChar '0') [] -> continue $ regenerateScreens j d $ setDepth (Just 0) st
|
||||
EvKey (KChar '1') [] -> continue $ regenerateScreens j d $ setDepth (Just 1) st
|
||||
EvKey (KChar '2') [] -> continue $ regenerateScreens j d $ setDepth (Just 2) st
|
||||
EvKey (KChar '3') [] -> continue $ regenerateScreens j d $ setDepth (Just 3) st
|
||||
EvKey (KChar '4') [] -> continue $ regenerateScreens j d $ setDepth (Just 4) st
|
||||
EvKey (KChar '5') [] -> continue $ regenerateScreens j d $ setDepth (Just 5) st
|
||||
EvKey (KChar '6') [] -> continue $ regenerateScreens j d $ setDepth (Just 6) st
|
||||
EvKey (KChar '7') [] -> continue $ regenerateScreens j d $ setDepth (Just 7) st
|
||||
EvKey (KChar '8') [] -> continue $ regenerateScreens j d $ setDepth (Just 8) st
|
||||
EvKey (KChar '9') [] -> continue $ regenerateScreens j d $ setDepth (Just 9) st
|
||||
EvKey (KChar '-') [] -> continue $ regenerateScreens j d $ decDepth st
|
||||
EvKey (KChar '_') [] -> continue $ regenerateScreens j d $ decDepth st
|
||||
EvKey k [] | k `elem` [KChar '+', KChar '='] -> continue $ regenerateScreens j d $ incDepth st
|
||||
EvKey (KChar 'F') [] -> continue $ regenerateScreens j d $ stToggleFlat st
|
||||
EvKey (KChar 'E') [] -> scrollTop >> (continue $ regenerateScreens j d $ stToggleEmpty st)
|
||||
EvKey (KChar 'C') [] -> scrollTop >> (continue $ regenerateScreens j d $ stToggleCleared st)
|
||||
EvKey (KChar 'U') [] -> scrollTop >> (continue $ regenerateScreens j d $ stToggleUncleared st)
|
||||
EvKey (KChar 'R') [] -> scrollTop >> (continue $ regenerateScreens j d $ stToggleReal st)
|
||||
EvKey k [] | k `elem` [KChar '/'] -> continue $ regenerateScreens j d $ stShowMinibuffer st
|
||||
EvKey k [] | k `elem` [KBS, KDel] -> (continue $ regenerateScreens j d $ stResetFilter st)
|
||||
EvKey (KLeft) [] -> continue $ popScreen st
|
||||
EvKey (k) [] | k `elem` [KRight, KEnter] -> scrollTopRegister >> continue (screenEnter d scr st)
|
||||
EvKey KEsc [] -> continue $ resetScreens d ui
|
||||
EvKey k [] | k `elem` [KChar 'h', KChar '?'] -> continue $ setMode Help ui
|
||||
EvKey (KChar 'g') [] -> liftIO (uiReloadJournalIfChanged copts d j ui) >>= continue
|
||||
EvKey (KChar 'a') [] -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add copts j >> uiReloadJournalIfChanged copts d j ui
|
||||
EvKey (KChar '0') [] -> continue $ regenerateScreens j d $ setDepth (Just 0) ui
|
||||
EvKey (KChar '1') [] -> continue $ regenerateScreens j d $ setDepth (Just 1) ui
|
||||
EvKey (KChar '2') [] -> continue $ regenerateScreens j d $ setDepth (Just 2) ui
|
||||
EvKey (KChar '3') [] -> continue $ regenerateScreens j d $ setDepth (Just 3) ui
|
||||
EvKey (KChar '4') [] -> continue $ regenerateScreens j d $ setDepth (Just 4) ui
|
||||
EvKey (KChar '5') [] -> continue $ regenerateScreens j d $ setDepth (Just 5) ui
|
||||
EvKey (KChar '6') [] -> continue $ regenerateScreens j d $ setDepth (Just 6) ui
|
||||
EvKey (KChar '7') [] -> continue $ regenerateScreens j d $ setDepth (Just 7) ui
|
||||
EvKey (KChar '8') [] -> continue $ regenerateScreens j d $ setDepth (Just 8) ui
|
||||
EvKey (KChar '9') [] -> continue $ regenerateScreens j d $ setDepth (Just 9) ui
|
||||
EvKey (KChar '-') [] -> continue $ regenerateScreens j d $ decDepth ui
|
||||
EvKey (KChar '_') [] -> continue $ regenerateScreens j d $ decDepth ui
|
||||
EvKey k [] | k `elem` [KChar '+', KChar '='] -> continue $ regenerateScreens j d $ incDepth ui
|
||||
EvKey (KChar 'F') [] -> continue $ regenerateScreens j d $ toggleFlat ui
|
||||
EvKey (KChar 'E') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleEmpty ui)
|
||||
EvKey (KChar 'C') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleCleared ui)
|
||||
EvKey (KChar 'U') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleUncleared ui)
|
||||
EvKey (KChar 'R') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleReal ui)
|
||||
EvKey k [] | k `elem` [KChar '/'] -> continue $ regenerateScreens j d $ showMinibuffer ui
|
||||
EvKey k [] | k `elem` [KBS, KDel] -> (continue $ regenerateScreens j d $ resetFilter ui)
|
||||
EvKey (KLeft) [] -> continue $ popScreen ui
|
||||
EvKey (k) [] | k `elem` [KRight, KEnter] -> scrollTopRegister >> continue (screenEnter d scr ui)
|
||||
where
|
||||
scr = rsSetAccount selacct registerScreen
|
||||
|
||||
-- fall through to the list's event handler (handles up/down)
|
||||
ev -> do
|
||||
newitems <- handleEvent ev (scr ^. asList)
|
||||
continue $ st{aScreen=scr & asList .~ newitems
|
||||
continue $ ui{aScreen=scr & asList .~ newitems
|
||||
& asSelectedAccount .~ selacct
|
||||
}
|
||||
-- continue =<< handleEventLensed st someLens ev
|
||||
-- continue =<< handleEventLensed ui someLens ev
|
||||
|
||||
where
|
||||
-- Encourage a more stable scroll position when toggling list items.
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
module Hledger.UI.ErrorScreen
|
||||
(errorScreen
|
||||
,stReloadJournalIfChanged
|
||||
,uiReloadJournalIfChanged
|
||||
)
|
||||
where
|
||||
|
||||
@ -13,7 +13,7 @@ import Control.Monad.IO.Class (liftIO)
|
||||
import Data.Monoid
|
||||
-- import Data.Maybe
|
||||
import Data.Time.Calendar (Day)
|
||||
import Graphics.Vty as Vty
|
||||
import Graphics.Vty
|
||||
import Brick
|
||||
-- import Brick.Widgets.List
|
||||
-- import Brick.Widgets.Border
|
||||
@ -26,6 +26,7 @@ import Hledger.Cli hiding (progname,prognameandversion,green)
|
||||
import Hledger.UI.UIOptions
|
||||
-- import Hledger.UI.Theme
|
||||
import Hledger.UI.UITypes
|
||||
import Hledger.UI.UIState
|
||||
import Hledger.UI.UIUtils
|
||||
|
||||
errorScreen :: Screen
|
||||
@ -36,12 +37,12 @@ errorScreen = ErrorScreen{
|
||||
,esError = ""
|
||||
}
|
||||
|
||||
esInit :: Day -> Bool -> AppState -> AppState
|
||||
esInit _ _ st@AppState{aScreen=ErrorScreen{}} = st
|
||||
esInit :: Day -> Bool -> UIState -> UIState
|
||||
esInit _ _ ui@UIState{aScreen=ErrorScreen{}} = ui
|
||||
esInit _ _ _ = error "init function called with wrong screen type, should not happen"
|
||||
|
||||
esDraw :: AppState -> [Widget]
|
||||
esDraw AppState{ -- aopts=_uopts@UIOpts{cliopts_=_copts@CliOpts{reportopts_=_ropts@ReportOpts{query_=querystr}}},
|
||||
esDraw :: UIState -> [Widget]
|
||||
esDraw UIState{ -- aopts=_uopts@UIOpts{cliopts_=_copts@CliOpts{reportopts_=_ropts@ReportOpts{query_=querystr}}},
|
||||
aScreen=ErrorScreen{..}
|
||||
,aMode=mode} =
|
||||
case mode of
|
||||
@ -65,8 +66,8 @@ esDraw AppState{ -- aopts=_uopts@UIOpts{cliopts_=_copts@CliOpts{reportopts_=_rop
|
||||
|
||||
esDraw _ = error "draw function called with wrong screen type, should not happen"
|
||||
|
||||
esHandle :: AppState -> Vty.Event -> EventM (Next AppState)
|
||||
esHandle st@AppState{
|
||||
esHandle :: UIState -> Event -> EventM (Next UIState)
|
||||
esHandle ui@UIState{
|
||||
aScreen=s@ErrorScreen{}
|
||||
,aopts=UIOpts{cliopts_=copts}
|
||||
,ajournal=j
|
||||
@ -75,35 +76,35 @@ esHandle st@AppState{
|
||||
case mode of
|
||||
Help ->
|
||||
case ev of
|
||||
EvKey (KChar 'q') [] -> halt st
|
||||
_ -> helpHandle st ev
|
||||
EvKey (KChar 'q') [] -> halt ui
|
||||
_ -> helpHandle ui ev
|
||||
|
||||
_ -> do
|
||||
d <- liftIO getCurrentDay
|
||||
case ev of
|
||||
EvKey (KChar 'q') [] -> halt st
|
||||
EvKey KEsc [] -> continue $ resetScreens d st
|
||||
EvKey k [] | k `elem` [KChar 'h', KChar '?'] -> continue $ setMode Help st
|
||||
EvKey (KChar 'q') [] -> halt ui
|
||||
EvKey KEsc [] -> continue $ resetScreens d ui
|
||||
EvKey k [] | k `elem` [KChar 'h', KChar '?'] -> continue $ setMode Help ui
|
||||
EvKey (KChar 'g') [] -> do
|
||||
(ej, _) <- liftIO $ journalReloadIfChanged copts d j
|
||||
case ej of
|
||||
Left err -> continue st{aScreen=s{esError=err}} -- show latest parse error
|
||||
Right j' -> continue $ regenerateScreens j' d $ popScreen st -- return to previous screen, and reload it
|
||||
-- EvKey (KLeft) [] -> continue $ popScreen st
|
||||
Left err -> continue ui{aScreen=s{esError=err}} -- show latest parse error
|
||||
Right j' -> continue $ regenerateScreens j' d $ popScreen ui -- return to previous screen, and reload it
|
||||
-- EvKey (KLeft) [] -> continue $ popScreen ui
|
||||
-- EvKey (KRight) [] -> error (show curItem) where curItem = listSelectedElement is
|
||||
-- fall through to the list's event handler (handles [pg]up/down)
|
||||
_ -> do continue st
|
||||
_ -> do continue ui
|
||||
-- is' <- handleEvent ev is
|
||||
-- continue st{aScreen=s{rsState=is'}}
|
||||
-- continue =<< handleEventLensed st someLens e
|
||||
-- continue ui{aScreen=s{rsState=is'}}
|
||||
-- continue =<< handleEventLensed ui someLens e
|
||||
esHandle _ _ = error "event handler called with wrong screen type, should not happen"
|
||||
|
||||
-- If journal file(s) have changed, reload the journal and regenerate all screens.
|
||||
-- This is here so it can reference the error screen.
|
||||
stReloadJournalIfChanged :: CliOpts -> Day -> Journal -> AppState -> IO AppState
|
||||
stReloadJournalIfChanged copts d j st = do
|
||||
uiReloadJournalIfChanged :: CliOpts -> Day -> Journal -> UIState -> IO UIState
|
||||
uiReloadJournalIfChanged copts d j ui = do
|
||||
(ej, _) <- journalReloadIfChanged copts d j
|
||||
return $ case ej of
|
||||
Right j' -> regenerateScreens j' d st
|
||||
Left err -> screenEnter d errorScreen{esError=err} st
|
||||
Right j' -> regenerateScreens j' d ui
|
||||
Left err -> screenEnter d errorScreen{esError=err} ui
|
||||
|
||||
|
@ -107,10 +107,10 @@ runBrickUi uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}} j = do
|
||||
(error' $ "--register "++apat++" did not match any account")
|
||||
$ filter (regexMatches apat . T.unpack) $ journalAccountNames j
|
||||
-- Initialising the accounts screen is awkward, requiring
|
||||
-- another temporary AppState value..
|
||||
-- another temporary UIState value..
|
||||
ascr' = aScreen $
|
||||
asInit d True $
|
||||
AppState{
|
||||
UIState{
|
||||
aopts=uopts'
|
||||
,ajournal=j
|
||||
,aScreen=asSetSelectedAccount acct accountsScreen
|
||||
@ -118,8 +118,8 @@ runBrickUi uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}} j = do
|
||||
,aMode=Normal
|
||||
}
|
||||
|
||||
st = (sInit scr) d True
|
||||
AppState{
|
||||
ui = (sInit scr) d True
|
||||
UIState{
|
||||
aopts=uopts'
|
||||
,ajournal=j
|
||||
,aScreen=scr
|
||||
@ -127,20 +127,15 @@ runBrickUi uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}} j = do
|
||||
,aMode=Normal
|
||||
}
|
||||
|
||||
brickapp :: App (AppState) V.Event
|
||||
brickapp :: App (UIState) V.Event
|
||||
brickapp = App {
|
||||
appLiftVtyEvent = id
|
||||
, appStartEvent = return
|
||||
, appAttrMap = const theme
|
||||
, appChooseCursor = showFirstCursor
|
||||
, appHandleEvent = \st ev -> sHandle (aScreen st) st ev
|
||||
, appDraw = \st -> sDraw (aScreen st) st
|
||||
-- XXX bizarro. removing the st arg and parameter above,
|
||||
-- which according to GHCI does not change the type,
|
||||
-- causes "Exception: draw function called with wrong screen type"
|
||||
-- on entering a register. Likewise, removing the st ev args and parameters
|
||||
-- causes an exception on exiting a register.
|
||||
, appHandleEvent = \ui ev -> sHandle (aScreen ui) ui ev
|
||||
, appDraw = \ui -> sDraw (aScreen ui) ui
|
||||
}
|
||||
|
||||
void $ defaultMain brickapp st
|
||||
void $ defaultMain brickapp ui
|
||||
|
||||
|
@ -18,7 +18,7 @@ import Data.Maybe
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Calendar (Day)
|
||||
import qualified Data.Vector as V
|
||||
import Graphics.Vty as Vty
|
||||
import Graphics.Vty
|
||||
import Brick
|
||||
import Brick.Widgets.List
|
||||
import Brick.Widgets.Edit
|
||||
@ -33,6 +33,7 @@ import Hledger.Cli hiding (progname,prognameandversion,green)
|
||||
import Hledger.UI.UIOptions
|
||||
-- import Hledger.UI.Theme
|
||||
import Hledger.UI.UITypes
|
||||
import Hledger.UI.UIState
|
||||
import Hledger.UI.UIUtils
|
||||
import Hledger.UI.TransactionScreen
|
||||
import Hledger.UI.ErrorScreen
|
||||
@ -49,9 +50,9 @@ registerScreen = RegisterScreen{
|
||||
rsSetAccount a scr@RegisterScreen{} = scr{rsAccount=a}
|
||||
rsSetAccount _ scr = scr
|
||||
|
||||
rsInit :: Day -> Bool -> AppState -> AppState
|
||||
rsInit d reset st@AppState{aopts=opts, ajournal=j, aScreen=s@RegisterScreen{..}} =
|
||||
st{aScreen=s{rsList=newitems'}}
|
||||
rsInit :: Day -> Bool -> UIState -> UIState
|
||||
rsInit d reset ui@UIState{aopts=opts, ajournal=j, aScreen=s@RegisterScreen{..}} =
|
||||
ui{aScreen=s{rsList=newitems'}}
|
||||
where
|
||||
-- gather arguments and queries
|
||||
ropts = (reportopts_ $ cliopts_ opts)
|
||||
@ -99,8 +100,8 @@ rsInit d reset st@AppState{aopts=opts, ajournal=j, aScreen=s@RegisterScreen{..}}
|
||||
|
||||
rsInit _ _ _ = error "init function called with wrong screen type, should not happen"
|
||||
|
||||
rsDraw :: AppState -> [Widget]
|
||||
rsDraw AppState{aopts=UIOpts{cliopts_=CliOpts{reportopts_=ropts}}
|
||||
rsDraw :: UIState -> [Widget]
|
||||
rsDraw UIState{aopts=UIOpts{cliopts_=CliOpts{reportopts_=ropts}}
|
||||
,aScreen=RegisterScreen{..}
|
||||
,aMode=mode
|
||||
} =
|
||||
@ -219,8 +220,8 @@ rsDrawItem (datewidth,descwidth,acctswidth,changewidth,balwidth) selected Regist
|
||||
sel | selected = (<> "selected")
|
||||
| otherwise = id
|
||||
|
||||
rsHandle :: AppState -> Vty.Event -> EventM (Next AppState)
|
||||
rsHandle st@AppState{
|
||||
rsHandle :: UIState -> Event -> EventM (Next UIState)
|
||||
rsHandle ui@UIState{
|
||||
aScreen=s@RegisterScreen{..}
|
||||
,aopts=UIOpts{cliopts_=copts}
|
||||
,ajournal=j
|
||||
@ -231,31 +232,31 @@ rsHandle st@AppState{
|
||||
case mode of
|
||||
Minibuffer ed ->
|
||||
case ev of
|
||||
EvKey KEsc [] -> continue $ stCloseMinibuffer st
|
||||
EvKey KEnter [] -> continue $ regenerateScreens j d $ stFilter s $ stCloseMinibuffer st
|
||||
EvKey KEsc [] -> continue $ closeMinibuffer ui
|
||||
EvKey KEnter [] -> continue $ regenerateScreens j d $ setFilter s $ closeMinibuffer ui
|
||||
where s = chomp $ unlines $ getEditContents ed
|
||||
ev -> do ed' <- handleEvent ev ed
|
||||
continue $ st{aMode=Minibuffer ed'}
|
||||
continue $ ui{aMode=Minibuffer ed'}
|
||||
|
||||
Help ->
|
||||
case ev of
|
||||
EvKey (KChar 'q') [] -> halt st
|
||||
_ -> helpHandle st ev
|
||||
EvKey (KChar 'q') [] -> halt ui
|
||||
_ -> helpHandle ui ev
|
||||
|
||||
Normal ->
|
||||
case ev of
|
||||
EvKey (KChar 'q') [] -> halt st
|
||||
EvKey KEsc [] -> continue $ resetScreens d st
|
||||
EvKey k [] | k `elem` [KChar 'h', KChar '?'] -> continue $ setMode Help st
|
||||
EvKey (KChar 'g') [] -> liftIO (stReloadJournalIfChanged copts d j st) >>= continue
|
||||
EvKey (KChar 'a') [] -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add copts j >> stReloadJournalIfChanged copts d j st
|
||||
EvKey (KChar 'E') [] -> scrollTop >> (continue $ regenerateScreens j d $ stToggleEmpty st)
|
||||
EvKey (KChar 'C') [] -> scrollTop >> (continue $ regenerateScreens j d $ stToggleCleared st)
|
||||
EvKey (KChar 'U') [] -> scrollTop >> (continue $ regenerateScreens j d $ stToggleUncleared st)
|
||||
EvKey (KChar 'R') [] -> scrollTop >> (continue $ regenerateScreens j d $ stToggleReal st)
|
||||
EvKey k [] | k `elem` [KChar '/'] -> (continue $ regenerateScreens j d $ stShowMinibuffer st)
|
||||
EvKey k [] | k `elem` [KBS, KDel] -> (continue $ regenerateScreens j d $ stResetFilter st)
|
||||
EvKey (KLeft) [] -> continue $ popScreen st
|
||||
EvKey (KChar 'q') [] -> halt ui
|
||||
EvKey KEsc [] -> continue $ resetScreens d ui
|
||||
EvKey k [] | k `elem` [KChar 'h', KChar '?'] -> continue $ setMode Help ui
|
||||
EvKey (KChar 'g') [] -> liftIO (uiReloadJournalIfChanged copts d j ui) >>= continue
|
||||
EvKey (KChar 'a') [] -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add copts j >> uiReloadJournalIfChanged copts d j ui
|
||||
EvKey (KChar 'E') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleEmpty ui)
|
||||
EvKey (KChar 'C') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleCleared ui)
|
||||
EvKey (KChar 'U') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleUncleared ui)
|
||||
EvKey (KChar 'R') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleReal ui)
|
||||
EvKey k [] | k `elem` [KChar '/'] -> (continue $ regenerateScreens j d $ showMinibuffer ui)
|
||||
EvKey k [] | k `elem` [KBS, KDel] -> (continue $ regenerateScreens j d $ resetFilter ui)
|
||||
EvKey (KLeft) [] -> continue $ popScreen ui
|
||||
|
||||
EvKey (k) [] | k `elem` [KRight, KEnter] -> do
|
||||
case listSelectedElement rsList of
|
||||
@ -267,13 +268,13 @@ rsHandle st@AppState{
|
||||
in
|
||||
continue $ screenEnter d transactionScreen{tsTransaction=(i,t)
|
||||
,tsTransactions=numberedts
|
||||
,tsAccount=rsAccount} st
|
||||
Nothing -> continue st
|
||||
,tsAccount=rsAccount} ui
|
||||
Nothing -> continue ui
|
||||
|
||||
-- fall through to the list's event handler (handles [pg]up/down)
|
||||
ev -> do newitems <- handleEvent ev rsList
|
||||
continue st{aScreen=s{rsList=newitems}}
|
||||
-- continue =<< handleEventLensed st someLens ev
|
||||
continue ui{aScreen=s{rsList=newitems}}
|
||||
-- continue =<< handleEventLensed ui someLens ev
|
||||
|
||||
where
|
||||
-- Encourage a more stable scroll position when toggling list items (cf AccountsScreen.hs)
|
||||
|
@ -19,7 +19,7 @@ import Data.Monoid
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Calendar (Day)
|
||||
-- import qualified Data.Vector as V
|
||||
import Graphics.Vty as Vty
|
||||
import Graphics.Vty
|
||||
-- import Safe (headDef, lastDef)
|
||||
import Brick
|
||||
import Brick.Widgets.List (listMoveTo)
|
||||
@ -33,6 +33,7 @@ import Hledger.Cli hiding (progname,prognameandversion,green)
|
||||
import Hledger.UI.UIOptions
|
||||
-- import Hledger.UI.Theme
|
||||
import Hledger.UI.UITypes
|
||||
import Hledger.UI.UIState
|
||||
import Hledger.UI.UIUtils
|
||||
import Hledger.UI.ErrorScreen
|
||||
|
||||
@ -46,14 +47,14 @@ transactionScreen = TransactionScreen{
|
||||
,tsAccount = ""
|
||||
}
|
||||
|
||||
tsInit :: Day -> Bool -> AppState -> AppState
|
||||
tsInit _d _reset st@AppState{aopts=UIOpts{cliopts_=CliOpts{reportopts_=_ropts}}
|
||||
tsInit :: Day -> Bool -> UIState -> UIState
|
||||
tsInit _d _reset ui@UIState{aopts=UIOpts{cliopts_=CliOpts{reportopts_=_ropts}}
|
||||
,ajournal=_j
|
||||
,aScreen=TransactionScreen{..}} = st
|
||||
,aScreen=TransactionScreen{..}} = ui
|
||||
tsInit _ _ _ = error "init function called with wrong screen type, should not happen"
|
||||
|
||||
tsDraw :: AppState -> [Widget]
|
||||
tsDraw AppState{aopts=UIOpts{cliopts_=CliOpts{reportopts_=ropts}}
|
||||
tsDraw :: UIState -> [Widget]
|
||||
tsDraw UIState{aopts=UIOpts{cliopts_=CliOpts{reportopts_=ropts}}
|
||||
,aScreen=TransactionScreen{
|
||||
tsTransaction=(i,t)
|
||||
,tsTransactions=nts
|
||||
@ -107,8 +108,8 @@ tsDraw AppState{aopts=UIOpts{cliopts_=CliOpts{reportopts_=ropts}}
|
||||
|
||||
tsDraw _ = error "draw function called with wrong screen type, should not happen"
|
||||
|
||||
tsHandle :: AppState -> Vty.Event -> EventM (Next AppState)
|
||||
tsHandle st@AppState{aScreen=s@TransactionScreen{tsTransaction=(i,t)
|
||||
tsHandle :: UIState -> Event -> EventM (Next UIState)
|
||||
tsHandle ui@UIState{aScreen=s@TransactionScreen{tsTransaction=(i,t)
|
||||
,tsTransactions=nts
|
||||
,tsAccount=acct}
|
||||
,aopts=UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}
|
||||
@ -119,8 +120,8 @@ tsHandle st@AppState{aScreen=s@TransactionScreen{tsTransaction=(i,t)
|
||||
case mode of
|
||||
Help ->
|
||||
case ev of
|
||||
EvKey (KChar 'q') [] -> halt st
|
||||
_ -> helpHandle st ev
|
||||
EvKey (KChar 'q') [] -> halt ui
|
||||
_ -> helpHandle ui ev
|
||||
|
||||
_ -> do
|
||||
d <- liftIO getCurrentDay
|
||||
@ -128,14 +129,14 @@ tsHandle st@AppState{aScreen=s@TransactionScreen{tsTransaction=(i,t)
|
||||
(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
|
||||
EvKey (KChar 'q') [] -> halt st
|
||||
EvKey KEsc [] -> continue $ resetScreens d st
|
||||
EvKey k [] | k `elem` [KChar 'h', KChar '?'] -> continue $ setMode Help st
|
||||
EvKey (KChar 'q') [] -> halt ui
|
||||
EvKey KEsc [] -> continue $ resetScreens d ui
|
||||
EvKey k [] | k `elem` [KChar 'h', KChar '?'] -> continue $ setMode Help ui
|
||||
EvKey (KChar 'g') [] -> do
|
||||
d <- liftIO getCurrentDay
|
||||
(ej, _) <- liftIO $ journalReloadIfChanged copts d j
|
||||
case ej of
|
||||
Left err -> continue $ screenEnter d errorScreen{esError=err} st
|
||||
Left err -> continue $ screenEnter d errorScreen{esError=err} ui
|
||||
Right j' -> do
|
||||
-- got to redo the register screen's transactions report, to get the latest transactions list for this screen
|
||||
-- XXX duplicates rsInit
|
||||
@ -155,21 +156,21 @@ tsHandle st@AppState{aScreen=s@TransactionScreen{tsTransaction=(i,t)
|
||||
Nothing | null numberedts -> (0,nulltransaction)
|
||||
| i > fst (last numberedts) -> last numberedts
|
||||
| otherwise -> head numberedts
|
||||
st' = st{aScreen=s{tsTransaction=(i',t')
|
||||
ui' = ui{aScreen=s{tsTransaction=(i',t')
|
||||
,tsTransactions=numberedts
|
||||
,tsAccount=acct}}
|
||||
continue $ regenerateScreens j' d st'
|
||||
continue $ regenerateScreens j' d ui'
|
||||
-- if allowing toggling here, we should refresh the txn list from the parent register screen
|
||||
-- EvKey (KChar 'E') [] -> continue $ regenerateScreens j d $ stToggleEmpty st
|
||||
-- EvKey (KChar 'C') [] -> continue $ regenerateScreens j d $ stToggleCleared st
|
||||
-- EvKey (KChar 'R') [] -> continue $ regenerateScreens j d $ stToggleReal st
|
||||
EvKey KUp [] -> continue $ regenerateScreens j d st{aScreen=s{tsTransaction=(iprev,tprev)}}
|
||||
EvKey KDown [] -> continue $ regenerateScreens j d st{aScreen=s{tsTransaction=(inext,tnext)}}
|
||||
EvKey KLeft [] -> continue st''
|
||||
-- EvKey (KChar 'E') [] -> continue $ regenerateScreens j d $ stToggleEmpty ui
|
||||
-- EvKey (KChar 'C') [] -> continue $ regenerateScreens j d $ stToggleCleared ui
|
||||
-- EvKey (KChar 'R') [] -> continue $ regenerateScreens j d $ stToggleReal ui
|
||||
EvKey KUp [] -> continue $ regenerateScreens j d ui{aScreen=s{tsTransaction=(iprev,tprev)}}
|
||||
EvKey KDown [] -> continue $ regenerateScreens j d ui{aScreen=s{tsTransaction=(inext,tnext)}}
|
||||
EvKey KLeft [] -> continue ui''
|
||||
where
|
||||
st'@AppState{aScreen=scr} = popScreen st
|
||||
st'' = st'{aScreen=rsSelect (fromIntegral i) scr}
|
||||
_ -> continue st
|
||||
ui'@UIState{aScreen=scr} = popScreen ui
|
||||
ui'' = ui'{aScreen=rsSelect (fromIntegral i) scr}
|
||||
_ -> continue ui
|
||||
|
||||
tsHandle _ _ = error "event handler called with wrong screen type, should not happen"
|
||||
|
||||
|
172
hledger-ui/Hledger/UI/UIState.hs
Normal file
172
hledger-ui/Hledger/UI/UIState.hs
Normal file
@ -0,0 +1,172 @@
|
||||
{- | UIState operations. -}
|
||||
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Hledger.UI.UIState
|
||||
where
|
||||
|
||||
import Brick
|
||||
import Brick.Widgets.Edit
|
||||
import Data.List
|
||||
import Data.Text.Zipper (gotoEOL)
|
||||
import Data.Time.Calendar (Day)
|
||||
|
||||
import Hledger
|
||||
import Hledger.Cli.CliOptions
|
||||
import Hledger.UI.UITypes
|
||||
import Hledger.UI.UIOptions
|
||||
|
||||
-- | Toggle between showing only cleared items or all items.
|
||||
toggleCleared :: UIState -> UIState
|
||||
toggleCleared ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
ui{aopts=uopts{cliopts_=copts{reportopts_=toggleCleared ropts}}}
|
||||
where
|
||||
toggleCleared ropts = ropts{cleared_=not $ cleared_ ropts, uncleared_=False, pending_=False}
|
||||
|
||||
-- | Toggle between showing only pending items or all items.
|
||||
togglePending :: UIState -> UIState
|
||||
togglePending ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
ui{aopts=uopts{cliopts_=copts{reportopts_=togglePending ropts}}}
|
||||
where
|
||||
togglePending ropts = ropts{pending_=not $ pending_ ropts, uncleared_=False, cleared_=False}
|
||||
|
||||
-- | Toggle between showing only uncleared items or all items.
|
||||
toggleUncleared :: UIState -> UIState
|
||||
toggleUncleared ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
ui{aopts=uopts{cliopts_=copts{reportopts_=toggleUncleared ropts}}}
|
||||
where
|
||||
toggleUncleared ropts = ropts{uncleared_=not $ uncleared_ ropts, cleared_=False, pending_=False}
|
||||
|
||||
-- | Toggle between showing all and showing only nonempty (more precisely, nonzero) items.
|
||||
toggleEmpty :: UIState -> UIState
|
||||
toggleEmpty ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
ui{aopts=uopts{cliopts_=copts{reportopts_=toggleEmpty ropts}}}
|
||||
where
|
||||
toggleEmpty ropts = ropts{empty_=not $ empty_ ropts}
|
||||
|
||||
-- | Toggle between flat and tree mode. If in the third "default" mode, go to flat mode.
|
||||
toggleFlat :: UIState -> UIState
|
||||
toggleFlat ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
ui{aopts=uopts{cliopts_=copts{reportopts_=toggleFlatMode ropts}}}
|
||||
where
|
||||
toggleFlatMode ropts@ReportOpts{accountlistmode_=ALFlat} = ropts{accountlistmode_=ALTree}
|
||||
toggleFlatMode ropts = ropts{accountlistmode_=ALFlat}
|
||||
|
||||
-- | Toggle between showing all and showing only real (non-virtual) items.
|
||||
toggleReal :: UIState -> UIState
|
||||
toggleReal ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
ui{aopts=uopts{cliopts_=copts{reportopts_=toggleReal ropts}}}
|
||||
where
|
||||
toggleReal ropts = ropts{real_=not $ real_ ropts}
|
||||
|
||||
-- | Apply a new filter query.
|
||||
setFilter :: String -> UIState -> UIState
|
||||
setFilter s ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{query_=s}}}}
|
||||
|
||||
-- | Clear all filter queries/flags.
|
||||
resetFilter :: UIState -> UIState
|
||||
resetFilter ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{
|
||||
empty_=True
|
||||
,cleared_=False
|
||||
,pending_=False
|
||||
,uncleared_=False
|
||||
,real_=False
|
||||
,query_=""
|
||||
}}}}
|
||||
|
||||
resetDepth :: UIState -> UIState
|
||||
resetDepth ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=Nothing}}}}
|
||||
|
||||
-- | Get the maximum account depth in the current journal.
|
||||
maxDepth :: UIState -> Int
|
||||
maxDepth UIState{ajournal=j} = maximum $ map accountNameLevel $ journalAccountNames j
|
||||
|
||||
-- | Decrement the current depth limit towards 0. If there was no depth limit,
|
||||
-- set it to one less than the maximum account depth.
|
||||
decDepth :: UIState -> UIState
|
||||
decDepth ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts@ReportOpts{..}}}}
|
||||
= ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=dec depth_}}}}
|
||||
where
|
||||
dec (Just d) = Just $ max 0 (d-1)
|
||||
dec Nothing = Just $ maxDepth ui - 1
|
||||
|
||||
-- | Increment the current depth limit. If this makes it equal to the
|
||||
-- the maximum account depth, remove the depth limit.
|
||||
incDepth :: UIState -> UIState
|
||||
incDepth ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts@ReportOpts{..}}}}
|
||||
= ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=inc depth_}}}}
|
||||
where
|
||||
inc (Just d) | d < (maxDepth ui - 1) = Just $ d+1
|
||||
inc _ = Nothing
|
||||
|
||||
-- | Set the current depth limit to the specified depth, or remove the depth limit.
|
||||
-- Also remove the depth limit if the specified depth is greater than the current
|
||||
-- maximum account depth. If the specified depth is negative, reset the depth limit
|
||||
-- to whatever was specified at uiartup.
|
||||
setDepth :: Maybe Int -> UIState -> UIState
|
||||
setDepth mdepth ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}}
|
||||
= ui{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=mdepth'}}}}
|
||||
where
|
||||
mdepth' = case mdepth of
|
||||
Nothing -> Nothing
|
||||
Just d | d < 0 -> depth_ ropts
|
||||
| d >= maxDepth ui -> Nothing
|
||||
| otherwise -> mdepth
|
||||
|
||||
-- | Open the minibuffer, setting its content to the current query with the cursor at the end.
|
||||
showMinibuffer :: UIState -> UIState
|
||||
showMinibuffer ui = setMode (Minibuffer e) ui
|
||||
where
|
||||
e = applyEdit gotoEOL $ editor "minibuffer" (str . unlines) (Just 1) oldq
|
||||
oldq = query_ $ reportopts_ $ cliopts_ $ aopts ui
|
||||
|
||||
-- | Close the minibuffer, discarding any edit in progress.
|
||||
closeMinibuffer :: UIState -> UIState
|
||||
closeMinibuffer = setMode Normal
|
||||
|
||||
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
|
||||
first:rest = reverse $ s:ss :: [Screen]
|
||||
ui0 = ui{ajournal=j, aScreen=first, aPrevScreens=[]} :: UIState
|
||||
|
||||
ui1 = (sInit first) 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
|
||||
}
|
||||
|
||||
popScreen :: UIState -> UIState
|
||||
popScreen ui@UIState{aPrevScreens=s:ss} = ui{aScreen=s, aPrevScreens=ss}
|
||||
popScreen ui = ui
|
||||
|
||||
resetScreens :: Day -> UIState -> UIState
|
||||
resetScreens d ui@UIState{aScreen=s,aPrevScreens=ss} =
|
||||
(sInit topscreen) d True $ resetDepth $ resetFilter $ closeMinibuffer ui{aScreen=topscreen, aPrevScreens=[]}
|
||||
where
|
||||
topscreen = case ss of _:_ -> last ss
|
||||
[] -> s
|
||||
|
||||
-- | 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
|
||||
|
@ -1,16 +1,16 @@
|
||||
{- |
|
||||
Overview:
|
||||
hledger-ui's AppState holds the currently active screen and any previously visited
|
||||
hledger-ui's UIState holds the currently active screen and any previously visited
|
||||
screens (and their states).
|
||||
The brick App delegates all event-handling and rendering
|
||||
to the AppState's active screen.
|
||||
to the UIState's active screen.
|
||||
Screens have their own screen state, render function, event handler, and app state
|
||||
update function, so they have full control.
|
||||
|
||||
@
|
||||
Brick.defaultMain brickapp st
|
||||
where
|
||||
brickapp :: App (AppState) V.Event
|
||||
brickapp :: App (UIState) V.Event
|
||||
brickapp = App {
|
||||
appLiftVtyEvent = id
|
||||
, appStartEvent = return
|
||||
@ -19,9 +19,9 @@ Brick.defaultMain brickapp st
|
||||
, appHandleEvent = \st ev -> sHandle (aScreen st) st ev
|
||||
, appDraw = \st -> sDraw (aScreen st) st
|
||||
}
|
||||
st :: AppState
|
||||
st :: UIState
|
||||
st = (sInit s) d
|
||||
AppState{
|
||||
UIState{
|
||||
aopts=uopts'
|
||||
,ajournal=j
|
||||
,aScreen=s
|
||||
@ -40,7 +40,7 @@ module Hledger.UI.UITypes where
|
||||
|
||||
import Data.Monoid
|
||||
import Data.Time.Calendar (Day)
|
||||
import qualified Graphics.Vty as Vty
|
||||
import Graphics.Vty
|
||||
import Brick
|
||||
import Brick.Widgets.List
|
||||
import Brick.Widgets.Edit (Editor)
|
||||
@ -59,12 +59,12 @@ instance Show Editor where show _ = "<Editor>"
|
||||
-- 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 AppState = AppState {
|
||||
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
|
||||
data UIState = UIState {
|
||||
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
|
||||
} deriving (Show)
|
||||
|
||||
-- | The mode modifies the screen's rendering and event handling.
|
||||
@ -86,34 +86,34 @@ instance Eq Editor where _ == _ = True
|
||||
-- cases need to be handled, and also that their lenses are traversals, not single-value getters.
|
||||
data Screen =
|
||||
AccountsScreen {
|
||||
sInit :: Day -> Bool -> AppState -> AppState -- ^ function to initialise or update this screen's state
|
||||
,sDraw :: AppState -> [Widget] -- ^ brick renderer for this screen
|
||||
,sHandle :: AppState -> Vty.Event -> EventM (Next AppState) -- ^ brick event handler for this screen
|
||||
sInit :: Day -> Bool -> UIState -> UIState -- ^ function to initialise or update this screen's state
|
||||
,sDraw :: UIState -> [Widget] -- ^ brick renderer for this screen
|
||||
,sHandle :: UIState -> Event -> EventM (Next UIState) -- ^ brick event handler for this screen
|
||||
-- state fields.These ones have lenses:
|
||||
,_asList :: List 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 -> AppState -> AppState
|
||||
,sDraw :: AppState -> [Widget]
|
||||
,sHandle :: AppState -> Vty.Event -> EventM (Next AppState)
|
||||
sInit :: Day -> Bool -> UIState -> UIState
|
||||
,sDraw :: UIState -> [Widget]
|
||||
,sHandle :: UIState -> Event -> EventM (Next UIState)
|
||||
--
|
||||
,rsList :: List RegisterScreenItem -- ^ list widget showing transactions affecting this account
|
||||
,rsAccount :: AccountName -- ^ the account this register is for
|
||||
}
|
||||
| TransactionScreen {
|
||||
sInit :: Day -> Bool -> AppState -> AppState
|
||||
,sDraw :: AppState -> [Widget]
|
||||
,sHandle :: AppState -> Vty.Event -> EventM (Next AppState)
|
||||
sInit :: Day -> Bool -> UIState -> UIState
|
||||
,sDraw :: UIState -> [Widget]
|
||||
,sHandle :: UIState -> Event -> EventM (Next 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 -> AppState -> AppState
|
||||
,sDraw :: AppState -> [Widget]
|
||||
,sHandle :: AppState -> Vty.Event -> EventM (Next AppState)
|
||||
sInit :: Day -> Bool -> UIState -> UIState
|
||||
,sDraw :: UIState -> [Widget]
|
||||
,sHandle :: UIState -> Event -> EventM (Next UIState)
|
||||
--
|
||||
,esError :: String -- ^ error message to show
|
||||
}
|
||||
@ -139,7 +139,7 @@ data RegisterScreenItem = RegisterScreenItem {
|
||||
|
||||
type NumberedTransaction = (Integer, Transaction)
|
||||
|
||||
-- dummy monoid instance needed for lenses for now since the List fields are not common across constructors
|
||||
-- dummy monoid instance needed make lenses work with List fields not common across constructors
|
||||
instance Monoid (List a)
|
||||
where
|
||||
mempty = list "" V.empty 1
|
||||
|
@ -1,212 +1,26 @@
|
||||
{- | Rendering & misc. helpers. -}
|
||||
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Hledger.UI.UIUtils
|
||||
-- (
|
||||
-- pushScreen
|
||||
-- ,popScreen
|
||||
-- ,resetScreens
|
||||
-- ,screenEnter
|
||||
-- ,regenerateScreens
|
||||
-- ,getViewportSize
|
||||
-- -- ,margin
|
||||
-- ,withBorderAttr
|
||||
-- ,topBottomBorderWithLabel
|
||||
-- ,topBottomBorderWithLabels
|
||||
-- ,defaultLayout
|
||||
-- ,borderQueryStr
|
||||
-- ,borderDepthStr
|
||||
-- ,borderKeysStr
|
||||
-- ,minibuffer
|
||||
-- --
|
||||
-- ,stToggleCleared
|
||||
-- ,stTogglePending
|
||||
-- ,stToggleUncleared
|
||||
-- ,stToggleEmpty
|
||||
-- ,stToggleFlat
|
||||
-- ,stToggleReal
|
||||
-- ,stFilter
|
||||
-- ,stResetFilter
|
||||
-- ,stShowMinibuffer
|
||||
-- ,stCloseMinibuffer
|
||||
-- )
|
||||
where
|
||||
where
|
||||
|
||||
import Lens.Micro.Platform ((^.))
|
||||
-- import Control.Monad
|
||||
-- import Control.Monad.IO.Class
|
||||
-- import Data.Default
|
||||
import Data.List
|
||||
import Data.Monoid
|
||||
import Data.Text.Zipper (gotoEOL)
|
||||
import Data.Time.Calendar (Day)
|
||||
import Brick
|
||||
import Brick.Widgets.Dialog
|
||||
-- import Brick.Widgets.List
|
||||
import Brick.Widgets.Edit
|
||||
import Brick.Widgets.Border
|
||||
import Brick.Widgets.Border.Style
|
||||
import Graphics.Vty as Vty
|
||||
import Brick.Widgets.Dialog
|
||||
import Brick.Widgets.Edit
|
||||
import Data.List
|
||||
import Data.Monoid
|
||||
import Graphics.Vty
|
||||
import Lens.Micro.Platform
|
||||
|
||||
import Hledger
|
||||
import Hledger.Cli.CliOptions
|
||||
import Hledger.UI.UITypes
|
||||
import Hledger.UI.UIOptions
|
||||
|
||||
-- | Toggle between showing only cleared items or all items.
|
||||
stToggleCleared :: AppState -> AppState
|
||||
stToggleCleared st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
st{aopts=uopts{cliopts_=copts{reportopts_=toggleCleared ropts}}}
|
||||
where
|
||||
toggleCleared ropts = ropts{cleared_=not $ cleared_ ropts, uncleared_=False, pending_=False}
|
||||
|
||||
-- | Toggle between showing only pending items or all items.
|
||||
stTogglePending :: AppState -> AppState
|
||||
stTogglePending st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
st{aopts=uopts{cliopts_=copts{reportopts_=togglePending ropts}}}
|
||||
where
|
||||
togglePending ropts = ropts{pending_=not $ pending_ ropts, uncleared_=False, cleared_=False}
|
||||
|
||||
-- | Toggle between showing only uncleared items or all items.
|
||||
stToggleUncleared :: AppState -> AppState
|
||||
stToggleUncleared st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
st{aopts=uopts{cliopts_=copts{reportopts_=toggleUncleared ropts}}}
|
||||
where
|
||||
toggleUncleared ropts = ropts{uncleared_=not $ uncleared_ ropts, cleared_=False, pending_=False}
|
||||
|
||||
-- | Toggle between showing all and showing only nonempty (more precisely, nonzero) items.
|
||||
stToggleEmpty :: AppState -> AppState
|
||||
stToggleEmpty st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
st{aopts=uopts{cliopts_=copts{reportopts_=toggleEmpty ropts}}}
|
||||
where
|
||||
toggleEmpty ropts = ropts{empty_=not $ empty_ ropts}
|
||||
|
||||
-- | Toggle between flat and tree mode. If in the third "default" mode, go to flat mode.
|
||||
stToggleFlat :: AppState -> AppState
|
||||
stToggleFlat st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
st{aopts=uopts{cliopts_=copts{reportopts_=toggleFlatMode ropts}}}
|
||||
where
|
||||
toggleFlatMode ropts@ReportOpts{accountlistmode_=ALFlat} = ropts{accountlistmode_=ALTree}
|
||||
toggleFlatMode ropts = ropts{accountlistmode_=ALFlat}
|
||||
|
||||
-- | Toggle between showing all and showing only real (non-virtual) items.
|
||||
stToggleReal :: AppState -> AppState
|
||||
stToggleReal st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
st{aopts=uopts{cliopts_=copts{reportopts_=toggleReal ropts}}}
|
||||
where
|
||||
toggleReal ropts = ropts{real_=not $ real_ ropts}
|
||||
|
||||
-- | Apply a new filter query.
|
||||
stFilter :: String -> AppState -> AppState
|
||||
stFilter s st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
st{aopts=uopts{cliopts_=copts{reportopts_=ropts{query_=s}}}}
|
||||
|
||||
-- | Clear all filter queries/flags.
|
||||
stResetFilter :: AppState -> AppState
|
||||
stResetFilter st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
st{aopts=uopts{cliopts_=copts{reportopts_=ropts{
|
||||
empty_=True
|
||||
,cleared_=False
|
||||
,pending_=False
|
||||
,uncleared_=False
|
||||
,real_=False
|
||||
,query_=""
|
||||
}}}}
|
||||
|
||||
resetDepth :: AppState -> AppState
|
||||
resetDepth st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}} =
|
||||
st{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=Nothing}}}}
|
||||
|
||||
-- | Get the maximum account depth in the current journal.
|
||||
maxDepth :: AppState -> Int
|
||||
maxDepth AppState{ajournal=j} = maximum $ map accountNameLevel $ journalAccountNames j
|
||||
|
||||
-- | Decrement the current depth limit towards 0. If there was no depth limit,
|
||||
-- set it to one less than the maximum account depth.
|
||||
decDepth :: AppState -> AppState
|
||||
decDepth st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts@ReportOpts{..}}}}
|
||||
= st{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=dec depth_}}}}
|
||||
where
|
||||
dec (Just d) = Just $ max 0 (d-1)
|
||||
dec Nothing = Just $ maxDepth st - 1
|
||||
|
||||
-- | Increment the current depth limit. If this makes it equal to the
|
||||
-- the maximum account depth, remove the depth limit.
|
||||
incDepth :: AppState -> AppState
|
||||
incDepth st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts@ReportOpts{..}}}}
|
||||
= st{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=inc depth_}}}}
|
||||
where
|
||||
inc (Just d) | d < (maxDepth st - 1) = Just $ d+1
|
||||
inc _ = Nothing
|
||||
|
||||
-- | Set the current depth limit to the specified depth, or remove the depth limit.
|
||||
-- Also remove the depth limit if the specified depth is greater than the current
|
||||
-- maximum account depth. If the specified depth is negative, reset the depth limit
|
||||
-- to whatever was specified at startup.
|
||||
setDepth :: Maybe Int -> AppState -> AppState
|
||||
setDepth mdepth st@AppState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportopts_=ropts}}}
|
||||
= st{aopts=uopts{cliopts_=copts{reportopts_=ropts{depth_=mdepth'}}}}
|
||||
where
|
||||
mdepth' = case mdepth of
|
||||
Nothing -> Nothing
|
||||
Just d | d < 0 -> depth_ ropts
|
||||
| d >= maxDepth st -> Nothing
|
||||
| otherwise -> mdepth
|
||||
|
||||
-- | Open the minibuffer, setting its content to the current query with the cursor at the end.
|
||||
stShowMinibuffer st = setMode (Minibuffer e) st
|
||||
where
|
||||
e = applyEdit gotoEOL $ editor "minibuffer" (str . unlines) (Just 1) oldq
|
||||
oldq = query_ $ reportopts_ $ cliopts_ $ aopts st
|
||||
|
||||
-- | Close the minibuffer, discarding any edit in progress.
|
||||
stCloseMinibuffer = setMode Normal
|
||||
|
||||
setMode :: Mode -> AppState -> AppState
|
||||
setMode m st = st{aMode=m}
|
||||
|
||||
-- | Regenerate the content for the current and previous screens, from a new journal and current date.
|
||||
regenerateScreens :: Journal -> Day -> AppState -> AppState
|
||||
regenerateScreens j d st@AppState{aScreen=s,aPrevScreens=ss} =
|
||||
-- XXX clumsy due to entanglement of AppState 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
|
||||
first:rest = reverse $ s:ss :: [Screen]
|
||||
st0 = st{ajournal=j, aScreen=first, aPrevScreens=[]} :: AppState
|
||||
st1 = (sInit first) d False st0 :: AppState
|
||||
st2 = foldl' (\st s -> (sInit s) d False $ pushScreen s st) st1 rest :: AppState
|
||||
in
|
||||
st2
|
||||
|
||||
pushScreen :: Screen -> AppState -> AppState
|
||||
pushScreen scr st = st{aPrevScreens=(aScreen st:aPrevScreens st)
|
||||
,aScreen=scr
|
||||
}
|
||||
|
||||
popScreen :: AppState -> AppState
|
||||
popScreen st@AppState{aPrevScreens=s:ss} = st{aScreen=s, aPrevScreens=ss}
|
||||
popScreen st = st
|
||||
|
||||
resetScreens :: Day -> AppState -> AppState
|
||||
resetScreens d st@AppState{aScreen=s,aPrevScreens=ss} =
|
||||
(sInit topscreen) d True $ resetDepth $ stResetFilter $ stCloseMinibuffer st{aScreen=topscreen, aPrevScreens=[]}
|
||||
where
|
||||
topscreen = case ss of _:_ -> last ss
|
||||
[] -> s
|
||||
|
||||
-- clearScreens :: AppState -> AppState
|
||||
-- clearScreens st = st{aPrevScreens=[]}
|
||||
|
||||
-- | Enter a new screen, saving the old screen & state in the
|
||||
-- navigation history and initialising the new screen's state.
|
||||
screenEnter :: Day -> Screen -> AppState -> AppState
|
||||
screenEnter d scr st = (sInit scr) d True $
|
||||
pushScreen scr
|
||||
st
|
||||
import Hledger.UI.UIState
|
||||
|
||||
-- | Draw the help dialog, called when help mode is active.
|
||||
helpDialog :: Widget
|
||||
helpDialog =
|
||||
Widget Fixed Fixed $ do
|
||||
c <- getContext
|
||||
@ -251,22 +65,21 @@ helpDialog =
|
||||
renderKey (key,desc) = withAttr (borderAttr <> "keys") (str key) <+> str " " <+> str desc
|
||||
|
||||
-- | Event handler used when help mode is active.
|
||||
helpHandle st ev =
|
||||
helpHandle :: UIState -> Event -> EventM (Next UIState)
|
||||
helpHandle ui ev =
|
||||
case ev of
|
||||
EvKey k [] | k `elem` [KEsc, KChar 'h'] -> continue $ setMode Normal st
|
||||
_ -> continue st
|
||||
EvKey k [] | k `elem` [KEsc, KChar 'h'] -> continue $ setMode Normal ui
|
||||
_ -> continue ui
|
||||
|
||||
-- | In the EventM monad, get the named current viewport's width and height,
|
||||
-- or (0,0) if the named viewport is not found.
|
||||
getViewportSize :: Name -> EventM (Int,Int)
|
||||
getViewportSize name = do
|
||||
mvp <- lookupViewport name
|
||||
let (w,h) = case mvp of
|
||||
Just vp -> vp ^. vpSize
|
||||
Nothing -> (0,0)
|
||||
-- liftIO $ putStrLn $ show (w,h)
|
||||
return (w,h)
|
||||
-- | Draw the minibuffer.
|
||||
minibuffer :: Editor -> Widget
|
||||
minibuffer ed =
|
||||
forceAttr (borderAttr <> "minibuffer") $
|
||||
hBox $
|
||||
[txt "filter: ", renderEditor ed]
|
||||
|
||||
-- | Wrap a widget in the default hledger-ui screen layout.
|
||||
defaultLayout :: Widget -> Widget -> Widget -> Widget
|
||||
defaultLayout toplabel bottomlabel =
|
||||
topBottomBorderWithLabels (str " "<+>toplabel<+>str " ") (str " "<+>bottomlabel<+>str " ") .
|
||||
margin 1 0 Nothing
|
||||
@ -274,6 +87,26 @@ defaultLayout toplabel bottomlabel =
|
||||
-- padLeftRight 1 -- XXX should reduce inner widget's width by 2, but doesn't
|
||||
-- "the layout adjusts... if you use the core combinators"
|
||||
|
||||
borderQueryStr :: String -> Widget
|
||||
borderQueryStr "" = str ""
|
||||
borderQueryStr qry = str " matching " <+> withAttr (borderAttr <> "query") (str qry)
|
||||
|
||||
borderDepthStr :: Maybe Int -> Widget
|
||||
borderDepthStr Nothing = str ""
|
||||
borderDepthStr (Just d) = str " to " <+> withAttr (borderAttr <> "depth") (str $ "depth "++show d)
|
||||
|
||||
borderKeysStr :: [(String,String)] -> Widget
|
||||
borderKeysStr keydescs =
|
||||
hBox $
|
||||
intersperse sep $
|
||||
[withAttr (borderAttr <> "keys") (str keys) <+> str ":" <+> str desc | (keys, desc) <- keydescs]
|
||||
where
|
||||
-- sep = str " | "
|
||||
sep = str " "
|
||||
|
||||
-- generic
|
||||
|
||||
topBottomBorderWithLabel :: Widget -> Widget -> Widget
|
||||
topBottomBorderWithLabel label = \wrapped ->
|
||||
Widget Greedy Greedy $ do
|
||||
c <- getContext
|
||||
@ -290,6 +123,7 @@ topBottomBorderWithLabel label = \wrapped ->
|
||||
<=>
|
||||
hBorder
|
||||
|
||||
topBottomBorderWithLabels :: Widget -> Widget -> Widget -> Widget
|
||||
topBottomBorderWithLabels toplabel bottomlabel = \wrapped ->
|
||||
Widget Greedy Greedy $ do
|
||||
c <- getContext
|
||||
@ -307,6 +141,7 @@ topBottomBorderWithLabels toplabel bottomlabel = \wrapped ->
|
||||
hBorderWithLabel bottomlabel
|
||||
|
||||
-- XXX should be equivalent to the above, but isn't (page down goes offscreen)
|
||||
_topBottomBorderWithLabel2 :: Widget -> Widget -> Widget
|
||||
_topBottomBorderWithLabel2 label = \wrapped ->
|
||||
let debugmsg = ""
|
||||
in hBorderWithLabel (label <+> str debugmsg)
|
||||
@ -340,33 +175,6 @@ margin h v mcolour = \w ->
|
||||
-- withBorderStyle (borderStyleFromChar ' ') .
|
||||
-- applyN n border
|
||||
|
||||
withBorderAttr :: Attr -> Widget -> Widget
|
||||
withBorderAttr attr = updateAttrMap (applyAttrMappings [(borderAttr, attr)])
|
||||
|
||||
-- _ui = vCenter $ vBox [ hCenter box
|
||||
-- , str " "
|
||||
-- , hCenter $ str "Press Esc to exit."
|
||||
-- ]
|
||||
|
||||
borderQueryStr :: String -> Widget
|
||||
borderQueryStr "" = str ""
|
||||
borderQueryStr qry = str " matching " <+> withAttr (borderAttr <> "query") (str qry)
|
||||
|
||||
borderDepthStr :: Maybe Int -> Widget
|
||||
borderDepthStr Nothing = str ""
|
||||
borderDepthStr (Just d) = str " to " <+> withAttr (borderAttr <> "depth") (str $ "depth "++show d)
|
||||
|
||||
borderKeysStr :: [(String,String)] -> Widget
|
||||
borderKeysStr keydescs =
|
||||
hBox $
|
||||
intersperse sep $
|
||||
[withAttr (borderAttr <> "keys") (str keys) <+> str ":" <+> str desc | (keys, desc) <- keydescs]
|
||||
where
|
||||
-- sep = str " | "
|
||||
sep = str " "
|
||||
|
||||
minibuffer :: Editor -> Widget
|
||||
minibuffer ed =
|
||||
forceAttr (borderAttr <> "minibuffer") $
|
||||
hBox $
|
||||
[txt "filter: ", renderEditor ed]
|
||||
|
||||
|
@ -92,6 +92,7 @@ executable hledger-ui
|
||||
Hledger.UI.Main
|
||||
Hledger.UI.UIOptions
|
||||
Hledger.UI.Theme
|
||||
Hledger.UI.UIState
|
||||
Hledger.UI.UITypes
|
||||
Hledger.UI.UIUtils
|
||||
Hledger.UI.AccountsScreen
|
||||
|
Loading…
Reference in New Issue
Block a user