{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
module Text.Colour.Code where
import Data.ByteString (ByteString)
import Data.ByteString.Builder (Builder)
import qualified Data.ByteString.Builder as SBB
import qualified Data.ByteString.Internal as SBI
import qualified Data.ByteString.Lazy as LB
import Data.List
import Data.Validity
import Data.Validity.ByteString ()
import Data.Validity.Text ()
import Data.Word
import GHC.Generics (Generic)
-- https://en.wikipedia.org/wiki/Escape_character#ASCII_escape_character
asciiEscape :: Word8
asciiEscape = SBI.c2w '\ESC'
csiStart :: Word8
csiStart = SBI.c2w '['
csiDelimiter :: Word8
csiDelimiter = SBI.c2w ';'
newtype CSI
= SGR [SGR]
deriving (Show, Eq, Generic)
instance Validity CSI
-- | Render a CSI directly to bytestring.
-- You probably want to use 'renderCSI' instead.
-- This is just for testing.
renderCSIBS :: CSI -> ByteString
renderCSIBS = LB.toStrict . SBB.toLazyByteString . renderCSI
-- https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
renderCSI :: CSI -> Builder
renderCSI =
let csi ps c =
mconcat
[ SBB.word8 asciiEscape,
SBB.word8 csiStart,
csiParamsToWords ps,
SBB.word8 c
]
in \case
SGR sgrs -> csi (concatMap sgrToCSIParams sgrs) (SBI.c2w 'm')
-- https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
data SGR
= Reset
| SetItalic !Bool
| SetUnderlining !Underlining
| SetConsoleIntensity !ConsoleIntensity
| SetColour !ColourIntensity !ConsoleLayer !TerminalColour
| Set8BitColour !ConsoleLayer !Word8
| Set24BitColour
!ConsoleLayer
!Word8 -- Red
!Word8 -- Green
!Word8 -- Blue
deriving (Show, Eq, Generic)
instance Validity SGR
csiParamsToWords :: [Word8] -> Builder
csiParamsToWords = mconcat . intersperse (SBB.word8 csiDelimiter) . map csiParamToWord
csiParamToWord :: Word8 -> Builder
csiParamToWord = \case
0 -> mempty
w -> SBB.word8Dec w
sgrToCSIParams :: SGR -> [Word8]
sgrToCSIParams = \case
Reset -> [] -- [0] would be fine too
SetItalic b -> [if b then 3 else 23]
SetUnderlining u ->
[ case u of
SingleUnderline -> 4
DoubleUnderline -> 21
NoUnderline -> 24
]
SetConsoleIntensity ci ->
[ case ci of
BoldIntensity -> 1
FaintIntensity -> 2
NormalIntensity -> 22
]
SetColour i l c ->
[ case i of
Dull -> case l of
Foreground -> 30 + terminalColourSGRParameter c
Background -> 40 + terminalColourSGRParameter c
Bright -> case l of
Foreground -> 90 + terminalColourSGRParameter c
Background -> 100 + terminalColourSGRParameter c
]
Set8BitColour l w ->
[ case l of
Foreground -> 38
Background -> 48,
5,
w
]
Set24BitColour l r g b ->
[ case l of
Foreground -> 38
Background -> 48,
2,
r,
g,
b
]
-- | ANSI text underlining
data Underlining
= SingleUnderline
| DoubleUnderline
| NoUnderline
deriving (Show, Eq, Generic, Bounded, Enum)
instance Validity Underlining
-- | ANSI general console intensity: usually treated as setting the font style
-- (e.g. 'BoldIntensity' causes text to be bold)
data ConsoleIntensity
= BoldIntensity
| FaintIntensity
| NormalIntensity
deriving (Show, Eq, Generic, Bounded, Enum)
instance Validity ConsoleIntensity
-- | ANSI's standard colours come in two intensities
data ColourIntensity
= Dull
| Bright
deriving (Show, Eq, Generic, Enum, Bounded)
instance Validity ColourIntensity
-- | ANSI colours can be set on two different layers
data ConsoleLayer
= Foreground
| Background
deriving (Show, Eq, Generic, Enum, Bounded)
instance Validity ConsoleLayer
data TerminalColour
= Black
| Red
| Green
| Yellow
| Blue
| Magenta
| Cyan
| White
deriving (Show, Eq, Generic, Enum, Bounded)
instance Validity TerminalColour
terminalColourSGRParameter :: TerminalColour -> Word8
terminalColourSGRParameter = \case
Black -> 0
Red -> 1
Green -> 2
Yellow -> 3
Blue -> 4
Magenta -> 5
Cyan -> 6
White -> 7
terminalColourFromIndex :: Word8 -> Maybe TerminalColour
terminalColourFromIndex = \case
0 -> Just Black
1 -> Just Red
2 -> Just Green
3 -> Just Yellow
4 -> Just Blue
5 -> Just Magenta
6 -> Just Cyan
7 -> Just White
_ -> Nothing