escaped-1.1.0.0: src/Data/Text/Escaped.hs
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 Prelude
import Data.String (IsString (..))
import Data.Text (Text, pack)
import Data.Text qualified 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 stock (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 stock (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 . 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 = \case
Plain t -> t
Many es -> mconcat $ map render es
Reset -> "\ESC[0m"
Bold -> "\ESC[1m"
Dim -> "\ESC[2m"
Underlined -> "\ESC[3m"
Blink -> "\ESC[5m"
Reverse -> "\ESC[7m"
Hidden -> "\ESC[8m"
FG c -> "\ESC[" <> fgColorCode c <> "m"
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 = \case
Plain t -> t
Many es -> mconcat $ map plain es
_ -> ""
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 = \case
Plain t -> T.length t
Many es -> sum $ map visibleLength es
_ -> 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 = \case
Default -> "39"
Custom n -> "38;5;" <> pack (show n)
Black -> "30"
Blue -> "34"
Cyan -> "36"
DarkGray -> "90"
Green -> "32"
LightBlue -> "94"
LightCyan -> "96"
LightGray -> "37"
LightGreen -> "92"
LightMagenta -> "95"
LightRed -> "91"
LightYellow -> "93"
Magenta -> "35"
Red -> "31"
White -> "97"
Yellow -> "33"
bgColorCode :: Color -> Text
bgColorCode = \case
Default -> "49"
Custom n -> "48;5;" <> pack (show n)
Black -> "40"
Blue -> "44"
Cyan -> "46"
DarkGray -> "100"
Green -> "42"
LightBlue -> "104"
LightCyan -> "106"
LightGray -> "100"
LightGreen -> "102"
LightMagenta -> "105"
LightRed -> "101"
LightYellow -> "103"
Magenta -> "45"
Red -> "41"
White -> "107"
Yellow -> "103"