mirror of
https://github.com/simonmichael/hledger.git
synced 2024-11-14 02:14:14 +03:00
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:
parent
9a86c9ee52
commit
10d85bedec
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user