bal: show negative amounts in red

The balance command now shows negative amounts in red, when it thinks
ANSI codes are supported, ie when TERM is not "dumb" and stdout is not
being redirected or piped somewhere.
This commit is contained in:
Simon Michael 2017-04-25 18:34:09 -07:00
parent 9a86c9ee52
commit 10d85bedec
5 changed files with 79 additions and 17 deletions

View File

@ -61,6 +61,7 @@ module Hledger.Data.Amount (
-- ** rendering -- ** rendering
amountstyle, amountstyle,
showAmount, showAmount,
cshowAmount,
showAmountWithZeroCommodity, showAmountWithZeroCommodity,
showAmountDebug, showAmountDebug,
showAmountWithoutPrice, showAmountWithoutPrice,
@ -95,6 +96,8 @@ module Hledger.Data.Amount (
showMixedAmountDebug, showMixedAmountDebug,
showMixedAmountWithoutPrice, showMixedAmountWithoutPrice,
showMixedAmountOneLineWithoutPrice, showMixedAmountOneLineWithoutPrice,
cshowMixedAmountWithoutPrice,
cshowMixedAmountOneLineWithoutPrice,
showMixedAmountWithZeroCommodity, showMixedAmountWithZeroCommodity,
showMixedAmountWithPrecision, showMixedAmountWithPrecision,
setMixedAmountPrecision, setMixedAmountPrecision,
@ -239,6 +242,10 @@ showAmountDebug Amount{..} = printf "Amount {acommodity=%s, aquantity=%s, aprice
showAmountWithoutPrice :: Amount -> String showAmountWithoutPrice :: Amount -> String
showAmountWithoutPrice a = showAmount a{aprice=NoPrice} showAmountWithoutPrice a = showAmount a{aprice=NoPrice}
-- | Colour version.
cshowAmountWithoutPrice :: Amount -> String
cshowAmountWithoutPrice a = cshowAmount a{aprice=NoPrice}
-- | Get the string representation of an amount, without any price or commodity symbol. -- | Get the string representation of an amount, without any price or commodity symbol.
showAmountWithoutPriceOrCommodity :: Amount -> String showAmountWithoutPriceOrCommodity :: Amount -> String
showAmountWithoutPriceOrCommodity a = showAmount a{acommodity="", aprice=NoPrice} showAmountWithoutPriceOrCommodity a = showAmount a{acommodity="", aprice=NoPrice}
@ -260,6 +267,13 @@ showPriceDebug (TotalPrice pa) = " @@ " ++ showAmountDebug pa
showAmount :: Amount -> String showAmount :: Amount -> String
showAmount = showAmountHelper False showAmount = showAmountHelper False
-- | Colour version. For a negative amount, adds ANSI codes to change the colour,
-- currently to hard-coded red.
cshowAmount :: Amount -> String
cshowAmount a =
(if isNegativeAmount a then color Dull Red else id) $
showAmountHelper False a
showAmountHelper :: Bool -> Amount -> String showAmountHelper :: Bool -> Amount -> String
showAmountHelper _ Amount{acommodity="AUTO"} = "" showAmountHelper _ Amount{acommodity="AUTO"} = ""
showAmountHelper showzerocommodity a@(Amount{acommodity=c, aprice=p, astyle=AmountStyle{..}}) = showAmountHelper showzerocommodity a@(Amount{acommodity=c, aprice=p, astyle=AmountStyle{..}}) =
@ -559,6 +573,17 @@ showMixedAmountWithoutPrice m = concat $ intersperse "\n" $ map showfixedwidth a
width = maximum $ map (length . showAmount) as width = maximum $ map (length . showAmount) as
showfixedwidth = printf (printf "%%%ds" width) . showAmountWithoutPrice showfixedwidth = printf (printf "%%%ds" width) . showAmountWithoutPrice
-- | Colour version.
cshowMixedAmountWithoutPrice :: MixedAmount -> String
cshowMixedAmountWithoutPrice m = concat $ intersperse "\n" $ map showamt as
where
(Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m
stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice}
width = maximum $ map (length . showAmount) as
showamt a =
(if isNegativeAmount a then color Dull Red else id) $
printf (printf "%%%ds" width) $ showAmountWithoutPrice a
-- | Get the one-line string representation of a mixed amount, but without -- | Get the one-line string representation of a mixed amount, but without
-- any \@ prices. -- any \@ prices.
showMixedAmountOneLineWithoutPrice :: MixedAmount -> String showMixedAmountOneLineWithoutPrice :: MixedAmount -> String
@ -567,6 +592,13 @@ showMixedAmountOneLineWithoutPrice m = concat $ intersperse ", " $ map showAmoun
(Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m (Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m
stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice} stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice}
-- | Colour version.
cshowMixedAmountOneLineWithoutPrice :: MixedAmount -> String
cshowMixedAmountOneLineWithoutPrice m = concat $ intersperse ", " $ map cshowAmountWithoutPrice as
where
(Mixed as) = normaliseMixedAmountSquashPricesForDisplay $ stripPrices m
stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=NoPrice}
-- | Canonicalise a mixed amount's display styles using the provided commodity style map. -- | Canonicalise a mixed amount's display styles using the provided commodity style map.
canonicaliseMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount canonicaliseMixedAmount :: M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount
canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styles) as canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styles) as

