ui: solidified register screen, added themes

- register screen:
  - smarter width-sensitive layout, with multi-commodity amounts on one line
  - items are sorted in date order
  - jumps to the latest item by default, with consistent scroll position
  - more prerendering, might speed up movement/paging slightly

- themes! --theme to select, --help to list (current themes: default, terminal, greenterm)

- border tweaks - dropped side borders, added side padding
This commit is contained in:
Simon Michael 2015-08-22 17:46:57 -07:00
parent 3a7a5d6035
commit e7aa150e52
13 changed files with 339 additions and 160 deletions

View File

@ -5,6 +5,7 @@ Re-export the modules of the hledger-ui program.
module Hledger.UI ( module Hledger.UI (
module Hledger.UI.Main, module Hledger.UI.Main,
module Hledger.UI.Options, module Hledger.UI.Options,
module Hledger.UI.Theme,
tests_Hledger_UI tests_Hledger_UI
) )
where where
@ -12,6 +13,7 @@ import Test.HUnit
import Hledger.UI.Main import Hledger.UI.Main
import Hledger.UI.Options import Hledger.UI.Options
import Hledger.UI.Theme
tests_Hledger_UI :: Test tests_Hledger_UI :: Test
tests_Hledger_UI = TestList tests_Hledger_UI = TestList

View File

@ -14,16 +14,17 @@ import Data.List
-- import Data.Monoid -- -- import Data.Monoid --
import Data.Time.Calendar (Day) import Data.Time.Calendar (Day)
import qualified Data.Vector as V import qualified Data.Vector as V
import qualified Graphics.Vty as Vty import Graphics.Vty as Vty
import Brick import Brick
import Brick.Widgets.List import Brick.Widgets.List
import Brick.Widgets.Border -- import Brick.Widgets.Border
import Brick.Widgets.Center -- import Brick.Widgets.Center
import Hledger import Hledger
import Hledger.Cli hiding (progname,prognameandversion,green) import Hledger.Cli hiding (progname,prognameandversion,green)
-- import Hledger.Cli.Options (defaultBalanceLineFormat) -- import Hledger.Cli.Options (defaultBalanceLineFormat)
import Hledger.UI.Options import Hledger.UI.Options
-- import Hledger.UI.Theme
import Hledger.UI.UITypes import Hledger.UI.UITypes
import Hledger.UI.UIUtils import Hledger.UI.UIUtils
import qualified Hledger.UI.RegisterScreen2 as RS2 (screen) import qualified Hledger.UI.RegisterScreen2 as RS2 (screen)
@ -47,7 +48,10 @@ initAccountsScreen d args st@AppState{aopts=opts, ajournal=j, aScreen=s@Accounts
--{query_=unwords' $ locArgs l} --{query_=unwords' $ locArgs l}
ropts = (reportopts_ cliopts) ropts = (reportopts_ cliopts)
{no_elide_=True} {no_elide_=True}
{query_=unwords' args} {
query_=unwords' args,
balancetype_=HistoricalBalance -- XXX balanceReport doesn't respect this yet
}
cliopts = cliopts_ opts cliopts = cliopts_ opts
initAccountsScreen _ _ _ = error "init function called with wrong screen type, should not happen" initAccountsScreen _ _ _ = error "init function called with wrong screen type, should not happen"
@ -59,15 +63,7 @@ drawAccountsScreen st@AppState{aScreen=AccountsScreen{asState=is}} = [ui]
Nothing -> "-" Nothing -> "-"
Just i -> show (i + 1)) Just i -> show (i + 1))
total = str $ show $ length $ is^.listElementsL total = str $ show $ length $ is^.listElementsL
box = borderWithLabel label $
-- hLimit 25 $
-- vLimit 15 $
renderList is (drawAccountsItem fmt)
ui = box
_ui = vCenter $ vBox [ hCenter box
, str " "
, hCenter $ str "Press Esc to exit."
]
items = listElements is items = listElements is
flat = flat_ $ reportopts_ $ cliopts_ $ aopts st flat = flat_ $ reportopts_ $ cliopts_ $ aopts st
acctcolwidth = maximum $ acctcolwidth = maximum $
@ -82,16 +78,17 @@ drawAccountsScreen st@AppState{aScreen=AccountsScreen{asState=is}} = [ui]
, FormatField False (Just 40) Nothing TotalField , FormatField False (Just 40) Nothing TotalField
] ]
ui = defaultLayout label $ renderList is (drawAccountsItem fmt)
drawAccountsScreen _ = error "draw function called with wrong screen type, should not happen" drawAccountsScreen _ = error "draw function called with wrong screen type, should not happen"
drawAccountsItem :: StringFormat -> Bool -> BalanceReportItem -> Widget drawAccountsItem :: StringFormat -> Bool -> BalanceReportItem -> Widget
drawAccountsItem fmt sel item = drawAccountsItem fmt _sel item =
let selStr i = if sel Widget Greedy Fixed $ do
then withAttr customAttr (str $ showitem i) -- c <- getContext
else str $ showitem i let
showitem = intercalate "\n" . balanceReportItemAsText defreportopts fmt showitem = intercalate "\n" . balanceReportItemAsText defreportopts fmt
in render $ str $ showitem item
selStr item
handleAccountsScreen :: AppState -> Vty.Event -> EventM (Next AppState) handleAccountsScreen :: AppState -> Vty.Event -> EventM (Next AppState)
handleAccountsScreen st@AppState{aScreen=scr@AccountsScreen{asState=is}} e = do handleAccountsScreen st@AppState{aScreen=scr@AccountsScreen{asState=is}} e = do
@ -104,8 +101,9 @@ handleAccountsScreen st@AppState{aScreen=scr@AccountsScreen{asState=is}} e = do
Vty.EvKey (Vty.KChar 'q') [] -> halt st Vty.EvKey (Vty.KChar 'q') [] -> halt st
Vty.EvKey (Vty.KLeft) [] -> continue $ popScreen st Vty.EvKey (Vty.KLeft) [] -> continue $ popScreen st
Vty.EvKey (Vty.KRight) [] -> do Vty.EvKey (Vty.KRight) [] -> do
(w,h) <- getViewportSize "accounts" let st' = screenEnter d args RS2.screen st
continue $ screenEnter d args RS2.screen{rs2Size=(w,h)} st vScrollToBeginning $ viewportScroll "register"
continue st'
where where
args = case listSelectedElement is of args = case listSelectedElement is of
Just (_, ((acct, _, _), _)) -> ["acct:"++accountNameToAccountRegex acct] Just (_, ((acct, _, _), _)) -> ["acct:"++accountNameToAccountRegex acct]

