{-# 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.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)
-- | 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)
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)
-- | 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)
-- | ANSI's standard colours come in two intensities
data ColourIntensity
= Dull
| Bright
deriving (Show, Eq, Generic, Enum, Bounded)
-- | ANSI colours can be set on two different layers
data ConsoleLayer
= Foreground
| Background
deriving (Show, Eq, Generic, Enum, Bounded)
data TerminalColour
= Black
| Red
| Green
| Yellow
| Blue
| Magenta
| Cyan
| White
deriving (Show, Eq, Generic, Enum, Bounded)
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