View File

@ -40,6 +40,8 @@ import Data.Typeable (Typeable)
import Data.Time.Calendar import Data.Time.Calendar
import Data.Default import Data.Default
import Safe import Safe
import System.Console.ANSI (hSupportsANSI)
import System.IO (stdout)
import Test.HUnit import Test.HUnit
import Text.Megaparsec.Error import Text.Megaparsec.Error
@ -92,6 +94,7 @@ data ReportOpts = ReportOpts {
,no_total_ :: Bool ,no_total_ :: Bool
,value_ :: Bool ,value_ :: Bool
,pretty_tables_ :: Bool ,pretty_tables_ :: Bool
,color_ :: Bool
} deriving (Show, Data, Typeable) } deriving (Show, Data, Typeable)
instance Default ReportOpts where def = defreportopts instance Default ReportOpts where def = defreportopts
@ -120,11 +123,13 @@ defreportopts = ReportOpts
def def
def def
def def
def
rawOptsToReportOpts :: RawOpts -> IO ReportOpts rawOptsToReportOpts :: RawOpts -> IO ReportOpts
rawOptsToReportOpts rawopts = checkReportOpts <$> do rawOptsToReportOpts rawopts = checkReportOpts <$> do
d <- getCurrentDay
let rawopts' = checkRawOpts rawopts let rawopts' = checkRawOpts rawopts
d <- getCurrentDay
color <- hSupportsANSI stdout
return defreportopts{ return defreportopts{
period_ = periodFromRawOpts d rawopts' period_ = periodFromRawOpts d rawopts'
,interval_ = intervalFromRawOpts rawopts' ,interval_ = intervalFromRawOpts rawopts'
@ -147,6 +152,7 @@ rawOptsToReportOpts rawopts = checkReportOpts <$> do
,no_total_ = boolopt "no-total" rawopts' ,no_total_ = boolopt "no-total" rawopts'
,value_ = boolopt "value" rawopts' ,value_ = boolopt "value" rawopts'
,pretty_tables_ = boolopt "pretty-tables" rawopts' ,pretty_tables_ = boolopt "pretty-tables" rawopts'
,color_ = color
} }
-- | Do extra validation of raw option values, raising an error if there's a problem. -- | Do extra validation of raw option values, raising an error if there's a problem.

View File