View File

@ -12,10 +12,11 @@ module Hledger.UI.Main where
-- import Control.Applicative -- import Control.Applicative
-- import Control.Lens ((^.)) -- import Control.Lens ((^.))
import Control.Monad import Control.Monad
-- import Control.Monad.IO.Class (liftIO)
-- import Data.Default -- import Data.Default
-- import Data.Monoid -- -- import Data.Monoid --
-- import Data.List -- import Data.List
-- import Data.Maybe import Data.Maybe
-- import Data.Time.Calendar -- import Data.Time.Calendar
-- import Safe -- import Safe
import System.Exit import System.Exit
@ -27,19 +28,14 @@ import Hledger
import Hledger.Cli hiding (progname,prognameandversion,green) import Hledger.Cli hiding (progname,prognameandversion,green)
import Hledger.UI.Options import Hledger.UI.Options
import Hledger.UI.UITypes import Hledger.UI.UITypes
import Hledger.UI.UIUtils -- import Hledger.UI.UIUtils
import Hledger.UI.Theme
import Hledger.UI.AccountsScreen as AS import Hledger.UI.AccountsScreen as AS
-- import Hledger.UI.RegisterScreen as RS -- import Hledger.UI.RegisterScreen as RS
import Hledger.UI.RegisterScreen2 as RS2 -- import Hledger.UI.RegisterScreen2 as RS2
---------------------------------------------------------------------- ----------------------------------------------------------------------
-- | The available screens.
appScreens = [
AS.screen
,RS2.screen
]
main :: IO () main :: IO ()
main = do main = do
opts <- getHledgerUIOpts opts <- getHledgerUIOpts
@ -65,8 +61,10 @@ runBrickUi opts j = do
d <- getCurrentDay d <- getCurrentDay
let let
theme = maybe defaultTheme (fromMaybe defaultTheme . getTheme) $
maybestringopt "theme" $ rawopts_ $ cliopts_ opts
args = words' $ query_ $ reportopts_ $ cliopts_ opts args = words' $ query_ $ reportopts_ $ cliopts_ opts
scr = head appScreens scr = AS.screen
st = (sInitFn scr) d args st = (sInitFn scr) d args
AppState{ AppState{
aopts=opts aopts=opts
@ -80,10 +78,11 @@ runBrickUi opts j = do
app = App { app = App {
appLiftVtyEvent = id appLiftVtyEvent = id
, appStartEvent = return , appStartEvent = return
, appAttrMap = const customAttrMap , appAttrMap = const theme
, appChooseCursor = showFirstCursor , appChooseCursor = showFirstCursor
, appHandleEvent = \st ev -> (sHandleFn $ aScreen st) st ev , appHandleEvent = \st ev -> (sHandleFn $ aScreen st) st ev
, appDraw = \st -> (sDrawFn $ aScreen st) st , appDraw = \st -> (sDrawFn $ aScreen st) st
} }
void $ defaultMain app st void $ defaultMain app st

View File

@ -5,10 +5,12 @@
module Hledger.UI.Options module Hledger.UI.Options
where where
import Data.List (intercalate)
import System.Console.CmdArgs import System.Console.CmdArgs
import System.Console.CmdArgs.Explicit import System.Console.CmdArgs.Explicit
import Hledger.Cli hiding (progname,version,prognameandversion) import Hledger.Cli hiding (progname,version,prognameandversion)
import Hledger.UI.Theme (themeNames)
progname, version :: String progname, version :: String
progname = "hledger-ui" progname = "hledger-ui"
@ -25,6 +27,7 @@ uiflags = [
,flagNone ["flat"] (\opts -> setboolopt "flat" opts) "show full account names, unindented" ,flagNone ["flat"] (\opts -> setboolopt "flat" opts) "show full account names, unindented"
,flagReq ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "with --flat, omit this many leading account name components" ,flagReq ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "with --flat, omit this many leading account name components"
,flagReq ["format"] (\s opts -> Right $ setopt "format" s opts) "FORMATSTR" "use this custom line format" ,flagReq ["format"] (\s opts -> Right $ setopt "format" s opts) "FORMATSTR" "use this custom line format"
,flagReq ["theme"] (\s opts -> Right $ setopt "theme" s opts) "THEME" ("use this custom display theme ("++intercalate ", " themeNames++")")
,flagNone ["no-elide"] (\opts -> setboolopt "no-elide" opts) "no eliding at all, stronger than --empty" ,flagNone ["no-elide"] (\opts -> setboolopt "no-elide" opts) "no eliding at all, stronger than --empty"
-- ,flagNone ["no-total"] (\opts -> setboolopt "no-total" opts) "don't show the final total" -- ,flagNone ["no-total"] (\opts -> setboolopt "no-total" opts) "don't show the final total"
] ]
@ -66,6 +69,10 @@ toUIOpts rawopts = do
checkUIOpts :: UIOpts -> IO UIOpts checkUIOpts :: UIOpts -> IO UIOpts
checkUIOpts opts = do checkUIOpts opts = do
checkCliOpts $ cliopts_ opts checkCliOpts $ cliopts_ opts
case maybestringopt "theme" $ rawopts_ $ cliopts_ opts of
Just t | not $ elem t themeNames ->
optserror $ "invalid theme name: "++t
_ -> return ()
return opts return opts
getHledgerUIOpts :: IO UIOpts getHledgerUIOpts :: IO UIOpts

View File

@ -7,6 +7,7 @@ module Hledger.UI.RegisterScreen
where where
import Control.Lens ((^.)) import Control.Lens ((^.))
-- import Control.Monad.IO.Class (liftIO)
import Data.List import Data.List
import Data.Time.Calendar (Day) import Data.Time.Calendar (Day)
import qualified Data.Vector as V import qualified Data.Vector as V
@ -42,7 +43,7 @@ initRegisterScreen d args st@AppState{aopts=opts, ajournal=j, aScreen=s@Register
-- query_="cur:\\$"} -- XXX limit to one commodity to ensure one-line items -- query_="cur:\\$"} -- XXX limit to one commodity to ensure one-line items
--{query_=unwords' $ locArgs l} --{query_=unwords' $ locArgs l}
ropts = (reportopts_ cliopts) ropts = (reportopts_ cliopts)
{query_=unwords' args} { query_=unwords' args }
cliopts = cliopts_ opts cliopts = cliopts_ opts
initRegisterScreen _ _ _ = error "init function called with wrong screen type, should not happen" initRegisterScreen _ _ _ = error "init function called with wrong screen type, should not happen"
@ -72,12 +73,17 @@ drawRegisterScreen _ = error "draw function called with wrong screen type, shoul
drawRegisterItem :: Bool -> PostingsReportItem -> Widget drawRegisterItem :: Bool -> PostingsReportItem -> Widget
drawRegisterItem sel item = drawRegisterItem sel item =
-- (w,_) <- getViewportSize "register" -- getCurrentViewportSize
-- st@AppState{aopts=opts} <- getAppState
-- let opts' = opts{width_=Just $ show w}
let selStr i = if sel let selStr i = if sel
then withAttr customAttr (str $ showitem i) then {- withAttr selectedAttr -} str $ showitem i
else str $ showitem i else str $ showitem i
showitem (_,_,_,p,b) = showitem (_,_,_,p,b) =
intercalate ", " $ map strip $ lines $ intercalate ", " $ map strip $ lines $
postingsReportItemAsText defcliopts $ postingsReportItemAsText defcliopts{width_=Just "160"} $ -- XXX
mkpostingsReportItem True True PrimaryDate Nothing p b mkpostingsReportItem True True PrimaryDate Nothing p b
-- fmt = BottomAligned [ -- fmt = BottomAligned [
-- FormatField False (Just 20) Nothing TotalField -- FormatField False (Just 20) Nothing TotalField
@ -89,7 +95,7 @@ drawRegisterItem sel item =
selStr item selStr item
handleRegisterScreen :: AppState -> Vty.Event -> EventM (Next AppState) handleRegisterScreen :: AppState -> Vty.Event -> EventM (Next AppState)
handleRegisterScreen st@AppState{aScreen=s@RegisterScreen{rsState=is}} e = handleRegisterScreen st@AppState{aopts=_opts,aScreen=s@RegisterScreen{rsState=is}} e = do
case e of case e of
Vty.EvKey Vty.KEsc [] -> halt st Vty.EvKey Vty.KEsc [] -> halt st
Vty.EvKey (Vty.KChar 'q') [] -> halt st Vty.EvKey (Vty.KChar 'q') [] -> halt st

View File

@ -1,6 +1,6 @@
-- The register screen, showing account postings, like the CLI register command. -- The register screen, showing account postings, like the CLI register command.
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings, FlexibleContexts #-}
module Hledger.UI.RegisterScreen2 module Hledger.UI.RegisterScreen2
(screen) (screen)
@ -8,111 +8,147 @@ where
import Control.Lens ((^.)) import Control.Lens ((^.))
-- import Control.Monad.IO.Class (liftIO) -- import Control.Monad.IO.Class (liftIO)
import Data.List -- import Data.List
import Data.List.Split (splitOn) import Data.List.Split (splitOn)
-- import Data.Maybe
import Data.Time.Calendar (Day) import Data.Time.Calendar (Day)
import qualified Data.Vector as V import qualified Data.Vector as V
import qualified Graphics.Vty as Vty import Graphics.Vty as Vty
import Brick import Brick
import Brick.Widgets.List import Brick.Widgets.List
import Brick.Widgets.Border -- import Brick.Widgets.Border
import Brick.Widgets.Center -- import Brick.Widgets.Border.Style
-- import Brick.Widgets.Center
-- import Text.Printf
import Hledger import Hledger
import Hledger.Cli hiding (progname,prognameandversion,green) import Hledger.Cli hiding (progname,prognameandversion,green)
import Hledger.UI.Options import Hledger.UI.Options
-- import Hledger.UI.Theme
import Hledger.UI.UITypes import Hledger.UI.UITypes
import Hledger.UI.UIUtils import Hledger.UI.UIUtils
screen = RegisterScreen2{ screen = RegisterScreen2{
rs2State = list "register" V.empty 1 rs2State = list "register" V.empty 1
,rs2Size = (0,0)
,sInitFn = initRegisterScreen2 ,sInitFn = initRegisterScreen2
,sDrawFn = drawRegisterScreen2 ,sDrawFn = drawRegisterScreen2
,sHandleFn = handleRegisterScreen2 ,sHandleFn = handleRegisterScreen2
} }
initRegisterScreen2 :: Day -> [String] -> AppState -> AppState initRegisterScreen2 :: Day -> [String] -> AppState -> AppState
initRegisterScreen2 d args st@AppState{aopts=opts, ajournal=j, aScreen=s@RegisterScreen2{rs2Size=size}} = initRegisterScreen2 d args st@AppState{aopts=opts, ajournal=j, aScreen=s@RegisterScreen2{}} =
st{aScreen=s{rs2State=is'}} st{aScreen=s{rs2State=l}}
where where
is' = -- gather arguments and queries
-- listMoveTo (length items) $ ropts = (reportopts_ $ cliopts_ opts)
list (Name "register") (V.fromList items') 1 {
query_=unwords' args,
balancetype_=HistoricalBalance
}
-- XXX temp
curacct = drop 5 $ head args -- should be "acct:..."
thisacctq = Acct $ curacct -- XXX why is this excluding subs: accountNameToAccountRegex curacct
q = queryFromOpts d ropts
-- query_="cur:\\$"} -- XXX limit to one commodity to ensure one-line items
--{query_=unwords' $ locArgs l}
-- XXX temporary hack: include saved viewport size in list elements -- run a transactions report, most recent last
-- for element draw function (_label,items') = accountTransactionsReport ropts j thisacctq q
items' = zip (repeat size) items items = reverse items'
(_label,items) = accountTransactionsReport ropts j thisacctq q
where -- pre-render all items; these will be the List elements. This helps calculate column widths.
-- XXX temp displayitem (_, t, _issplit, otheracctsstr, change, bal) =
curacct = drop 5 $ head args -- should be "acct:..." (showDate $ tdate t
thisacctq = Acct $ curacct -- XXX why is this excluding subs: accountNameToAccountRegex curacct ,tdescription t
,case splitOn ", " otheracctsstr of
[s] -> s
_ -> "<split>"
,showMixedAmountOneLineWithoutPrice change
,showMixedAmountOneLineWithoutPrice bal
)
displayitems = map displayitem items
-- build the List, moving the selection to the end
l = listMoveTo (length items) $
list (Name "register") (V.fromList displayitems) 1
-- (listName someList)
q = queryFromOpts d ropts
-- query_="cur:\\$"} -- XXX limit to one commodity to ensure one-line items
--{query_=unwords' $ locArgs l}
ropts = (reportopts_ cliopts)
{query_=unwords' args}
cliopts = cliopts_ opts
initRegisterScreen2 _ _ _ = error "init function called with wrong screen type, should not happen" initRegisterScreen2 _ _ _ = error "init function called with wrong screen type, should not happen"
drawRegisterScreen2 :: AppState -> [Widget] drawRegisterScreen2 :: AppState -> [Widget]
drawRegisterScreen2 AppState{aopts=_opts, aScreen=RegisterScreen2{rs2State=is}} = [ui] drawRegisterScreen2 AppState{aopts=_opts, aScreen=RegisterScreen2{rs2State=l}} = [ui]
where where
label = str "Transaction " label = str "Transaction "
<+> cur <+> cur
<+> str " of " <+> str " of "
<+> total <+> total
<+> str " to/from this account" -- " <+> str query <+> "and subaccounts" <+> str " to/from this account" -- " <+> str query <+> "and subaccounts"
cur = str $ case is^.(listSelectedL) of cur = str $ case l^.listSelectedL of
Nothing -> "-" Nothing -> "-"
Just i -> show (i + 1) Just i -> show (i + 1)
total = str $ show $ length $ is^.(listElementsL) total = str $ show $ length displayitems
displayitems = V.toList $ l^.listElementsL
-- query = query_ $ reportopts_ $ cliopts_ opts -- query = query_ $ reportopts_ $ cliopts_ opts
box = borderWithLabel label $
-- hLimit 25 $ ui = Widget Greedy Greedy $ do
-- vLimit 15 $
renderList is drawRegisterItem -- calculate column widths, based on current available width
ui = box c <- getContext
_ui = vCenter $ vBox [ hCenter box let
, str " " totalwidth = c^.availWidthL - 2 -- XXX trimmed.. for the margin ?
, hCenter $ str "Press Esc to exit."
] -- the date column is fixed width
datewidth = 10
-- multi-commodity amounts rendered on one line can be
-- arbitrarily wide. Give the two amounts as much space as
-- they need, while reserving a minimum of space for other
-- columns and whitespace. If they don't get all they need,
-- allocate it to them proportionally to their maximum widths.
maxamtswidth = max 0 (totalwidth - 21)
changewidth' = maximum' $ map (length . fourth5) displayitems
balwidth' = maximum' $ map (length . fifth5) displayitems
changewidthproportion = (changewidth' + balwidth') `div` changewidth'
maxchangewidth = maxamtswidth `div` changewidthproportion
maxbalwidth = maxamtswidth - maxchangewidth
changewidth = min maxchangewidth changewidth'
balwidth = min maxbalwidth balwidth'
-- assign the remaining space to the description and accounts columns
maxdescacctswidth = totalwidth - 17 - changewidth - balwidth
-- allocating proportionally.
-- descwidth' = maximum' $ map (length . second5) displayitems
-- acctswidth' = maximum' $ map (length . third5) displayitems
-- descwidthproportion = (descwidth' + acctswidth') `div` descwidth'
-- maxdescwidth = min (maxdescacctswidth - 7) (maxdescacctswidth `div` descwidthproportion)
-- maxacctswidth = maxdescacctswidth - maxdescwidth
-- descwidth = min maxdescwidth descwidth'
-- acctswidth = min maxacctswidth acctswidth'
-- allocating equally.
descwidth = maxdescacctswidth `div` 2
acctswidth = maxdescacctswidth - descwidth
colwidths = (datewidth,descwidth,acctswidth,changewidth,balwidth)
render $ defaultLayout label $ renderList l (drawRegisterItem colwidths)
drawRegisterScreen2 _ = error "draw function called with wrong screen type, should not happen" drawRegisterScreen2 _ = error "draw function called with wrong screen type, should not happen"
drawRegisterItem :: Bool -> ((Int,Int), AccountTransactionsReportItem) -> Widget drawRegisterItem :: (Int,Int,Int,Int,Int) -> Bool -> (String,String,String,String,String) -> Widget
drawRegisterItem sel ((w,_h),item) = drawRegisterItem (datewidth,descwidth,acctswidth,changewidth,balwidth) _sel (date,desc,accts,change,bal) =
Widget Greedy Fixed $ do
-- (w,_) <- getViewportSize "register" -- getCurrentViewportSize render $
-- st@AppState{aopts=opts} <- getAppState str (padright datewidth $ elideRight datewidth date) <+>
-- let opts' = opts{width_=Just $ show w} str " " <+>
str (padright descwidth $ elideRight descwidth desc) <+>
let selStr i = if sel str " " <+>
then withAttr customAttr (str $ showitem i) str (padright acctswidth $ elideLeft acctswidth $ accts) <+>
else str $ showitem i str " " <+>
showitem (_origt,t,split,acctsstr,postedamt,totalamt) = str (padleft changewidth $ elideLeft changewidth change) <+>
-- make a fake posting to render str " " <+>
let p = nullposting{ str (padleft balwidth $ elideLeft balwidth bal)
pdate=Just $ tdate t
,paccount=if split then intercalate ", " acctnames ++" (split)" else acctsstr
-- XXX elideAccountName doesn't elide combined split names well
,pamount=postedamt
,ptransaction=Just t
}
acctnames = nub $ sort $ splitOn ", " acctsstr -- XXX
in
intercalate ", " $ map strip $ lines $
postingsReportItemAsText defcliopts{width_=Just (show w)} $
mkpostingsReportItem True True PrimaryDate Nothing p totalamt
-- fmt = BottomAligned [
-- FormatField False (Just 20) Nothing TotalField
-- , FormatLiteral " "
-- , FormatField True (Just 2) Nothing DepthSpacerField
-- , FormatField True Nothing Nothing AccountField
-- ]
in
selStr item
handleRegisterScreen2 :: AppState -> Vty.Event -> EventM (Next AppState) handleRegisterScreen2 :: AppState -> Vty.Event -> EventM (Next AppState)
handleRegisterScreen2 st@AppState{aopts=_opts,aScreen=s@RegisterScreen2{rs2State=is}} e = do handleRegisterScreen2 st@AppState{aopts=_opts,aScreen=s@RegisterScreen2{rs2State=is}} e = do

View File

@ -1,34 +1,99 @@
---------------------------------------------------------------------- -- | The all-important theming engine!
-- Theme --
-- the all-important theming engine! -- Cf
-- https://hackage.haskell.org/package/vty/docs/Graphics-Vty-Attributes.html
-- http://hackage.haskell.org/package/brick/docs/Brick-AttrMap.html
-- http://hackage.haskell.org/package/brick-0.1/docs/Brick-Util.html
-- http://hackage.haskell.org/package/brick-0.1/docs/Brick-Widgets-Core.html#g:5
-- http://hackage.haskell.org/package/brick-0.1/docs/Brick-Widgets-Border.html
-- theme = Restrained
-- -- theme = Colorful
-- -- theme = Blood
-- data UITheme = Restrained | Colorful | Blood {-# LANGUAGE OverloadedStrings #-}
-- (defaultattr, module Hledger.UI.Theme (
-- currentlineattr, defaultTheme
-- statusattr ,getTheme
-- ) = case theme of ,themes
-- Restrained -> (defAttr ,themeNames
-- ,defAttr `withStyle` bold ) where
-- ,defAttr `withStyle` reverseVideo
-- )
-- Colorful -> (defAttr `withStyle` reverseVideo
-- ,defAttr `withForeColor` white `withBackColor` red
-- ,defAttr `withForeColor` black `withBackColor` green
-- )
-- Blood -> (defAttr `withStyle` reverseVideo
-- ,defAttr `withForeColor` white `withBackColor` red
-- ,defAttr `withStyle` reverseVideo
-- )
-- -- halfbrightattr = defAttr `withStyle` dim import qualified Data.Map as M
-- -- reverseattr = defAttr `withStyle` reverseVideo import Data.Maybe
-- -- redattr = defAttr `withForeColor` red import Data.Monoid
-- -- greenattr = defAttr `withForeColor` green import Graphics.Vty
-- -- reverseredattr = defAttr `withStyle` reverseVideo `withForeColor` red import Brick
-- -- reversegreenattr= defAttr `withStyle` reverseVideo `withForeColor` green import Brick.Widgets.Border
import Brick.Widgets.List
defaultTheme :: AttrMap
defaultTheme = fromMaybe (snd $ head themesList) $ getTheme "white"
-- the theme named here should exist;
-- otherwise it will take the first one from the list,
-- which must be non-empty.
-- | Look up the named theme, if it exists.
getTheme :: String -> Maybe AttrMap
getTheme name = M.lookup name themes
-- | A selection of named themes specifying terminal colours and styles.
-- One of these is active at a time.
--
-- A hledger-ui theme is a vty/brick AttrMap. Each theme specifies a
-- default style (Attr), plus extra styles which are applied when
-- their (hierarchical) name matches the widget rendering context. Eg
-- when rendering a widget named "b" which is inside a widget named
-- "a", the following styles will be applied if they exist: the
-- default style, then a style named "a", and finally a style named
-- "a" <> "b".
--
themes :: M.Map String AttrMap
themes = M.fromList themesList
themeNames :: [String]
themeNames = map fst themesList
(&) = withStyle
themesList :: [(String, AttrMap)]
themesList = [
("default", attrMap
(black `on` white & bold) [ -- default style for this theme
(borderAttr , white `on` black),
-- ("normal" , black `on` white),
("list" , black `on` white), -- regular list items
("list" <> "selected" , white `on` blue & bold) -- selected list items
-- ("list" <> "selected" , black `on` brightYellow),
-- ("list" <> "accounts" , white `on` brightGreen),
-- ("list" <> "amount" , black `on` white & bold)
]),
("terminal", attrMap
defAttr [ -- use the current terminal's default style
(borderAttr , white `on` black),
-- ("normal" , defAttr),
(listAttr , defAttr),
(listSelectedAttr , defAttr & reverseVideo & bold)
-- ("status" , defAttr & reverseVideo)
]),
("greenterm", attrMap
(green `on` black) [
-- (listAttr , green `on` black),
(listSelectedAttr , black `on` green & bold)
])
-- ("colorful", attrMap
-- defAttr [
-- (listAttr , defAttr & reverseVideo),
-- (listSelectedAttr , defAttr `withForeColor` white `withBackColor` red)
-- -- ("status" , defAttr `withForeColor` black `withBackColor` green)
-- ])
]
-- halfbrightattr = defAttr & dim
-- reverseattr = defAttr & reverseVideo
-- redattr = defAttr `withForeColor` red
-- greenattr = defAttr `withForeColor` green
-- reverseredattr = defAttr & reverseVideo `withForeColor` red
-- reversegreenattr= defAttr & reverseVideo `withForeColor` green

View File

@ -37,8 +37,7 @@ data Screen =
,sDrawFn :: AppState -> [Widget] ,sDrawFn :: AppState -> [Widget]
} }
| RegisterScreen2 { | RegisterScreen2 {
rs2Size :: (Int,Int) -- ^ XXX prev screen's viewport size on entering this screen rs2State :: List (String,String,String,String,String)
,rs2State :: List ((Int,Int), AccountTransactionsReportItem)
,sInitFn :: Day -> [String] -> AppState -> AppState ,sInitFn :: Day -> [String] -> AppState -> AppState
,sHandleFn :: AppState -> V.Event -> EventM (Next AppState) ,sHandleFn :: AppState -> V.Event -> EventM (Next AppState)
,sDrawFn :: AppState -> [Widget] ,sDrawFn :: AppState -> [Widget]

View File

@ -4,23 +4,27 @@ module Hledger.UI.UIUtils (
pushScreen pushScreen
,popScreen ,popScreen
,screenEnter ,screenEnter
,attrMap
,customAttrMap
,customAttr
,getViewportSize ,getViewportSize
,margin
,withBorderAttr
,topBottomBorderWithLabel
,defaultLayout
) where ) where
import Control.Lens ((^.)) import Control.Lens ((^.))
-- import Control.Monad -- import Control.Monad
import Control.Monad.IO.Class -- import Control.Monad.IO.Class
-- import Data.Default -- import Data.Default
import Data.Monoid -- -- import Data.Monoid --
import Data.Time.Calendar (Day) import Data.Time.Calendar (Day)
import qualified Graphics.Vty as V
import Brick import Brick
import Brick.Widgets.List -- import Brick.Widgets.List
import Brick.Widgets.Border
import Brick.Widgets.Border.Style
import Graphics.Vty as Vty
import Hledger.UI.UITypes import Hledger.UI.UITypes
import Hledger.Utils (applyN)
pushScreen :: Screen -> AppState -> AppState pushScreen :: Screen -> AppState -> AppState
pushScreen scr st = st{aPrevScreens=(aScreen st:aPrevScreens st) pushScreen scr st = st{aPrevScreens=(aScreen st:aPrevScreens st)
@ -43,15 +47,6 @@ screenEnter d args scr st = (sInitFn scr) d args $
pushScreen scr pushScreen scr
st st
customAttrMap :: AttrMap
customAttrMap = attrMap V.defAttr
[ (listAttr, V.white `on` V.blue)
, (listSelectedAttr, V.black `on` V.white)
-- , (customAttr, fg V.cyan)
]
customAttr :: AttrName
-- | In the EventM monad, get the named current viewport's width and height, -- | In the EventM monad, get the named current viewport's width and height,
-- or (0,0) if the named viewport is not found. -- or (0,0) if the named viewport is not found.
getViewportSize :: Name -> EventM (Int,Int) getViewportSize :: Name -> EventM (Int,Int)
@ -60,6 +55,55 @@ getViewportSize name = do
let (w,h) = case mvp of let (w,h) = case mvp of
Just vp -> vp ^. vpSize Just vp -> vp ^. vpSize
Nothing -> (0,0) Nothing -> (0,0)
-- liftIO $ putStrLn $ show (w,h)
return (w,h) return (w,h)
customAttr = listSelectedAttr <> "custom" defaultLayout label =
topBottomBorderWithLabel label .
margin 1 0 Nothing
-- margin 1 0 (Just white)
topBottomBorderWithLabel label = \wrapped ->
Widget Greedy Greedy $ do
c <- getContext
let (_w,h) = (c^.availWidthL, c^.availHeightL)
h' = h - 2
wrapped' = vLimit (h') wrapped
debugmsg =
""
-- " debug: "++show (_w,h')
render $
hBorderWithLabel (label <+> str debugmsg)
<=>
wrapped'
<=>
hBorder
-- | Wrap a widget in a margin with the given horizontal and vertical
-- thickness, using the current background colour or the specified
-- colour. XXX May disrupt border style of inner widgets.
margin :: Int -> Int -> Maybe Color -> Widget -> Widget
margin h v mcolour = \w ->
Widget Greedy Greedy $ do
c <- getContext
let w' = vLimit (c^.availHeightL - v*2) $ hLimit (c^.availWidthL - h*2) w
attr = maybe currentAttr (\c -> c `on` c) mcolour
render $
withBorderAttr attr $
withBorderStyle (borderStyleFromChar ' ') $
applyN v (hBorder <=>) $
applyN h (vBorder <+>) $
applyN v (<=> hBorder) $
applyN h (<+> vBorder) $
w'
-- withBorderAttr attr .
-- withBorderStyle (borderStyleFromChar ' ') .
-- applyN n border
withBorderAttr attr = updateAttrMap (applyAttrMappings [(borderAttr, attr)])
-- _ui = vCenter $ vBox [ hCenter box
-- , str " "
-- , hCenter $ str "Press Esc to exit."
-- ]

View File

@ -8,17 +8,31 @@ hledger-ui currently allows browsing the balance, register and print
reports, with drill-down and scrolling. reports, with drill-down and scrolling.
Backlog: # HACKING
show journal entries
## Backlog:
```
merge to master
brick release
make it more useful
register
simplify/remove unhelpful account names ?
show journal entries
transaction dialog / journal screen
bs/is/cf-ish reports
save custom reports
fix -H fix -H
fix --drop fix --drop
track current account better track current account better
show it in register title
track current query better track current query better
search search in page
filter adjust query
depth adjustment adjust depth
use color, selectable themes
switch to next brick release switch to next brick release
reg: use full width
reg2: find subaccounts' transactions better reg2: find subaccounts' transactions better
keep cursor at bottom of screen if jumping to end keep cursor at bottom of screen if jumping to end
add add
@ -28,3 +42,5 @@ reload
on screen change on screen change
on redraw on redraw
on file change on file change
```

View File

@ -56,6 +56,7 @@ executable hledger-ui
, base >= 3 && < 5 , base >= 3 && < 5
, brick , brick
, cmdargs >= 0.8 , cmdargs >= 0.8
, containers
, data-default , data-default
, HUnit , HUnit
, lens >= 4.12.3 && < 4.13 , lens >= 4.12.3 && < 4.13
@ -74,6 +75,7 @@ executable hledger-ui
Hledger.UI Hledger.UI
Hledger.UI.Main Hledger.UI.Main
Hledger.UI.Options Hledger.UI.Options
Hledger.UI.Theme
Hledger.UI.UITypes Hledger.UI.UITypes
Hledger.UI.UIUtils Hledger.UI.UIUtils
Hledger.UI.AccountsScreen Hledger.UI.AccountsScreen

View File

@ -66,6 +66,7 @@ executables:
- hledger-lib == 0.26.98 - hledger-lib == 0.26.98
- base >= 3 && < 5 - base >= 3 && < 5
- cmdargs >= 0.8 - cmdargs >= 0.8
- containers
- HUnit - HUnit
- safe >= 0.2 - safe >= 0.2
- split >= 0.1 && < 0.3 - split >= 0.1 && < 0.3

View File

@ -113,6 +113,10 @@ tests_postingsReportAsText = [
-- --
-- date and description are shown for the first posting of a transaction only. -- date and description are shown for the first posting of a transaction only.
-- --
-- Returns a string which can be multi-line, eg if the running balance
-- has multiple commodities. Does not yet support formatting control
-- like balance reports.
--
postingsReportItemAsText :: CliOpts -> PostingsReportItem -> String postingsReportItemAsText :: CliOpts -> PostingsReportItem -> String
postingsReportItemAsText opts (mdate, menddate, mdesc, p, b) = postingsReportItemAsText opts (mdate, menddate, mdesc, p, b) =
intercalate "\n" $ intercalate "\n" $