diff --git a/hledger-lib/Hledger/Utils.hs b/hledger-lib/Hledger/Utils.hs index 43155fca6..9f4a0f349 100644 --- a/hledger-lib/Hledger/Utils.hs +++ b/hledger-lib/Hledger/Utils.hs @@ -1,10 +1,8 @@ {-| Utilities used throughout hledger, or needed low in the module hierarchy. -This is the bottom of hledger's module graph. +These are the bottom of hledger's module graph. -} -{-# LANGUAGE LambdaCase #-} - module Hledger.Utils ( -- * Currying @@ -15,30 +13,9 @@ module Hledger.Utils ( curry4, uncurry4, - -- * Console - color, - bgColor, - colorB, - bgColorB, - - -- * IO - error', - usageError, - embedFileRelative, - expandHomePath, - expandPath, - readFileOrStdinPortably, - readFilePortably, - readHandlePortably, - -- hereFileRelative, - -- * Lists splitAtElement, - -- * Time - getCurrentLocalTime, - getCurrentZonedTime, - -- * Trees treeLeaves, @@ -65,14 +42,19 @@ module Hledger.Utils ( -- * Misc applyN, mapM', - numDigitsInt, maximum', maximumStrict, minimumStrict, + numDigitsInt, sequence', sumStrict, + makeHledgerClassyLenses, + -- * Tests + tests_Utils, + module Hledger.Utils.Test, + -- * Other module Hledger.Utils.Debug, module Hledger.Utils.Parse, @@ -81,34 +63,16 @@ module Hledger.Utils ( module Hledger.Utils.String, module Hledger.Utils.Text, - -- * Tests - tests_Utils, - module Hledger.Utils.Test, ) where -import Control.Monad (when) import Data.Char (toLower) -import Data.FileEmbed (makeRelativeToProject, embedStringFile) import Data.List.Extra (foldl', foldl1', uncons, unsnoc) import qualified Data.Set as Set -import Data.Text (Text) -import qualified Data.Text.IO as T -import qualified Data.Text.Lazy.Builder as TB -import Data.Time.Clock (getCurrentTime) -import Data.Time.LocalTime (LocalTime, ZonedTime, getCurrentTimeZone, - utcToLocalTime, utcToZonedTime) import Data.Tree (foldTree, Tree (Node, subForest)) import Language.Haskell.TH (DecsQ, Name, mkName, nameBase) -import Language.Haskell.TH.Syntax (Q, Exp) import Lens.Micro ((&), (.~)) import Lens.Micro.TH (DefName(TopName), lensClass, lensField, makeLensesWith, classyRules) -import System.Console.ANSI (Color,ColorIntensity,ConsoleLayer(..), SGR(..), setSGRCode) -import System.Directory (getHomeDirectory) -import System.FilePath (isRelative, ()) -import System.IO - (Handle, IOMode (..), hGetEncoding, hSetEncoding, hSetNewlineMode, - openFile, stdin, universalNewlineMode, utf8_bom) import Hledger.Utils.Debug import Hledger.Utils.Parse @@ -139,87 +103,6 @@ curry4 f w x y z = f (w, x, y, z) uncurry4 :: (a -> b -> c -> d -> e) -> (a, b, c, d) -> e uncurry4 f (w, x, y, z) = f w x y z --- Console - --- | Wrap a string in ANSI codes to set and reset foreground colour. -color :: ColorIntensity -> Color -> String -> String -color int col s = setSGRCode [SetColor Foreground int col] ++ s ++ setSGRCode [] - --- | Wrap a string in ANSI codes to set and reset background colour. -bgColor :: ColorIntensity -> Color -> String -> String -bgColor int col s = setSGRCode [SetColor Background int col] ++ s ++ setSGRCode [] - --- | Wrap a WideBuilder in ANSI codes to set and reset foreground colour. -colorB :: ColorIntensity -> Color -> WideBuilder -> WideBuilder -colorB int col (WideBuilder s w) = - WideBuilder (TB.fromString (setSGRCode [SetColor Foreground int col]) <> s <> TB.fromString (setSGRCode [])) w - --- | Wrap a WideBuilder in ANSI codes to set and reset background colour. -bgColorB :: ColorIntensity -> Color -> WideBuilder -> WideBuilder -bgColorB int col (WideBuilder s w) = - WideBuilder (TB.fromString (setSGRCode [SetColor Background int col]) <> s <> TB.fromString (setSGRCode [])) w - --- IO - --- | Convert a possibly relative, possibly tilde-containing file path to an absolute one, --- given the current directory. ~username is not supported. Leave "-" unchanged. --- Can raise an error. -expandPath :: FilePath -> FilePath -> IO FilePath -- general type sig for use in reader parsers -expandPath _ "-" = return "-" -expandPath curdir p = (if isRelative p then (curdir ) else id) <$> expandHomePath p --- PARTIAL: - --- | Expand user home path indicated by tilde prefix -expandHomePath :: FilePath -> IO FilePath -expandHomePath = \case - ('~':'/':p) -> ( p) <$> getHomeDirectory - ('~':'\\':p) -> ( p) <$> getHomeDirectory - ('~':_) -> ioError $ userError "~USERNAME in paths is not supported" - p -> return p - --- | Read text from a file, --- converting any \r\n line endings to \n,, --- using the system locale's text encoding, --- ignoring any utf8 BOM prefix (as seen in paypal's 2018 CSV, eg) if that encoding is utf8. -readFilePortably :: FilePath -> IO Text -readFilePortably f = openFile f ReadMode >>= readHandlePortably - --- | Like readFilePortably, but read from standard input if the path is "-". -readFileOrStdinPortably :: String -> IO Text -readFileOrStdinPortably f = openFileOrStdin f ReadMode >>= readHandlePortably - where - openFileOrStdin :: String -> IOMode -> IO Handle - openFileOrStdin "-" _ = return stdin - openFileOrStdin f' m = openFile f' m - -readHandlePortably :: Handle -> IO Text -readHandlePortably h = do - hSetNewlineMode h universalNewlineMode - menc <- hGetEncoding h - when (fmap show menc == Just "UTF-8") $ -- XXX no Eq instance, rely on Show - hSetEncoding h utf8_bom - T.hGetContents h - --- | Simpler alias for errorWithoutStackTrace -error' :: String -> a -error' = errorWithoutStackTrace . ("Error: " <>) - --- | A version of errorWithoutStackTrace that adds a usage hint. -usageError :: String -> a -usageError = error' . (++ " (use -h to see usage)") - --- | Like embedFile, but takes a path relative to the package directory. --- Similar to embedFileRelative ? -embedFileRelative :: FilePath -> Q Exp -embedFileRelative f = makeRelativeToProject f >>= embedStringFile - --- -- | Like hereFile, but takes a path relative to the package directory. --- -- Similar to embedFileRelative ? --- hereFileRelative :: FilePath -> Q Exp --- hereFileRelative f = makeRelativeToProject f >>= hereFileExp --- where --- QuasiQuoter{quoteExp=hereFileExp} = hereFile - -- Lists splitAtElement :: Eq a => a -> [a] -> [[a]] @@ -232,20 +115,6 @@ splitAtElement x l = split es = let (first,rest) = break (x==) es in first : splitAtElement x rest --- Time - -getCurrentLocalTime :: IO LocalTime -getCurrentLocalTime = do - t <- getCurrentTime - tz <- getCurrentTimeZone - return $ utcToLocalTime tz t - -getCurrentZonedTime :: IO ZonedTime -getCurrentZonedTime = do - t <- getCurrentTime - tz <- getCurrentTimeZone - return $ utcToZonedTime tz t - -- Trees -- | Get the leaves of this tree as a list. diff --git a/hledger-lib/Hledger/Utils/IO.hs b/hledger-lib/Hledger/Utils/IO.hs index c3d5820c4..49527aa3a 100644 --- a/hledger-lib/Hledger/Utils/IO.hs +++ b/hledger-lib/Hledger/Utils/IO.hs @@ -9,33 +9,78 @@ The colour scheme may be somewhat hard-coded. -} +{-# LANGUAGE LambdaCase #-} + module Hledger.Utils.IO ( - -- * Pretty showing as a string - pshow - ,pshow' - -- * Pretty printing to stdout - ,pprint - ,pprint' + + -- * Pretty showing/printing + pshow, + pshow', + pprint, + pprint', + -- * Command line arguments - ,progArgs - -- * Detecting --color/--colour/NO_COLOR - ,colorOption - ,useColorOnStdout - ,useColorOnStderr - -- * Detecting -o/--output-file - ,outputFileOption - ,hasOutputFile + progArgs, + outputFileOption, + hasOutputFile, + + -- * ANSI color + colorOption, + useColorOnStdout, + useColorOnStderr, + color, + bgColor, + colorB, + bgColorB, + + -- * Errors + error', + usageError, + + -- * Files + embedFileRelative, + expandHomePath, + expandPath, + readFileOrStdinPortably, + readFilePortably, + readHandlePortably, + -- hereFileRelative, + + -- * Time + getCurrentLocalTime, + getCurrentZonedTime, + ) where +import Control.Monad (when) +import Data.FileEmbed (makeRelativeToProject, embedStringFile) import Data.List hiding (uncons) import Data.Maybe (isJust) +import Data.Text (Text) +import qualified Data.Text.IO as T import qualified Data.Text.Lazy as TL -import System.Console.ANSI (hSupportsANSIColor) +import qualified Data.Text.Lazy.Builder as TB +import Data.Time.Clock (getCurrentTime) +import Data.Time.LocalTime + (LocalTime, ZonedTime, getCurrentTimeZone, utcToLocalTime, utcToZonedTime) +import Language.Haskell.TH.Syntax (Q, Exp) +import System.Console.ANSI + (Color,ColorIntensity,ConsoleLayer(..), SGR(..), hSupportsANSIColor, setSGRCode) +import System.Directory (getHomeDirectory) import System.Environment (getArgs, lookupEnv) -import System.IO (stdout, Handle, stderr) +import System.FilePath (isRelative, ()) +import System.IO + (Handle, IOMode (..), hGetEncoding, hSetEncoding, hSetNewlineMode, + openFile, stdin, stdout, stderr, universalNewlineMode, utf8_bom) import System.IO.Unsafe (unsafePerformIO) -import Text.Pretty.Simple -- (defaultOutputOptionsDarkBg, OutputOptions(..), pShowOpt, pPrintOpt) +import Text.Pretty.Simple + (CheckColorTty(CheckColorTty), OutputOptions(..), + defaultOutputOptionsDarkBg, defaultOutputOptionsNoColor, pShowOpt, pPrintOpt) + +import Hledger.Utils.Text (WideBuilder(WideBuilder)) + +-- Pretty showing/printing with pretty-simple -- | pretty-simple options with colour enabled if allowed. prettyopts = @@ -67,7 +112,9 @@ pprint = pPrintOpt CheckColorTty prettyopts pprint' :: Show a => a -> IO () pprint' = pPrintOpt CheckColorTty prettyopts' --- Avoid using pshow, pprint, dbg* in the code below to prevent infinite loops. +-- "Avoid using pshow, pprint, dbg* in the code below to prevent infinite loops." (?) + +-- Command line arguments -- | The command line arguments that were used at program startup. -- Uses unsafePerformIO. @@ -75,6 +122,34 @@ pprint' = pPrintOpt CheckColorTty prettyopts' progArgs :: [String] progArgs = unsafePerformIO getArgs +-- | Read the value of the -o/--output-file command line option provided at program startup, +-- if any, using unsafePerformIO. +outputFileOption :: Maybe String +outputFileOption = + -- keep synced with output-file flag definition in hledger:CliOptions. + let args = progArgs in + case dropWhile (not . ("-o" `isPrefixOf`)) args of + -- -oARG + ('-':'o':v@(_:_)):_ -> Just v + -- -o ARG + "-o":v:_ -> Just v + _ -> + case dropWhile (/="--output-file") args of + -- --output-file ARG + "--output-file":v:_ -> Just v + _ -> + case take 1 $ filter ("--output-file=" `isPrefixOf`) args of + -- --output=file=ARG + ['-':'-':'o':'u':'t':'p':'u':'t':'-':'f':'i':'l':'e':'=':v] -> Just v + _ -> Nothing + +-- | Check whether the -o/--output-file option has been used at program startup +-- with an argument other than "-", using unsafePerformIO. +hasOutputFile :: Bool +hasOutputFile = outputFileOption `notElem` [Nothing, Just "-"] + +-- ANSI colour + -- | Read the value of the --color or --colour command line option provided at program startup -- using unsafePerformIO. If this option was not provided, returns the empty string. colorOption :: String @@ -126,29 +201,98 @@ useColorOnHandle h = unsafePerformIO $ do return $ coloroption `elem` ["always","yes"] || (coloroption `notElem` ["never","no"] && not no_color && supports_color) --- | Read the value of the -o/--output-file command line option provided at program startup, --- if any, using unsafePerformIO. -outputFileOption :: Maybe String -outputFileOption = - -- keep synced with output-file flag definition in hledger:CliOptions. - let args = progArgs in - case dropWhile (not . ("-o" `isPrefixOf`)) args of - -- -oARG - ('-':'o':v@(_:_)):_ -> Just v - -- -o ARG - "-o":v:_ -> Just v - _ -> - case dropWhile (/="--output-file") args of - -- --output-file ARG - "--output-file":v:_ -> Just v - _ -> - case take 1 $ filter ("--output-file=" `isPrefixOf`) args of - -- --output=file=ARG - ['-':'-':'o':'u':'t':'p':'u':'t':'-':'f':'i':'l':'e':'=':v] -> Just v - _ -> Nothing +-- | Wrap a string in ANSI codes to set and reset foreground colour. +color :: ColorIntensity -> Color -> String -> String +color int col s = setSGRCode [SetColor Foreground int col] ++ s ++ setSGRCode [] --- | Check whether the -o/--output-file option has been used at program startup --- with an argument other than "-", using unsafePerformIO. -hasOutputFile :: Bool -hasOutputFile = outputFileOption `notElem` [Nothing, Just "-"] +-- | Wrap a string in ANSI codes to set and reset background colour. +bgColor :: ColorIntensity -> Color -> String -> String +bgColor int col s = setSGRCode [SetColor Background int col] ++ s ++ setSGRCode [] + +-- | Wrap a WideBuilder in ANSI codes to set and reset foreground colour. +colorB :: ColorIntensity -> Color -> WideBuilder -> WideBuilder +colorB int col (WideBuilder s w) = + WideBuilder (TB.fromString (setSGRCode [SetColor Foreground int col]) <> s <> TB.fromString (setSGRCode [])) w + +-- | Wrap a WideBuilder in ANSI codes to set and reset background colour. +bgColorB :: ColorIntensity -> Color -> WideBuilder -> WideBuilder +bgColorB int col (WideBuilder s w) = + WideBuilder (TB.fromString (setSGRCode [SetColor Background int col]) <> s <> TB.fromString (setSGRCode [])) w + +-- Errors + +-- | Simpler alias for errorWithoutStackTrace +error' :: String -> a +error' = errorWithoutStackTrace . ("Error: " <>) + +-- | A version of errorWithoutStackTrace that adds a usage hint. +usageError :: String -> a +usageError = error' . (++ " (use -h to see usage)") + +-- Files + +-- | Convert a possibly relative, possibly tilde-containing file path to an absolute one, +-- given the current directory. ~username is not supported. Leave "-" unchanged. +-- Can raise an error. +expandPath :: FilePath -> FilePath -> IO FilePath -- general type sig for use in reader parsers +expandPath _ "-" = return "-" +expandPath curdir p = (if isRelative p then (curdir ) else id) <$> expandHomePath p +-- PARTIAL: + +-- | Expand user home path indicated by tilde prefix +expandHomePath :: FilePath -> IO FilePath +expandHomePath = \case + ('~':'/':p) -> ( p) <$> getHomeDirectory + ('~':'\\':p) -> ( p) <$> getHomeDirectory + ('~':_) -> ioError $ userError "~USERNAME in paths is not supported" + p -> return p + +-- | Read text from a file, +-- converting any \r\n line endings to \n,, +-- using the system locale's text encoding, +-- ignoring any utf8 BOM prefix (as seen in paypal's 2018 CSV, eg) if that encoding is utf8. +readFilePortably :: FilePath -> IO Text +readFilePortably f = openFile f ReadMode >>= readHandlePortably + +-- | Like readFilePortably, but read from standard input if the path is "-". +readFileOrStdinPortably :: String -> IO Text +readFileOrStdinPortably f = openFileOrStdin f ReadMode >>= readHandlePortably + where + openFileOrStdin :: String -> IOMode -> IO Handle + openFileOrStdin "-" _ = return stdin + openFileOrStdin f' m = openFile f' m + +readHandlePortably :: Handle -> IO Text +readHandlePortably h = do + hSetNewlineMode h universalNewlineMode + menc <- hGetEncoding h + when (fmap show menc == Just "UTF-8") $ -- XXX no Eq instance, rely on Show + hSetEncoding h utf8_bom + T.hGetContents h + +-- | Like embedFile, but takes a path relative to the package directory. +-- Similar to embedFileRelative ? +embedFileRelative :: FilePath -> Q Exp +embedFileRelative f = makeRelativeToProject f >>= embedStringFile + +-- -- | Like hereFile, but takes a path relative to the package directory. +-- -- Similar to embedFileRelative ? +-- hereFileRelative :: FilePath -> Q Exp +-- hereFileRelative f = makeRelativeToProject f >>= hereFileExp +-- where +-- QuasiQuoter{quoteExp=hereFileExp} = hereFile + +-- Time + +getCurrentLocalTime :: IO LocalTime +getCurrentLocalTime = do + t <- getCurrentTime + tz <- getCurrentTimeZone + return $ utcToLocalTime tz t + +getCurrentZonedTime :: IO ZonedTime +getCurrentZonedTime = do + t <- getCurrentTime + tz <- getCurrentTimeZone + return $ utcToZonedTime tz t