@ -358,7 +358,7 @@ balanceReportAsText opts ((items, total)) = unlines $ concat lines ++ t
let let
-- abuse renderBalanceReportItem to render the total with similar format -- abuse renderBalanceReportItem to render the total with similar format
acctcolwidth = maximum' [T.length fullname | (fullname, _, _, _) <- items] acctcolwidth = maximum' [T.length fullname | (fullname, _, _, _) <- items]
totallines = map rstrip $ renderBalanceReportItem fmt (T.replicate (acctcolwidth+1) " ", 0, total) totallines = map rstrip $ renderBalanceReportItem opts fmt (T.replicate (acctcolwidth+1) " ", 0, total)
-- with a custom format, extend the line to the full report width; -- with a custom format, extend the line to the full report width;
-- otherwise show the usual 20-char line for compatibility -- otherwise show the usual 20-char line for compatibility
overlinewidth | isJust (format_ opts) = maximum' $ map length $ concat lines overlinewidth | isJust (format_ opts) = maximum' $ map length $ concat lines
@ -399,52 +399,69 @@ This implementation turned out to be a bit convoluted but implements the followi
-- The output will be one or more lines depending on the format and number of commodities. -- The output will be one or more lines depending on the format and number of commodities.
balanceReportItemAsText :: ReportOpts -> StringFormat -> BalanceReportItem -> [String] balanceReportItemAsText :: ReportOpts -> StringFormat -> BalanceReportItem -> [String]
balanceReportItemAsText opts fmt (_, accountName, depth, amt) = balanceReportItemAsText opts fmt (_, accountName, depth, amt) =
renderBalanceReportItem fmt ( renderBalanceReportItem opts fmt (
maybeAccountNameDrop opts accountName, maybeAccountNameDrop opts accountName,
depth, depth,
normaliseMixedAmountSquashPricesForDisplay amt normaliseMixedAmountSquashPricesForDisplay amt
) )
-- | Render a balance report item using the given StringFormat, generating one or more lines of text. -- | Render a balance report item using the given StringFormat, generating one or more lines of text.
renderBalanceReportItem :: StringFormat -> (AccountName, Int, MixedAmount) -> [String] renderBalanceReportItem :: ReportOpts -> StringFormat -> (AccountName, Int, MixedAmount) -> [String]
renderBalanceReportItem fmt (acctname, depth, total) = renderBalanceReportItem opts fmt (acctname, depth, total) =
lines $ lines $
case fmt of case fmt of
OneLine comps -> concatOneLine $ render1 comps OneLine comps -> concatOneLine $ render1 comps
TopAligned comps -> concatBottomPadded $ render comps TopAligned comps -> concatBottomPadded $ render comps
BottomAligned comps -> concatTopPadded $ render comps BottomAligned comps -> concatTopPadded $ render comps
where where
render1 = map (renderComponent1 (acctname, depth, total)) render1 = map (renderComponent1 opts (acctname, depth, total))
render = map (renderComponent (acctname, depth, total)) render = map (renderComponent opts (acctname, depth, total))
defaultTotalFieldWidth = 20 defaultTotalFieldWidth = 20
-- | Render one StringFormat component for a balance report item. -- | Render one StringFormat component for a balance report item.
renderComponent :: (AccountName, Int, MixedAmount) -> StringFormatComponent -> String renderComponent :: ReportOpts -> (AccountName, Int, MixedAmount) -> StringFormatComponent -> String
renderComponent _ (FormatLiteral s) = s renderComponent _ _ (FormatLiteral s) = s
renderComponent (acctname, depth, total) (FormatField ljust min max field) = case field of renderComponent opts (acctname, depth, total) (FormatField ljust min max field) = case field of
DepthSpacerField -> formatString ljust Nothing max $ replicate d ' ' DepthSpacerField -> formatString ljust Nothing max $ replicate d ' '
where d = case min of where d = case min of
Just m -> depth * m Just m -> depth * m
Nothing -> depth Nothing -> depth
AccountField -> formatString ljust min max (T.unpack acctname) AccountField -> formatString ljust min max (T.unpack acctname)
TotalField -> fitStringMulti min max True False $ showMixedAmountWithoutPrice total TotalField ->
-- TODO: does not color multicommodity amounts
-- setamtcolor $ fitStringMulti min max True False $ showMixedAmountWithoutPrice total
fitStringMulti min max True False $ showamt total
_ -> "" _ -> ""
where
showamt | color_ opts = cshowMixedAmountWithoutPrice
| otherwise = showMixedAmountWithoutPrice
-- setamtcolor
-- | color_ opts && isNegativeMixedAmount total == Just True = color Dull Red
-- | otherwise = id
-- | Render one StringFormat component for a balance report item. -- | Render one StringFormat component for a balance report item.
-- This variant is for use with OneLine string formats; it squashes -- This variant is for use with OneLine string formats; it squashes
-- any multi-line rendered values onto one line, comma-and-space separated, -- any multi-line rendered values onto one line, comma-and-space separated,
-- while still complying with the width spec. -- while still complying with the width spec.
renderComponent1 :: (AccountName, Int, MixedAmount) -> StringFormatComponent -> String renderComponent1 :: ReportOpts -> (AccountName, Int, MixedAmount) -> StringFormatComponent -> String
renderComponent1 _ (FormatLiteral s) = s renderComponent1 _ _ (FormatLiteral s) = s
renderComponent1 (acctname, depth, total) (FormatField ljust min max field) = case field of renderComponent1 opts (acctname, depth, total) (FormatField ljust min max field) = case field of
AccountField -> formatString ljust min max ((intercalate ", " . lines) (indented (T.unpack acctname))) AccountField -> formatString ljust min max ((intercalate ", " . lines) (indented (T.unpack acctname)))
where where
-- better to indent the account name here rather than use a DepthField component -- better to indent the account name here rather than use a DepthField component
-- so that it complies with width spec. Uses a fixed indent step size. -- so that it complies with width spec. Uses a fixed indent step size.
indented = ((replicate (depth*2) ' ')++) indented = ((replicate (depth*2) ' ')++)
TotalField -> fitStringMulti min max True False $ ((intercalate ", " . map strip . lines) (showMixedAmountWithoutPrice total)) TotalField ->
-- setamtcolor $ fitStringMulti min max True False $ ((intercalate ", " . map strip . lines) (showMixedAmountWithoutPrice total))
fitStringMulti min max True False $ ((intercalate ", " . map strip . lines) (showamt total))
_ -> "" _ -> ""
where
showamt | color_ opts = cshowMixedAmountWithoutPrice
| otherwise = showMixedAmountWithoutPrice
-- setamtcolor
-- | color_ opts && isNegativeMixedAmount total == Just True = color Dull Red
-- | otherwise = id
-- multi-column balance reports -- multi-column balance reports
@ -489,8 +506,8 @@ multiBalanceReportAsText opts r =
-- made using 'balanceReportAsTable'), render it in a format suitable for -- made using 'balanceReportAsTable'), render it in a format suitable for
-- console output. -- console output.
renderBalanceReportTable :: ReportOpts -> Table String String MixedAmount -> String renderBalanceReportTable :: ReportOpts -> Table String String MixedAmount -> String
renderBalanceReportTable (ReportOpts { pretty_tables_ = pretty }) = unlines . trimborder . lines renderBalanceReportTable (ReportOpts { pretty_tables_ = pretty, color_=usecolor }) = unlines . trimborder . lines
. render pretty id (" " ++) showMixedAmountOneLineWithoutPrice . render pretty id (" " ++) showamt
. align . align
where where
trimborder = ("":) . (++[""]) . drop 1 . init . map (drop 1 . init) trimborder = ("":) . (++[""]) . drop 1 . init . map (drop 1 . init)
@ -498,6 +515,8 @@ renderBalanceReportTable (ReportOpts { pretty_tables_ = pretty }) = unlines . tr
where where
acctswidth = maximum' $ map strWidth (headerContents l) acctswidth = maximum' $ map strWidth (headerContents l)
l' = padRightWide acctswidth <$> l l' = padRightWide acctswidth <$> l
showamt | usecolor = cshowMixedAmountOneLineWithoutPrice
| otherwise = showMixedAmountOneLineWithoutPrice
-- | Build a 'Table' from a multi-column balance report. -- | Build a 'Table' from a multi-column balance report.
balanceReportAsTable :: ReportOpts -> MultiBalanceReport -> Table String String MixedAmount balanceReportAsTable :: ReportOpts -> MultiBalanceReport -> Table String String MixedAmount

