hledger/hledger-ui/Hledger/UI/UIUtils.hs
Simon Michael 42a4a4a7e4 ui: use "border" instead of borderAttr everywhere
Assume it won't change, it's clearer and more consistent.
2018-10-23 06:43:21 -07:00

310 lines
12 KiB
Haskell

{- | Rendering & misc. helpers. -}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
module Hledger.UI.UIUtils (
borderDepthStr
,borderKeysStr
,borderKeysStr'
,borderPeriodStr
,borderQueryStr
,defaultLayout
,helpDialog
,helpHandle
,minibuffer
,moveDownEvents
,moveLeftEvents
,moveRightEvents
,moveUpEvents
,normaliseMovementKeys
,replaceHiddenAccountsNameWith
,scrollSelectionToMiddle
)
where
import Brick
import Brick.Widgets.Border
import Brick.Widgets.Border.Style
import Brick.Widgets.Dialog
import Brick.Widgets.Edit
import Brick.Widgets.List
import Control.Monad.IO.Class
import Data.List
import Data.Maybe
#if !(MIN_VERSION_base(4,11,0))
import Data.Monoid
#endif
import Graphics.Vty (Event(..),Key(..),Modifier(..),Color,Attr,currentAttr)
import Lens.Micro.Platform
import System.Environment
import Hledger hiding (Color)
import Hledger.Cli (CliOpts)
import Hledger.Cli.DocFiles
import Hledger.UI.UITypes
import Hledger.UI.UIState
-- ui
-- | Draw the help dialog, called when help mode is active.
helpDialog :: CliOpts -> Widget Name
helpDialog _copts =
Widget Fixed Fixed $ do
c <- getContext
render $
withDefAttr "help" $
renderDialog (dialog (Just "Help (?/LEFT/ESC to close)") Nothing (c^.availWidthL)) $ -- (Just (0,[("ok",())]))
padAll 1 $
vBox [
hBox [
padRight (Pad 1) $
vBox [
withAttr ("help" <> "heading") $ str "Navigation"
,renderKey ("UP/DOWN/PUP/PDN/HOME/END/emacs/vi keys", "")
,str " move selection"
,renderKey ("RIGHT", "show more detail")
,renderKey ("LEFT ", "go back")
,renderKey ("ESC ", "cancel or reset")
,str " "
,withAttr ("help" <> "heading") $ str "Report period"
,renderKey ("S-DOWN /S-UP ", "shrink/grow period")
,renderKey ("S-RIGHT/S-LEFT", "next/previous period")
,renderKey ("t ", "set period to today")
,str " "
,withAttr ("help" <> "heading") $ str "Accounts screen"
,renderKey ("-+0123456789 ", "set depth limit")
,renderKey ("T ", "toggle tree/flat mode")
,renderKey ("H ", "period change/historical end balance")
,str " "
,withAttr ("help" <> "heading") $ str "Register screen"
,renderKey ("T ", "toggle subaccount txns\n(and accounts screen tree/flat mode)")
,renderKey ("H ", "show period total/historical total")
,str " "
]
,padLeft (Pad 1) $ padRight (Pad 0) $
vBox [
withAttr ("help" <> "heading") $ str "Filtering"
,renderKey ("/ ", "set a filter query")
,renderKey ("UPC ", "show unmarked/pending/cleared")
,renderKey ("F ", "show future/present txns")
,renderKey ("R ", "show real/all postings")
,renderKey ("Z ", "show nonzero/all amounts")
,renderKey ("DEL ", "remove filters")
,str " "
,withAttr ("help" <> "heading") $ str "Help"
,renderKey ("? ", "toggle this help")
,renderKey ("pmi ", "(with help showing)\nshow manual in pager/man/info")
,str " "
,withAttr ("help" <> "heading") $ str "Other"
,renderKey ("a ", "add transaction (hledger add)")
,renderKey ("A ", "add transaction (hledger-iadd)")
,renderKey ("E ", "open editor")
,renderKey ("I ", "toggle balance assertions")
,renderKey ("C-l ", "redraw & recenter")
,renderKey ("g ", "reload data")
,renderKey ("q ", "quit")
]
]
-- ,vBox [
-- str " "
-- ,hCenter $ padLeftRight 1 $
-- hCenter (str "MANUAL")
-- <=>
-- hCenter (hBox [
-- renderKey ("t", "text")
-- ,str " "
-- ,renderKey ("m", "man page")
-- ,str " "
-- ,renderKey ("i", "info")
-- ])
-- ]
]
where
renderKey (key,desc) = withAttr ("help" <> "key") (str key) <+> str " " <+> str desc
-- | Event handler used when help mode is active.
-- May invoke $PAGER, less, man or info, which is likely to fail on MS Windows, TODO.
helpHandle :: UIState -> BrickEvent Name AppEvent -> EventM Name (Next UIState)
helpHandle ui ev = do
pagerprog <- liftIO $ fromMaybe "less" <$> lookupEnv "PAGER"
case ev of
VtyEvent e | e `elem` (moveLeftEvents ++ [EvKey KEsc [], EvKey (KChar '?') []]) -> continue $ setMode Normal ui
VtyEvent (EvKey (KChar 'p') []) -> suspendAndResume $ runPagerForTopic pagerprog "hledger-ui" >> return ui'
VtyEvent (EvKey (KChar 'm') []) -> suspendAndResume $ runManForTopic "hledger-ui" >> return ui'
VtyEvent (EvKey (KChar 'i') []) -> suspendAndResume $ runInfoForTopic "hledger-ui" >> return ui'
_ -> continue ui
where
ui' = setMode Normal ui
-- | Draw the minibuffer.
minibuffer :: Editor String Name -> Widget Name
minibuffer ed =
forceAttr ("border" <> "minibuffer") $
hBox $
#if MIN_VERSION_brick(0,19,0)
[txt "filter: ", renderEditor (str . unlines) True ed]
#else
[txt "filter: ", renderEditor True ed]
#endif
-- | Wrap a widget in the default hledger-ui screen layout.
defaultLayout :: Widget Name -> Widget Name -> Widget Name -> Widget Name
defaultLayout toplabel bottomlabel =
topBottomBorderWithLabels (str " "<+>toplabel<+>str " ") (str " "<+>bottomlabel<+>str " ") .
margin 1 0 Nothing
-- topBottomBorderWithLabel2 label .
-- 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 Name
borderQueryStr "" = str ""
borderQueryStr qry = str " matching " <+> withAttr ("border" <> "query") (str qry)
borderDepthStr :: Maybe Int -> Widget Name
borderDepthStr Nothing = str ""
borderDepthStr (Just d) = str " to " <+> withAttr ("border" <> "query") (str $ "depth "++show d)
borderPeriodStr :: String -> Period -> Widget Name
borderPeriodStr _ PeriodAll = str ""
borderPeriodStr preposition p = str (" "++preposition++" ") <+> withAttr ("border" <> "query") (str $ showPeriod p)
borderKeysStr :: [(String,String)] -> Widget Name
borderKeysStr = borderKeysStr' . map (\(a,b) -> (a, str b))
borderKeysStr' :: [(String,Widget Name)] -> Widget Name
borderKeysStr' keydescs =
hBox $
intersperse sep $
[withAttr ("border" <> "key") (str keys) <+> str ":" <+> desc | (keys, desc) <- keydescs]
where
-- sep = str " | "
sep = str " "
-- temporary shenanigans:
-- | Convert the special account name "*" (from balance report with depth limit 0) to something clearer.
replaceHiddenAccountsNameWith :: AccountName -> AccountName -> AccountName
replaceHiddenAccountsNameWith anew a | a == hiddenAccountsName = anew
| a == "*" = anew
| otherwise = a
hiddenAccountsName = "..." -- for now
-- generic
--topBottomBorderWithLabel :: Widget Name -> Widget Name -> Widget Name
--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
topBottomBorderWithLabels :: Widget Name -> Widget Name -> Widget Name -> Widget Name
topBottomBorderWithLabels toplabel bottomlabel body =
Widget Greedy Greedy $ do
c <- getContext
let (_w,h) = (c^.availWidthL, c^.availHeightL)
h' = h - 2
body' = vLimit (h') body
debugmsg =
""
-- " debug: "++show (_w,h')
render $
hBorderWithLabel (withAttr "border" $ toplabel <+> str debugmsg)
<=>
body'
<=>
hBorderWithLabel (withAttr "border" bottomlabel)
---- XXX should be equivalent to the above, but isn't (page down goes offscreen)
--_topBottomBorderWithLabel2 :: Widget Name -> Widget Name -> Widget Name
--_topBottomBorderWithLabel2 label = \wrapped ->
-- let debugmsg = ""
-- in hBorderWithLabel (label <+> str debugmsg)
-- <=>
-- wrapped
-- <=>
-- hBorder
-- XXX superseded by pad, in theory
-- | 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.
-- XXX Should reduce the available size visible to inner widget, but doesn't seem to (cf rsDraw2).
margin :: Int -> Int -> Maybe Color -> Widget Name -> Widget Name
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 -> Widget Name -> Widget Name
withBorderAttr attr = updateAttrMap (applyAttrMappings [("border", attr)])
---- | Like brick's continue, but first run some action to modify brick's state.
---- This action does not affect the app state, but might eg adjust a widget's scroll position.
--continueWith :: EventM n () -> ui -> EventM n (Next ui)
--continueWith brickaction ui = brickaction >> continue ui
---- | Scroll a list's viewport so that the selected item is at the top
---- of the display area.
--scrollToTop :: List Name e -> EventM Name ()
--scrollToTop list = do
-- let vpname = list^.listNameL
-- setTop (viewportScroll vpname) 0
-- | Scroll a list's viewport so that the selected item is centered in the
-- middle of the display area.
scrollSelectionToMiddle :: List Name e -> EventM Name ()
scrollSelectionToMiddle list = do
let mselectedrow = list^.listSelectedL
vpname = list^.listNameL
mvp <- lookupViewport vpname
case (mselectedrow, mvp) of
(Just selectedrow, Just vp) -> do
let
itemheight = dbg4 "itemheight" $ list^.listItemHeightL
vpheight = dbg4 "vpheight" $ vp^.vpSize._2
itemsperpage = dbg4 "itemsperpage" $ vpheight `div` itemheight
toprow = dbg4 "toprow" $ max 0 (selectedrow - (itemsperpage `div` 2)) -- assuming ViewportScroll's row offset is measured in list items not screen rows
setTop (viewportScroll vpname) toprow
_ -> return ()
-- arrow keys vi keys emacs keys
moveUpEvents = [EvKey KUp [] , EvKey (KChar 'k') [], EvKey (KChar 'p') [MCtrl]]
moveDownEvents = [EvKey KDown [] , EvKey (KChar 'j') [], EvKey (KChar 'n') [MCtrl]]
moveLeftEvents = [EvKey KLeft [] , EvKey (KChar 'h') [], EvKey (KChar 'b') [MCtrl]]
moveRightEvents = [EvKey KRight [], EvKey (KChar 'l') [], EvKey (KChar 'f') [MCtrl]]
normaliseMovementKeys ev
| ev `elem` moveUpEvents = EvKey KUp []
| ev `elem` moveDownEvents = EvKey KDown []
| ev `elem` moveLeftEvents = EvKey KLeft []
| ev `elem` moveRightEvents = EvKey KRight []
| otherwise = ev