mirror of
https://github.com/simonmichael/hledger.git
synced 2024-11-07 21:15:19 +03:00
simple ansi-based ui that should work on windows, enabled with -f ansi
This commit is contained in:
parent
00a89bf472
commit
e361b789a0
391
ANSICommand.hs
Normal file
391
ANSICommand.hs
Normal file
@ -0,0 +1,391 @@
|
||||
{-|
|
||||
|
||||
A simple text UI for hledger, based on the ansi-terminal library.
|
||||
Duplicates most of UICommand.hs for now.
|
||||
|
||||
-}
|
||||
|
||||
module ANSICommand
|
||||
where
|
||||
import qualified Data.Map as Map
|
||||
import Data.Map ((!))
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Graphics.Vty
|
||||
import System.Console.ANSI
|
||||
import System.IO
|
||||
import Ledger
|
||||
import Options
|
||||
import BalanceCommand
|
||||
import RegisterCommand
|
||||
import PrintCommand
|
||||
|
||||
|
||||
helpmsg = "Welcome to hledger ansi ui. (n)ext, (p)revious, (enter), (b)ack, or (q)uit"
|
||||
|
||||
-- | The application state when running the ui command.
|
||||
data AppState = AppState {
|
||||
aw :: Int -- ^ window width
|
||||
,ah :: Int -- ^ window height
|
||||
,amsg :: String -- ^ status message
|
||||
,aopts :: [Opt] -- ^ command-line opts
|
||||
,aargs :: [String] -- ^ command-line args
|
||||
,aledger :: Ledger -- ^ parsed ledger
|
||||
,abuf :: [String] -- ^ lines of the current buffered view
|
||||
,alocs :: [Loc] -- ^ user's navigation trail within the UI
|
||||
-- ^ never null, head is current location
|
||||
} deriving (Show)
|
||||
|
||||
-- | A location within the user interface.
|
||||
data Loc = Loc {
|
||||
scr :: Screen -- ^ one of the available screens
|
||||
,sy :: Int -- ^ viewport y scroll position
|
||||
,cy :: Int -- ^ cursor y position
|
||||
} deriving (Show)
|
||||
|
||||
-- | The screens available within the user interface.
|
||||
data Screen = BalanceScreen -- ^ like hledger balance, shows accounts
|
||||
| RegisterScreen -- ^ like hledger register, shows transactions
|
||||
| PrintScreen -- ^ like hledger print, shows entries
|
||||
| LedgerScreen -- ^ shows the raw ledger
|
||||
deriving (Eq,Show)
|
||||
|
||||
-- | Run the interactive text ui.
|
||||
ansi :: [Opt] -> [String] -> Ledger -> IO ()
|
||||
ansi opts args l = do
|
||||
let (w,h) = (80,25)
|
||||
let opts' = SubTotal:opts
|
||||
let a = enter BalanceScreen $
|
||||
AppState {
|
||||
aw=w
|
||||
,ah=h
|
||||
,amsg=helpmsg
|
||||
,aopts=opts'
|
||||
,aargs=args
|
||||
,aledger=l
|
||||
,abuf=[]
|
||||
,alocs=[]
|
||||
}
|
||||
hSetBuffering stdin NoBuffering
|
||||
hSetEcho stdin False
|
||||
go a
|
||||
|
||||
-- | Update the screen, wait for the next event, repeat.
|
||||
go :: AppState -> IO ()
|
||||
go a@AppState{aw=aw,ah=ah,abuf=buf,amsg=amsg,aopts=opts,aargs=args,aledger=l} = do
|
||||
when (not $ DebugNoUI `elem` opts) $ updateScreen a
|
||||
c <- getChar
|
||||
case c of
|
||||
-- EvResize x y -> go $ resize x y a
|
||||
-- EvKey (KASCII 'l') [MCtrl] -> refresh av >> go a{amsg=helpmsg}
|
||||
-- EvKey (KASCII 'b') [] -> go $ resetTrailAndEnter BalanceScreen a
|
||||
-- EvKey (KASCII 'r') [] -> go $ resetTrailAndEnter RegisterScreen a
|
||||
-- EvKey (KASCII 'p') [] -> go $ resetTrailAndEnter PrintScreen a
|
||||
-- EvKey (KASCII 'l') [] -> go $ resetTrailAndEnter LedgerScreen a
|
||||
-- EvKey KRight [] -> go $ drilldown a
|
||||
'\n' -> go $ drilldown a
|
||||
'b' -> go $ backout a
|
||||
'p' -> go $ moveUpAndPushEdge a
|
||||
'n' -> go $ moveDownAndPushEdge a
|
||||
'<' -> go $ moveToTop a
|
||||
'>' -> go $ moveToBottom a
|
||||
-- EvKey KHome [] -> go $ moveToTop a
|
||||
-- EvKey KUp [MCtrl] -> go $ moveToTop a
|
||||
-- EvKey KUp [MShift] -> go $ moveToTop a
|
||||
-- EvKey KEnd [] -> go $ moveToBottom a
|
||||
-- EvKey KDown [MCtrl] -> go $ moveToBottom a
|
||||
-- EvKey KDown [MShift] -> go $ moveToBottom a
|
||||
-- EvKey KPageUp [] -> go $ prevpage a
|
||||
-- EvKey KBS [] -> go $ prevpage a
|
||||
-- EvKey (KASCII ' ') [MShift] -> go $ prevpage a
|
||||
-- EvKey KPageDown [] -> go $ nextpage a
|
||||
-- EvKey (KASCII ' ') [] -> go $ nextpage a
|
||||
-- EvKey (KASCII 'q') [] -> shutdown av >> return ()
|
||||
-- EvKey KEsc [] -> shutdown av >> return ()
|
||||
'q' -> resetScreen 0 0
|
||||
_ -> print c >> go a
|
||||
where
|
||||
bh = length buf
|
||||
y = posY a
|
||||
|
||||
-- app state modifiers
|
||||
|
||||
-- | The number of lines currently available for the main data display area.
|
||||
pageHeight :: AppState -> Int
|
||||
pageHeight a = ah a - 1
|
||||
|
||||
setLocCursorY, setLocScrollY :: Int -> Loc -> Loc
|
||||
setLocCursorY y l = l{cy=y}
|
||||
setLocScrollY y l = l{sy=y}
|
||||
|
||||
cursorY, scrollY, posY :: AppState -> Int
|
||||
cursorY = cy . loc
|
||||
scrollY = sy . loc
|
||||
posY a = scrollY a + cursorY a
|
||||
|
||||
setCursorY, setScrollY, setPosY :: Int -> AppState -> AppState
|
||||
setCursorY y a@AppState{alocs=(l:locs)} = a{alocs=(l':locs)} where l' = setLocCursorY y l
|
||||
setScrollY y a@AppState{alocs=(l:locs)} = a{alocs=(l':locs)} where l' = setLocScrollY y l
|
||||
setPosY y a@AppState{alocs=(l:locs)} = a{alocs=(l':locs)}
|
||||
where
|
||||
l' = setLocScrollY sy $ setLocCursorY cy l
|
||||
ph = pageHeight a
|
||||
cy = y `mod` ph
|
||||
sy = y - cy
|
||||
|
||||
updateCursorY, updateScrollY, updatePosY :: (Int -> Int) -> AppState -> AppState
|
||||
updateCursorY f a = setCursorY (f $ cursorY a) a
|
||||
updateScrollY f a = setScrollY (f $ scrollY a) a
|
||||
updatePosY f a = setPosY (f $ posY a) a
|
||||
|
||||
resize :: Int -> Int -> AppState -> AppState
|
||||
resize x y a = setCursorY cy' a{aw=x,ah=y}
|
||||
where
|
||||
cy = cursorY a
|
||||
cy' = min cy (y-2)
|
||||
|
||||
moveToTop :: AppState -> AppState
|
||||
moveToTop a = setPosY 0 a
|
||||
|
||||
moveToBottom :: AppState -> AppState
|
||||
moveToBottom a = setPosY (length $ abuf a) a
|
||||
|
||||
moveUpAndPushEdge :: AppState -> AppState
|
||||
moveUpAndPushEdge a@AppState{alocs=(Loc{sy=sy,cy=cy}:_)}
|
||||
| cy > 0 = updateCursorY (subtract 1) a
|
||||
| sy > 0 = updateScrollY (subtract 1) a
|
||||
| otherwise = a
|
||||
|
||||
moveDownAndPushEdge :: AppState -> AppState
|
||||
moveDownAndPushEdge a@AppState{alocs=(Loc{sy=sy,cy=cy}:_)}
|
||||
| sy+cy >= bh = a
|
||||
| cy < ph-1 = updateCursorY (+1) a
|
||||
| otherwise = updateScrollY (+1) a
|
||||
where
|
||||
ph = pageHeight a
|
||||
bh = length $ abuf a
|
||||
|
||||
-- | Scroll down by page height or until we can just see the last line,
|
||||
-- without moving the cursor, or if we are already scrolled as far as
|
||||
-- possible then move the cursor to the last line.
|
||||
nextpage :: AppState -> AppState
|
||||
nextpage (a@AppState{abuf=b})
|
||||
| sy < bh-jump = setScrollY sy' a
|
||||
| otherwise = setCursorY (bh-sy) a
|
||||
where
|
||||
sy = scrollY a
|
||||
jump = pageHeight a - 1
|
||||
bh = length b
|
||||
sy' = min (sy+jump) (bh-jump)
|
||||
|
||||
-- | Scroll up by page height or until we can just see the first line,
|
||||
-- without moving the cursor, or if we are scrolled as far as possible
|
||||
-- then move the cursor to the first line.
|
||||
prevpage :: AppState -> AppState
|
||||
prevpage (a@AppState{abuf=b})
|
||||
| sy > 0 = setScrollY sy' a
|
||||
| otherwise = setCursorY 0 a
|
||||
where
|
||||
sy = scrollY a
|
||||
jump = pageHeight a - 1
|
||||
sy' = max (sy-jump) 0
|
||||
|
||||
-- | Push a new UI location on to the stack.
|
||||
pushLoc :: Loc -> AppState -> AppState
|
||||
pushLoc l a = a{alocs=(l:alocs a)}
|
||||
|
||||
popLoc :: AppState -> AppState
|
||||
popLoc a@AppState{alocs=locs}
|
||||
| length locs > 1 = a{alocs=drop 1 locs}
|
||||
| otherwise = a
|
||||
|
||||
clearLocs :: AppState -> AppState
|
||||
clearLocs a = a{alocs=[]}
|
||||
|
||||
exit :: AppState -> AppState
|
||||
exit = popLoc
|
||||
|
||||
loc :: AppState -> Loc
|
||||
loc = head . alocs
|
||||
|
||||
screen :: AppState -> Screen
|
||||
screen a = scr where (Loc scr _ _) = loc a
|
||||
|
||||
-- | Enter a new screen, saving the old ui location on the stack.
|
||||
enter :: Screen -> AppState -> AppState
|
||||
enter scr@BalanceScreen a = updateData $ pushLoc Loc{scr=scr,sy=0,cy=0} a
|
||||
enter scr@RegisterScreen a = updateData $ pushLoc Loc{scr=scr,sy=0,cy=0} a
|
||||
enter scr@PrintScreen a = updateData $ pushLoc Loc{scr=scr,sy=0,cy=0} a
|
||||
enter scr@LedgerScreen a = updateData $ pushLoc Loc{scr=scr,sy=0,cy=0} a
|
||||
|
||||
resetTrailAndEnter scr a = enter scr $ clearLocs a
|
||||
|
||||
-- | Regenerate the display data appropriate for the current screen.
|
||||
updateData :: AppState -> AppState
|
||||
updateData a@AppState{aopts=opts,aargs=args,aledger=l}
|
||||
| scr == BalanceScreen = a{abuf=lines $ showBalanceReport opts [] l, aargs=[]}
|
||||
| scr == RegisterScreen = a{abuf=lines $ showRegisterReport opts args l}
|
||||
| scr == PrintScreen = a{abuf=lines $ showEntries opts args l}
|
||||
| scr == LedgerScreen = a{abuf=lines $ rawledgertext l}
|
||||
where scr = screen a
|
||||
|
||||
backout :: AppState -> AppState
|
||||
backout a
|
||||
| screen a == BalanceScreen = a
|
||||
| otherwise = updateData $ popLoc a
|
||||
|
||||
drilldown :: AppState -> AppState
|
||||
drilldown a
|
||||
| screen a == BalanceScreen = enter RegisterScreen a{aargs=[currentAccountName a]}
|
||||
| screen a == RegisterScreen = scrollToEntry e $ enter PrintScreen a
|
||||
| screen a == PrintScreen = enter LedgerScreen a
|
||||
| screen a == LedgerScreen = a
|
||||
where e = currentEntry a
|
||||
|
||||
-- | Get the account name currently highlighted by the cursor on the
|
||||
-- balance screen. Results undefined while on other screens.
|
||||
currentAccountName :: AppState -> AccountName
|
||||
currentAccountName a = accountNameAt (abuf a) (posY a)
|
||||
|
||||
-- | Get the full name of the account being displayed at a specific line
|
||||
-- within the balance command's output.
|
||||
accountNameAt :: [String] -> Int -> AccountName
|
||||
accountNameAt buf lineno = accountNameFromComponents anamecomponents
|
||||
where
|
||||
namestohere = map (drop 22) $ take (lineno+1) buf
|
||||
(indented, nonindented) = span (" " `isPrefixOf`) $ reverse namestohere
|
||||
thisbranch = indented ++ take 1 nonindented
|
||||
anamecomponents = reverse $ map strip $ dropsiblings thisbranch
|
||||
|
||||
dropsiblings :: [AccountName] -> [AccountName]
|
||||
dropsiblings [] = []
|
||||
dropsiblings (x:xs) = [x] ++ dropsiblings xs'
|
||||
where
|
||||
xs' = dropWhile moreindented xs
|
||||
moreindented = (>= myindent) . indentof
|
||||
myindent = indentof x
|
||||
indentof = length . takeWhile (==' ')
|
||||
|
||||
-- | If on the print screen, move the cursor to highlight the specified entry
|
||||
-- (or a reasonable guess). Doesn't work.
|
||||
scrollToEntry :: Entry -> AppState -> AppState
|
||||
scrollToEntry e a@AppState{abuf=buf} = setCursorY cy $ setScrollY sy a
|
||||
where
|
||||
entryfirstline = head $ lines $ showEntry $ e
|
||||
halfph = pageHeight a `div` 2
|
||||
y = fromMaybe 0 $ findIndex (== entryfirstline) buf
|
||||
sy = max 0 $ y - halfph
|
||||
cy = y - sy
|
||||
|
||||
-- | Get the entry containing the transaction currently highlighted by the
|
||||
-- cursor on the register screen (or best guess). Results undefined while
|
||||
-- on other screens. Doesn't work.
|
||||
currentEntry :: AppState -> Entry
|
||||
currentEntry a@AppState{aledger=l,abuf=buf} = entryContainingTransaction a t
|
||||
where
|
||||
t = safehead nulltxn $ filter ismatch $ ledgerTransactions l
|
||||
ismatch t = date t == (parsedate $ take 10 datedesc)
|
||||
&& (take 70 $ showtxn False t nullmixedamt) == (datedesc ++ acctamt)
|
||||
datedesc = take 32 $ fromMaybe "" $ find (not . (" " `isPrefixOf`)) $ [safehead "" rest] ++ reverse above
|
||||
acctamt = drop 32 $ safehead "" rest
|
||||
safehead d ls = if null ls then d else head ls
|
||||
(above,rest) = splitAt y buf
|
||||
y = posY a
|
||||
|
||||
-- | Get the entry which contains the given transaction.
|
||||
-- Will raise an error if there are problems.
|
||||
entryContainingTransaction :: AppState -> Transaction -> Entry
|
||||
entryContainingTransaction AppState{aledger=l} t = (entries $ rawledger l) !! entryno t
|
||||
|
||||
-- renderers
|
||||
|
||||
renderScreen :: AppState -> String
|
||||
renderScreen (a@AppState{aw=w,ah=h,abuf=buf,amsg=msg}) = maintext ++ renderStatus w msg
|
||||
where
|
||||
(cx, cy) = (0, cursorY a)
|
||||
sy = scrollY a
|
||||
-- maintext = (vertcat $ map (render defaultattr) above)
|
||||
-- (render currentlineattr thisline)
|
||||
-- (vertcat $ map (render defaultattr) below)
|
||||
-- (thisline,below) | null rest = (blankline,[])
|
||||
-- | otherwise = (head rest, tail rest)
|
||||
-- (above,rest) = splitAt cy linestorender
|
||||
maintext = unlines $ map padclipline $ take (h-1) $ drop sy $ buf ++ replicate h blankline
|
||||
padclipline l = take w $ l ++ blankline
|
||||
blankline = replicate w ' '
|
||||
-- mainimg = (renderString attr $ unlines $ above)
|
||||
-- <->
|
||||
-- (renderString reverseattr $ thisline)
|
||||
-- <->
|
||||
-- (renderString attr $ unlines $ below)
|
||||
-- (above,(thisline:below))
|
||||
-- | null ls = ([],[""])
|
||||
-- | otherwise = splitAt y ls
|
||||
-- ls = lines $ fitto w (h-1) $ unlines $ drop as $ buf
|
||||
|
||||
-- padClipString :: Int -> Int -> String -> [String]
|
||||
-- padClipString h w s = rows
|
||||
-- where
|
||||
-- rows = map padclipline $ take h $ lines s ++ replicate h blankline
|
||||
-- padclipline l = take w $ l ++ blankline
|
||||
-- blankline = replicate w ' '
|
||||
|
||||
-- renderString :: Attr -> String -> Image
|
||||
-- renderString attr s = vertcat $ map (renderBS attr . B.pack) rows
|
||||
-- where
|
||||
-- rows = lines $ fitto w h s
|
||||
-- w = maximum $ map length $ ls
|
||||
-- h = length ls
|
||||
-- ls = lines s
|
||||
|
||||
renderStatus :: Int -> String -> String
|
||||
renderStatus w s = {- statusattr -} take w (s ++ repeat ' ')
|
||||
|
||||
|
||||
-- the all-important theming engine
|
||||
|
||||
theme = 1
|
||||
|
||||
(defaultattr,
|
||||
currentlineattr,
|
||||
statusattr
|
||||
) =
|
||||
case theme of
|
||||
1 -> ( -- restrained
|
||||
attr
|
||||
,setBold attr
|
||||
,setRV attr
|
||||
)
|
||||
2 -> ( -- colorful
|
||||
setRV attr
|
||||
,setFG white $ setBG red $ attr
|
||||
,setFG black $ setBG green $ attr
|
||||
)
|
||||
3 -> ( --
|
||||
setRV attr
|
||||
,setFG white $ setBG red $ attr
|
||||
,setRV attr
|
||||
)
|
||||
|
||||
halfbrightattr = setHalfBright attr
|
||||
reverseattr = setRV attr
|
||||
redattr = setFG red attr
|
||||
greenattr = setFG green attr
|
||||
reverseredattr = setRV $ setFG red attr
|
||||
reversegreenattr= setRV $ setFG green attr
|
||||
|
||||
|
||||
-- ansi output
|
||||
|
||||
updateScreen :: AppState -> IO ()
|
||||
updateScreen a = putAnsiStr (renderScreen a) 0 (cursorY a)
|
||||
|
||||
putAnsiStr :: String -> Int -> Int -> IO ()
|
||||
putAnsiStr s cx cy = do
|
||||
resetScreen 0 0
|
||||
putStr s
|
||||
setCursorPosition cy cx
|
||||
hFlush stdout
|
||||
|
||||
resetScreen :: Int -> Int -> IO ()
|
||||
resetScreen cx cy = clearScreen >> setSGR [Reset] >> setCursorPosition cy cx
|
||||
|
2
Makefile
2
Makefile
@ -2,7 +2,7 @@
|
||||
|
||||
# build the normal hledger binary
|
||||
BUILD=ghc --make hledger.hs -o hledger -O
|
||||
BUILDFLAGS=-DVTY
|
||||
BUILDFLAGS=-DVTY -DANSI
|
||||
build: setbuildversion tag
|
||||
$(BUILD) $(BUILDFLAGS)
|
||||
|
||||
|
3
README
3
README
@ -36,10 +36,11 @@ You can configure with optional flags to build additional hledger
|
||||
features. These are::
|
||||
|
||||
-f vty - Build vty-based text ui (requires vty, not available on windows)
|
||||
-f ansi - Build ansi-based text ui (requires ansi-terminal)
|
||||
|
||||
Eg::
|
||||
|
||||
cabal install -f "vty" hledger (or runhaskell Setup.hs configure -f "vty")
|
||||
cabal install -f "vty ansi" hledger (or runhaskell Setup.hs configure -f "vty ansi")
|
||||
|
||||
To get the latest development code do::
|
||||
|
||||
|
@ -21,6 +21,9 @@ Cabal-Version: >= 1.2
|
||||
Flag vty
|
||||
description: Build vty-based text ui (requires vty, not available on windows)
|
||||
default: False
|
||||
Flag ansi
|
||||
description: Build ansi-based text ui (requires ansi-terminal)
|
||||
default: False
|
||||
|
||||
Executable hledger
|
||||
Main-Is: hledger.hs
|
||||
@ -62,6 +65,10 @@ Executable hledger
|
||||
Build-Depends:vty>=3.1.8.2
|
||||
Other-Modules:UICommand
|
||||
cpp-options: -DVTY
|
||||
if flag(ansi)
|
||||
Build-Depends:ansi-terminal
|
||||
Other-Modules:ANSICommand
|
||||
cpp-options: -DANSI
|
||||
|
||||
Library
|
||||
Build-Depends: base, containers, haskell98, directory, parsec, regex-compat,
|
||||
|
@ -55,6 +55,9 @@ import RegisterCommand
|
||||
#ifdef VTY
|
||||
import qualified UICommand
|
||||
#endif
|
||||
#ifdef ANSI
|
||||
import qualified ANSICommand
|
||||
#endif
|
||||
import Tests
|
||||
|
||||
|
||||
@ -71,6 +74,9 @@ main = do
|
||||
| cmd `isPrefixOf` "register" = parseLedgerAndDo opts args register
|
||||
#ifdef VTY
|
||||
| cmd `isPrefixOf` "ui" = parseLedgerAndDo opts args UICommand.ui
|
||||
#endif
|
||||
#ifdef ANSI
|
||||
| cmd `isPrefixOf` "ansi" = parseLedgerAndDo opts args ANSICommand.ansi
|
||||
#endif
|
||||
| cmd `isPrefixOf` "test" = runtests opts args >> return ()
|
||||
| otherwise = putStr $ usage
|
||||
|
Loading…
Reference in New Issue
Block a user