View File

@ -84,6 +84,7 @@ library
build-depends: build-depends:
base >=4.8 && <5 base >=4.8 && <5
, base-compat >=0.8.1 , base-compat >=0.8.1
, ansi-terminal >= 0.6.2.3 && < 0.7
, directory , directory
, file-embed >=0.0.10 && <0.1 , file-embed >=0.0.10 && <0.1
, filepath , filepath
@ -169,6 +170,7 @@ executable hledger
build-depends: build-depends:
base >=4.8 && <5 base >=4.8 && <5
, base-compat >=0.8.1 , base-compat >=0.8.1
, ansi-terminal >= 0.6.2.3 && < 0.7
, directory , directory
, file-embed >=0.0.10 && <0.1 , file-embed >=0.0.10 && <0.1
, filepath , filepath
@ -231,6 +233,7 @@ test-suite test
build-depends: build-depends:
base >=4.8 && <5 base >=4.8 && <5
, base-compat >=0.8.1 , base-compat >=0.8.1
, ansi-terminal >= 0.6.2.3 && < 0.7
, directory , directory
, file-embed >=0.0.10 && <0.1 , file-embed >=0.0.10 && <0.1
, filepath , filepath
@ -292,6 +295,7 @@ benchmark bench
build-depends: build-depends:
base >=4.8 && <5 base >=4.8 && <5
, base-compat >=0.8.1 , base-compat >=0.8.1
, ansi-terminal >= 0.6.2.3 && < 0.7
, directory , directory
, file-embed >=0.0.10 && <0.1 , file-embed >=0.0.10 && <0.1
, filepath , filepath

View File

@ -65,6 +65,7 @@ flags:
dependencies: dependencies:
- base >=4.8 && <5 - base >=4.8 && <5
- base-compat >=0.8.1 - base-compat >=0.8.1
- ansi-terminal >= 0.6.2.3 && < 0.7
- directory - directory
- file-embed >=0.0.10 && <0.1 - file-embed >=0.0.10 && <0.1
- filepath - filepath