{-# LANGUAGE OverloadedStrings #-} module Data.Text.Escaped ( Escaped ( Plain , Reset , Bold , Dim , Underlined , Blink , Reverse , Hidden , FG , BG ) , Color(..) , black , blue , cyan , darkGray , green , lightBlue , lightCyan , lightGray , lightGreen , lightMagenta , lightRed , lightYellow , magenta , red , white , yellow , fg , bg , esc , render , plain , visibleLength , terminalRenderer ) where import Data.Semigroup (Semigroup(..)) import Data.String (IsString(..)) import Data.Text (Text) import qualified Data.Text as T import System.Posix.IO (stdOutput) import System.Posix.Terminal (queryTerminal) import Test.QuickCheck import Test.QuickCheck.Instances.Text () -- | Supported colors data Color = Default | Custom Int | Black | Blue | Cyan | DarkGray | Green | LightBlue | LightCyan | LightGray | LightGreen | LightMagenta | LightRed | LightYellow | Magenta | Red | White | Yellow deriving (Eq, Show) instance Arbitrary Color where -- Too bad @Custom@ makes arbitraryBoundedEnum not useful arbitrary = oneof [ pure Default , Custom <$> arbitrary , pure Black , pure Blue , pure Cyan , pure DarkGray , pure Green , pure LightBlue , pure LightCyan , pure LightGray , pure LightGreen , pure LightMagenta , pure LightRed , pure LightYellow , pure Magenta , pure Red , pure White , pure Yellow ] -- | Bits of escaped text data Escaped = Plain Text | Reset | Bold | Dim | Underlined | Blink | Reverse | Hidden | FG Color | BG Color | Many [Escaped] deriving (Eq, Show) instance Arbitrary Escaped where -- Avoid invalid Many-of-Many nesting, which triggers an infinite loop. Such -- a value is impossible to construct because we don't export the Many -- constructor. arbitrary = oneof $ chunks <> [Many <$> listOf1 (oneof chunks)] where chunks = [ Plain <$> arbitrary , pure Reset , pure Bold , pure Dim , pure Underlined , pure Blink , pure Reverse , pure Hidden , FG <$> arbitrary , BG <$> arbitrary ] instance IsString Escaped where fromString = Plain . T.pack instance Semigroup Escaped where -- A bit explicit, but ensures identity works x <> (Plain t) | T.null t = x (Plain t) <> y | T.null t = y (Many a) <> (Many b) = Many $ a <> b (Many a) <> b = Many $ a <> [b] a <> (Many b) = Many $ a:b a <> b = Many [a, b] instance Monoid Escaped where mempty = Plain "" mappend = (<>) -- | Render an @'Escaped'@ to actually-escaped @'Text'@ -- -- Examples: -- -- >>> render "Some text via OverloadedStrings." -- "Some text via OverloadedStrings." -- -- >>> render $ Plain "Some text." -- "Some text." -- -- >>> render $ "Some " <> FG Red <> "red" <> Reset <> " text." -- "Some \ESC[31mred\ESC[0m text." -- -- >>> render $ "Some " <> blue "blue" <> " text." -- "Some \ESC[34mblue\ESC[0m text." -- -- >>> render $ "Some " <> fg (Custom 212) "color 212" <> " text." -- "Some \ESC[38;5;212mcolor 212\ESC[0m text." -- render :: Escaped -> Text render (Plain t) = t render (Many es) = T.concat $ map render es render Reset = "\ESC[0m" render Bold = "\ESC[1m" render Dim = "\ESC[2m" render Underlined = "\ESC[3m" render Blink = "\ESC[5m" render Reverse = "\ESC[7m" render Hidden = "\ESC[8m" render (FG c) = "\ESC[" <> fgColorCode c <> "m" render (BG c) = "\ESC[" <> bgColorCode c <> "m" -- | Render only the @'Text'@ parts -- -- Examples -- -- >>> plain $ Plain "Some text." -- "Some text." -- -- >>> plain $ "Some " <> FG Red <> "red" <> Reset <> " text." -- "Some red text." -- plain :: Escaped -> Text plain (Plain t) = t plain (Many es) = T.concat $ map plain es plain _ = "" black :: Escaped -> Escaped black = fg Black blue :: Escaped -> Escaped blue = fg Blue cyan :: Escaped -> Escaped cyan = fg Cyan darkGray :: Escaped -> Escaped darkGray = fg DarkGray green :: Escaped -> Escaped green = fg Green lightBlue :: Escaped -> Escaped lightBlue = fg LightBlue lightCyan :: Escaped -> Escaped lightCyan = fg LightCyan lightGray :: Escaped -> Escaped lightGray = fg LightGray lightGreen :: Escaped -> Escaped lightGreen = fg LightGreen lightMagenta :: Escaped -> Escaped lightMagenta = fg LightMagenta lightRed :: Escaped -> Escaped lightRed = fg LightRed lightYellow :: Escaped -> Escaped lightYellow = fg LightYellow magenta :: Escaped -> Escaped magenta = fg Magenta red :: Escaped -> Escaped red = fg Red white :: Escaped -> Escaped white = fg White yellow :: Escaped -> Escaped yellow = fg Yellow -- | Escape with foreground @'Color'@, then @'Reset'@ -- -- >>> fg Red "red" -- Many [FG Red,Plain "red",Reset] -- fg :: Color -> Escaped -> Escaped fg = esc . FG -- | Escape with background @'Color'@, then @'Reset'@ -- -- >>> bg Red "red" -- Many [BG Red,Plain "red",Reset] -- bg :: Color -> Escaped -> Escaped bg = esc . BG -- | Apply the given escape, then @'Reset'@ -- -- >>> esc (FG Red) "red" -- Many [FG Red,Plain "red",Reset] -- esc :: Escaped -> Escaped -> Escaped esc a b = a <> b <> Reset -- | Calculate the /visible/ length of an @'Escaped'@ visibleLength :: Escaped -> Int visibleLength (Plain t) = T.length t visibleLength (Many es) = sum $ map visibleLength es visibleLength _ = 0 -- | An @'IO'@ action to produce the appropriate renderer for a terminal -- -- Returns @'render'@ if @stdout@ is a terminal, otherwise @'plain'@ -- -- >>> r <- terminalRenderer -- >>> print $ r $ red "red text" -- "red text" -- terminalRenderer :: IO (Escaped -> Text) terminalRenderer = do istty <- queryTerminal stdOutput return $ if istty then render else plain fgColorCode :: Color -> Text fgColorCode Default = "39" fgColorCode (Custom n) = "38;5;" <> T.pack (show n) fgColorCode Black = "30" fgColorCode Blue = "34" fgColorCode Cyan = "36" fgColorCode DarkGray = "90" fgColorCode Green = "32" fgColorCode LightBlue = "94" fgColorCode LightCyan = "96" fgColorCode LightGray = "37" fgColorCode LightGreen = "92" fgColorCode LightMagenta = "95" fgColorCode LightRed = "91" fgColorCode LightYellow = "93" fgColorCode Magenta = "35" fgColorCode Red = "31" fgColorCode White = "97" fgColorCode Yellow = "33" bgColorCode :: Color -> Text bgColorCode Default = "49" bgColorCode (Custom n) = "48;5;" <> T.pack (show n) bgColorCode Black = "40" bgColorCode Blue = "44" bgColorCode Cyan = "46" bgColorCode DarkGray = "100" bgColorCode Green = "42" bgColorCode LightBlue = "104" bgColorCode LightCyan = "106" bgColorCode LightGray = "100" bgColorCode LightGreen = "102" bgColorCode LightMagenta = "105" bgColorCode LightRed = "101" bgColorCode LightYellow = "103" bgColorCode Magenta = "45" bgColorCode Red = "41" bgColorCode White = "107" bgColorCode Yellow = "103"