dev: Hledger.Utils.IO colour helpers: cleanup

This commit is contained in:
Simon Michael 2024-11-02 11:53:59 -10:00
parent 75ff6c8218
commit 9c81bb2a06

View File

@ -40,20 +40,13 @@ module Hledger.Utils.IO (
parseYNA, parseYNA,
YNA(..), YNA(..),
-- * ANSI color -- * ANSI color/styles
-- ** hledger-specific
colorOption,
useColorOnStdout, useColorOnStdout,
useColorOnStderr, useColorOnStderr,
colorOption,
useColorOnStdoutUnsafe, useColorOnStdoutUnsafe,
useColorOnStderrUnsafe, useColorOnStderrUnsafe,
-- XXX needed for using color/bgColor/colorB/bgColorB, but clashing with UIUtils:
-- Color(..),
-- ColorIntensity(..),
color,
bgColor,
colorB,
bgColorB,
--
bold', bold',
faint', faint',
black', black',
@ -73,6 +66,16 @@ module Hledger.Utils.IO (
brightCyan', brightCyan',
brightWhite', brightWhite',
rgb', rgb',
-- ** Generic
-- XXX Types used with color/bgColor/colorB/bgColorB,
-- not re-exported because clashing with UIUtils:
-- Color(..),
-- ColorIntensity(..),
color,
bgColor,
colorB,
bgColorB,
terminalIsLight, terminalIsLight,
terminalLightness, terminalLightness,
terminalFgColor, terminalFgColor,
@ -358,8 +361,6 @@ parseYNA s
| otherwise = error' $ "value should be one of " <> (intercalate ", " ["y","yes","n","no","a","auto"]) | otherwise = error' $ "value should be one of " <> (intercalate ", " ["y","yes","n","no","a","auto"])
where l = map toLower s where l = map toLower s
-- Command line arguments
-- | The command line arguments that were used at program startup. -- | The command line arguments that were used at program startup.
-- Uses unsafePerformIO. -- Uses unsafePerformIO.
{-# NOINLINE progArgs #-} {-# NOINLINE progArgs #-}
@ -385,16 +386,55 @@ hasOutputFile = do
Just "-" -> False Just "-" -> False
_ -> True _ -> True
-- ANSI colour
-- ANSI colour/style helpers. Some of these use unsafePerformIO to read info.
-- hledger-specific:
-- | Get the value of the rightmost --color or --colour option from the program's command line arguments.
colorOption :: IO YNA
colorOption = maybe Auto parseYNA <$> getOpt ["color","colour"]
-- | Should ANSI color and styles be used with this output handle ?
-- Considers colorOption, the NO_COLOR environment variable, and hSupportsANSIColor.
useColorOnHandle :: Handle -> IO Bool
useColorOnHandle h = do
no_color <- isJust <$> lookupEnv "NO_COLOR"
supports_color <- hSupportsANSIColor h
yna <- colorOption
return $ yna==Yes || (yna==Auto && not no_color && supports_color)
-- | Should ANSI color and styles be used for standard output ?
-- Considers useColorOnHandle stdout and whether there's an --output-file option.
useColorOnStdout :: IO Bool
useColorOnStdout = do
nooutputfile <- not <$> hasOutputFile
usecolor <- useColorOnHandle stdout
return $ nooutputfile && usecolor
-- | Should ANSI color and styles be used for standard error output ?
-- Considers useColorOnHandle stderr; is not affected by an --output-file option.
useColorOnStderr :: IO Bool
useColorOnStderr = useColorOnHandle stderr
-- | Like useColorOnStdout, but using unsafePerformIO. Useful eg in low-level debug code.
-- Sticky in GHCI, may not be affected by --color in a config file, etc.
useColorOnStdoutUnsafe :: Bool
useColorOnStdoutUnsafe = unsafePerformIO useColorOnStdout
-- | Like useColorOnStdoutUnsafe, but for stderr.
useColorOnStderrUnsafe :: Bool
useColorOnStderrUnsafe = unsafePerformIO useColorOnStderr
-- Detect whether ANSI should be used on stdout using useColorOnStdoutUnsafe, -- Detect whether ANSI should be used on stdout using useColorOnStdoutUnsafe,
-- and if so prepend and append the given SGR codes to a string. -- and if so prepend and append the given SGR codes to a string.
-- Currently used in a few places (eg: the commands list, the demo command, the recentassertions error message.) -- Currently used in a few places (the commands list, the demo command, the recentassertions error message).
-- This tends to get stuck on or off in GHCI, -- This tends to get stuck on or off in GHCI until reloaded,
-- respects the command line --color if compiled, -- respects --color on the command line if the program is compiled,
-- and ignores the config file. -- and ignores --color in the config file.
ansiWrap :: SGRString -> SGRString -> String -> String ansiWrapUnsafe :: SGRString -> SGRString -> String -> String
ansiWrap pre post s = if useColorOnStdoutUnsafe then pre<>s<>post else s ansiWrapUnsafe pre post s = if useColorOnStdoutUnsafe then pre<>s<>post else s
type SGRString = String type SGRString = String
@ -422,106 +462,63 @@ sgrrgb r g b = setSGRCode [SetRGBColor Foreground $ sRGB r g b]
-- | Set various ANSI styles/colours in a string, only if useColorOnStdoutUnsafe says we should. -- | Set various ANSI styles/colours in a string, only if useColorOnStdoutUnsafe says we should.
bold' :: String -> String bold' :: String -> String
bold' = ansiWrap sgrbold sgrnormal bold' = ansiWrapUnsafe sgrbold sgrnormal
faint' :: String -> String faint' :: String -> String
faint' = ansiWrap sgrfaint sgrnormal faint' = ansiWrapUnsafe sgrfaint sgrnormal
black' :: String -> String black' :: String -> String
black' = ansiWrap sgrblack sgrresetfg black' = ansiWrapUnsafe sgrblack sgrresetfg
red' :: String -> String red' :: String -> String
red' = ansiWrap sgrred sgrresetfg red' = ansiWrapUnsafe sgrred sgrresetfg
green' :: String -> String green' :: String -> String
green' = ansiWrap sgrgreen sgrresetfg green' = ansiWrapUnsafe sgrgreen sgrresetfg
yellow' :: String -> String yellow' :: String -> String
yellow' = ansiWrap sgryellow sgrresetfg yellow' = ansiWrapUnsafe sgryellow sgrresetfg
blue' :: String -> String blue' :: String -> String
blue' = ansiWrap sgrblue sgrresetfg blue' = ansiWrapUnsafe sgrblue sgrresetfg
magenta' :: String -> String magenta' :: String -> String
magenta' = ansiWrap sgrmagenta sgrresetfg magenta' = ansiWrapUnsafe sgrmagenta sgrresetfg
cyan' :: String -> String cyan' :: String -> String
cyan' = ansiWrap sgrcyan sgrresetfg cyan' = ansiWrapUnsafe sgrcyan sgrresetfg
white' :: String -> String white' :: String -> String
white' = ansiWrap sgrwhite sgrresetfg white' = ansiWrapUnsafe sgrwhite sgrresetfg
brightBlack' :: String -> String brightBlack' :: String -> String
brightBlack' = ansiWrap sgrbrightblack sgrresetfg brightBlack' = ansiWrapUnsafe sgrbrightblack sgrresetfg
brightRed' :: String -> String brightRed' :: String -> String
brightRed' = ansiWrap sgrbrightred sgrresetfg brightRed' = ansiWrapUnsafe sgrbrightred sgrresetfg
brightGreen' :: String -> String brightGreen' :: String -> String
brightGreen' = ansiWrap sgrbrightgreen sgrresetfg brightGreen' = ansiWrapUnsafe sgrbrightgreen sgrresetfg
brightYellow' :: String -> String brightYellow' :: String -> String
brightYellow' = ansiWrap sgrbrightyellow sgrresetfg brightYellow' = ansiWrapUnsafe sgrbrightyellow sgrresetfg
brightBlue' :: String -> String brightBlue' :: String -> String
brightBlue' = ansiWrap sgrbrightblue sgrresetfg brightBlue' = ansiWrapUnsafe sgrbrightblue sgrresetfg
brightMagenta' :: String -> String brightMagenta' :: String -> String
brightMagenta' = ansiWrap sgrbrightmagenta sgrresetfg brightMagenta' = ansiWrapUnsafe sgrbrightmagenta sgrresetfg
brightCyan' :: String -> String brightCyan' :: String -> String
brightCyan' = ansiWrap sgrbrightcyan sgrresetfg brightCyan' = ansiWrapUnsafe sgrbrightcyan sgrresetfg
brightWhite' :: String -> String brightWhite' :: String -> String
brightWhite' = ansiWrap sgrbrightwhite sgrresetfg brightWhite' = ansiWrapUnsafe sgrbrightwhite sgrresetfg
rgb' :: Float -> Float -> Float -> String -> String rgb' :: Float -> Float -> Float -> String -> String
rgb' r g b = ansiWrap (sgrrgb r g b) sgrresetfg rgb' r g b = ansiWrapUnsafe (sgrrgb r g b) sgrresetfg
-- | Should ANSI color & styling be used for standard output ? -- Generic:
-- Considers useColorOnHandle stdout and whether there's an --output-file.
useColorOnStdout :: IO Bool
useColorOnStdout = do
nooutputfile <- not <$> hasOutputFile
usecolor <- useColorOnHandle stdout
return $ nooutputfile && usecolor
-- traceWith (("USE COLOR ON STDOUT: "<>).show) <$>
useColorOnStderr :: IO Bool
useColorOnStderr = useColorOnHandle stderr
-- | Should ANSI color & styling be used with this output handle ?
-- Considers hSupportsANSIColor stdout, whether NO_COLOR is defined,
-- and the rightmost --color option.
useColorOnHandle :: Handle -> IO Bool
useColorOnHandle h = do
no_color <- isJust <$> lookupEnv "NO_COLOR"
supports_color <- hSupportsANSIColor h
yna <- colorOption
return $ yna==Yes || (yna==Auto && not no_color && supports_color)
colorOption :: IO YNA
colorOption = maybe Auto parseYNA <$> getOpt ["color","colour"]
-- | Check the IO environment to see if ANSI colour codes should be used on stdout.
-- This is done using unsafePerformIO so it can be used anywhere, eg in
-- low-level debug utilities, which should be ok since we are just reading.
-- The logic is: use color if
-- the program was started with --color=yes|always
-- or (
-- the program was not started with --color=no|never
-- and a NO_COLOR environment variable is not defined
-- and stdout supports ANSI color
-- and -o/--output-file was not used, or its value is "-"
-- ).
useColorOnStdoutUnsafe :: Bool
useColorOnStdoutUnsafe = unsafePerformIO useColorOnStdout
-- | Like useColorOnStdoutUnsafe, but checks for ANSI color support on stderr,
-- and is not affected by -o/--output-file.
useColorOnStderrUnsafe :: Bool
useColorOnStderrUnsafe = unsafePerformIO useColorOnStderr
-- | Wrap a string in ANSI codes to set and reset foreground colour. -- | Wrap a string in ANSI codes to set and reset foreground colour.
-- ColorIntensity is @Dull@ or @Vivid@ (ie normal and bold). -- ColorIntensity is @Dull@ or @Vivid@ (ie normal and bold).
@ -568,7 +565,7 @@ terminalFgColor = terminalColor Foreground
terminalColor :: ConsoleLayer -> Maybe (RGB Float) terminalColor :: ConsoleLayer -> Maybe (RGB Float)
terminalColor = unsafePerformIO . getLayerColor' terminalColor = unsafePerformIO . getLayerColor'
-- A version of getLayerColor that is less likely to leak escape sequences to output, -- A version of ansi-terminal's getLayerColor that is less likely to leak escape sequences to output,
-- and that returns a RGB of Floats (0..1) that is more compatible with the colour package. -- and that returns a RGB of Floats (0..1) that is more compatible with the colour package.
-- This does nothing in a non-interactive context (eg when piping stdout to another command), -- This does nothing in a non-interactive context (eg when piping stdout to another command),
-- inside emacs (emacs shell buffers show the escape sequence for some reason), -- inside emacs (emacs shell buffers show the escape sequence for some reason),
@ -584,6 +581,8 @@ getLayerColor' l = do
fractionalRGB :: (Fractional a) => RGB Word16 -> RGB a fractionalRGB :: (Fractional a) => RGB Word16 -> RGB a
fractionalRGB (RGB r g b) = RGB (fromIntegral r / 65535) (fromIntegral g / 65535) (fromIntegral b / 65535) -- chatgpt fractionalRGB (RGB r g b) = RGB (fromIntegral r / 65535) (fromIntegral g / 65535) (fromIntegral b / 65535) -- chatgpt
-- Errors -- Errors
-- | Simpler alias for errorWithoutStackTrace -- | Simpler alias for errorWithoutStackTrace
@ -594,6 +593,8 @@ error' = errorWithoutStackTrace . ("Error: " <>)
usageError :: String -> a usageError :: String -> a
usageError = error' . (++ " (use -h to see usage)") usageError = error' . (++ " (use -h to see usage)")
-- Files -- Files
-- | Expand a tilde (representing home directory) at the start of a file path. -- | Expand a tilde (representing home directory) at the start of a file path.