{-# LANGUAGE FlexibleInstances #-} -- | This module contains functions that convert a 'T.Chunk' into -- 'ByteString's. Ordinarily everything you need from this module is -- exported from "Rainbow". module Rainbow.Translate where import Control.Exception (try) import Data.ByteString (ByteString) import Data.List (intersperse) import Data.Text (Text) import Data.Word (Word8) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BS8 import qualified Data.Text.Encoding as X import qualified Rainbow.Types as T import qualified System.Console.Terminfo as Terminfo import qualified System.IO as IO single :: Char -> [ByteString] -> [ByteString] single c = ((BS8.singleton c):) escape :: [ByteString] -> [ByteString] escape = single '\x1B' csi :: [ByteString] -> [ByteString] csi = escape . single '[' sgr :: ([ByteString] -> [ByteString]) -> [ByteString] -> [ByteString] sgr sq = csi . sq . single 'm' params :: Show a => [a] -> [ByteString] -> [ByteString] params cs = ((intersperse semi . map (BS8.pack . show) $ cs) ++) where semi = BS8.singleton ';' sgrSingle :: Word -> [ByteString] -> [ByteString] sgrSingle w = sgr $ params [w] sgrDouble :: Word -> Word -> [ByteString] -> [ByteString] sgrDouble x y = sgr $ params [x, y] normalDefault :: [ByteString] -> [ByteString] normalDefault = sgrSingle 0 bold :: [ByteString] -> [ByteString] bold = sgrSingle 1 faint :: [ByteString] -> [ByteString] faint = sgrSingle 2 italic :: [ByteString] -> [ByteString] italic = sgrSingle 3 underline :: [ByteString] -> [ByteString] underline = sgrSingle 4 blink :: [ByteString] -> [ByteString] blink = sgrSingle 5 -- Yes, blink is 5, inverse is 7; 6 is skipped. In ISO 6429 6 blinks -- at a different rate. inverse :: [ByteString] -> [ByteString] inverse = sgrSingle 7 invisible :: [ByteString] -> [ByteString] invisible = sgrSingle 8 strikeout :: [ByteString] -> [ByteString] strikeout = sgrSingle 9 foreBlack :: [ByteString] -> [ByteString] foreBlack = sgrSingle 30 foreRed :: [ByteString] -> [ByteString] foreRed = sgrSingle 31 foreGreen :: [ByteString] -> [ByteString] foreGreen = sgrSingle 32 foreYellow :: [ByteString] -> [ByteString] foreYellow = sgrSingle 33 foreBlue :: [ByteString] -> [ByteString] foreBlue = sgrSingle 34 foreMagenta :: [ByteString] -> [ByteString] foreMagenta = sgrSingle 35 foreCyan :: [ByteString] -> [ByteString] foreCyan = sgrSingle 36 foreWhite :: [ByteString] -> [ByteString] foreWhite = sgrSingle 37 -- code 3 8 is skipped foreDefault :: [ByteString] -> [ByteString] foreDefault = sgrSingle 39 backBlack :: [ByteString] -> [ByteString] backBlack = sgrSingle 40 backRed :: [ByteString] -> [ByteString] backRed = sgrSingle 41 backGreen :: [ByteString] -> [ByteString] backGreen = sgrSingle 42 backYellow :: [ByteString] -> [ByteString] backYellow = sgrSingle 43 backBlue :: [ByteString] -> [ByteString] backBlue = sgrSingle 44 backMagenta :: [ByteString] -> [ByteString] backMagenta = sgrSingle 45 backCyan :: [ByteString] -> [ByteString] backCyan = sgrSingle 46 backWhite :: [ByteString] -> [ByteString] backWhite = sgrSingle 47 -- code 4 8 is skipped backDefault :: [ByteString] -> [ByteString] backDefault = sgrSingle 49 fore256 :: Word8 -> [ByteString] -> [ByteString] fore256 c = sgr $ params [38,5,c] back256 :: Word8 -> [ByteString] -> [ByteString] back256 c = sgr $ params [48,5,c] foreColor8 :: T.Enum8 -> [ByteString] -> [ByteString] foreColor8 e8 = case e8 of T.E0 -> foreBlack T.E1 -> foreRed T.E2 -> foreGreen T.E3 -> foreYellow T.E4 -> foreBlue T.E5 -> foreMagenta T.E6 -> foreCyan T.E7 -> foreWhite backColor8 :: T.Enum8 -> [ByteString] -> [ByteString] backColor8 e8 = case e8 of T.E0 -> backBlack T.E1 -> backRed T.E2 -> backGreen T.E3 -> backYellow T.E4 -> backBlue T.E5 -> backMagenta T.E6 -> backCyan T.E7 -> backWhite renderFormat :: T.Format -> [ByteString] -> [ByteString] renderFormat (T.Format bld fnt ita und bli ivr isb stk) = effect bold bld . effect faint fnt . effect italic ita . effect underline und . effect blink bli . effect inverse ivr . effect invisible isb . effect strikeout stk where effect on x = if x then on else id renderStyle8 :: T.Style T.Enum8 -> [ByteString] -> [ByteString] renderStyle8 (T.Style fore back format) = effect foreColor8 fore . effect backColor8 back . renderFormat format where effect on (T.Color may) = maybe id on may renderStyle256 :: T.Style Word8 -> [ByteString] -> [ByteString] renderStyle256 (T.Style fore back format) = effect fore256 fore . effect back256 back . renderFormat format where effect on (T.Color may) = maybe id on may render :: Text -> [ByteString] -> [ByteString] render x = (X.encodeUtf8 x :) toByteStringsColors0 :: T.Chunk -> [ByteString] -> [ByteString] toByteStringsColors0 (T.Chunk _ yn) = render yn toByteStringsColors8 :: T.Chunk -> [ByteString] -> [ByteString] toByteStringsColors8 (T.Chunk (T.Scheme s8 _) yn) = normalDefault . renderStyle8 s8 . render yn . normalDefault toByteStringsColors256 :: T.Chunk -> [ByteString] -> [ByteString] toByteStringsColors256 (T.Chunk (T.Scheme _ s256) yn) = normalDefault . renderStyle256 s256 . render yn . normalDefault -- | Uses 'Terminfo.setupTermFromEnv' to obtain the terminal's color -- capability. If this says there are at least 256 colors are -- available, returns 'toByteStringsColors256'. Otherwise, if there -- are at least 8 colors available, returns 'toByteStringsColors8'. -- Otherwise, returns 'toByteStringsColors0'. -- -- If the terminfo database could not be read (that is, if -- 'System.Console.Terminfo.Base.SetupTermError' is returned), then return -- 'toByteStringsColors0'. byteStringMakerFromEnvironment :: IO (T.Chunk -> [ByteString] -> [ByteString]) byteStringMakerFromEnvironment = fmap g (try Terminfo.setupTermFromEnv) where g (Left e) = toByteStringsColors0 where -- Previously this caught all IOException. Now it catches only SetupTermError. -- See -- https://github.com/commercialhaskell/stackage/issues/4994 -- Hopefully this will fix this Stackage bug. _types = e :: Terminfo.SetupTermError g (Right terminal) = case Terminfo.getCapability terminal (Terminfo.tiGetNum "colors") of Nothing -> toByteStringsColors0 Just c | c >= 256 -> toByteStringsColors256 | c >= 8 -> toByteStringsColors8 | otherwise -> toByteStringsColors0 -- | Like 'byteStringMakerFromEnvironment' but also consults a -- provided 'IO.Handle'. If the 'IO.Handle' is not a terminal, -- 'toByteStringsColors0' is returned. Otherwise, the value of -- 'byteStringMakerFromEnvironment' is returned. byteStringMakerFromHandle :: IO.Handle -> IO (T.Chunk -> [ByteString] -> [ByteString]) byteStringMakerFromHandle h = IO.hIsTerminalDevice h >>= f where f isTerm | isTerm = byteStringMakerFromEnvironment | otherwise = return toByteStringsColors0 -- | Convert a list of 'T.Chunk' to a list of 'ByteString'. The -- length of the returned list may be longer than the length of the -- input list. -- -- So, for example, to print a bunch of chunks to standard output -- using 256 colors: -- -- > module PrintMyChunks where -- > -- > import qualified Data.ByteString as BS -- > import Rainbow -- > -- > myChunks :: [Chunk String] -- > myChunks = [ chunk "Roses" & fore red, chunk "\n", -- > chunk "Violets" & fore blue, chunk "\n" ] -- > -- > myPrintedChunks :: IO () -- > myPrintedChunks = mapM_ BS.putStr -- > . chunksToByteStrings toByteStringsColors256 -- > $ myChunks -- -- To use the highest number of colors that this terminal supports: -- -- > myPrintedChunks' :: IO () -- > myPrintedChunks' = do -- > printer <- byteStringMakerFromEnvironment -- > mapM_ BS.putStr -- > . chunksToByteStrings printer -- > $ myChunks chunksToByteStrings :: (T.Chunk -> [ByteString] -> [ByteString]) -- ^ Function that converts 'T.Chunk' to 'ByteString'. This -- function, when applied to a 'T.Chunk', returns a difference list. -> [T.Chunk] -> [ByteString] chunksToByteStrings mk = ($ []) . foldr (.) id . map mk -- | Writes a list of chunks to the given 'IO.Handle'. -- -- First uses 'byteStringMakerFromEnvironment' to determine how many -- colors to use. Then creates a list of 'ByteString' using -- 'chunksToByteStrings' and then writes them to the given 'IO.Handle'. hPutChunks :: IO.Handle -> [T.Chunk] -> IO () hPutChunks h cks = do maker <- byteStringMakerFromEnvironment let bsList = chunksToByteStrings maker cks mapM_ (BS.hPut h) bsList -- | Writes a list of chunks to the given 'IO.Handle', followed by a -- newline character. -- -- First uses 'byteStringMakerFromEnvironment' to determine how many -- colors to use. Then creates a list of 'ByteString' using -- 'chunksToByteStrings' and then writes them to the given 'IO.Handle'. hPutChunksLn :: IO.Handle -> [T.Chunk] -> IO () hPutChunksLn h cks = do hPutChunks h cks IO.hPutStr h "\n" -- | Writes a list of chunks to standard output. -- -- First uses 'byteStringMakerFromEnvironment' to determine how many -- colors to use. Then creates a list of 'ByteString' using -- 'chunksToByteStrings' and then writes them to standard output. putChunks :: [T.Chunk] -> IO () putChunks = hPutChunks IO.stdout -- | Writes a list of chunks to standard output, followed by a -- newline. -- -- First uses 'byteStringMakerFromEnvironment' to determine how many -- colors to use. Then creates a list of 'ByteString' using -- 'chunksToByteStrings' and then writes them to standard output. putChunksLn :: [T.Chunk] -> IO () putChunksLn cks = do putChunks cks IO.putStr "\n" -- | Writes a 'T.Chunk' to standard output. Uses -- 'byteStringMakerFromEnvironment' each time you apply it, so this -- might be inefficient. You are better off using -- 'chunksToByteStrings' and the functions in "Data.ByteString" to -- print your 'T.Chunk's if you are printing a lot of them. putChunk :: T.Chunk -> IO () putChunk ck = do mkr <- byteStringMakerFromEnvironment mapM_ BS.putStr . chunksToByteStrings mkr $ [ck] -- | Writes a 'T.Chunk' to standard output, and appends a newline. -- Uses 'byteStringMakerFromEnvironment' each time you apply it, so -- this might be inefficient. You are better off using -- 'chunksToByteStrings' and the functions in "Data.ByteString" to -- print your 'T.Chunk's if you are printing a lot of them. putChunkLn :: T.Chunk -> IO () putChunkLn ck = putChunk ck >> putStrLn ""