escaped-1.0.0.0: src/Data/Text/Escaped.hs
{-# 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"