{-# LANGUAGE NoImplicitPrelude #-} module RIO.PrettyPrint.StylesUpdate ( StylesUpdate (..) , parseStylesUpdateFromString , HasStylesUpdate (..) ) where import Data.Aeson ( FromJSON (..), withText ) import Data.Array.IArray ( assocs ) import Data.Colour.SRGB ( Colour, sRGB24 ) import Data.Text as T ( pack, unpack ) import RIO import RIO.PrettyPrint.DefaultStyles ( defaultStyles ) import RIO.PrettyPrint.Types ( Style, StyleSpec ) import System.Console.ANSI.Types ( BlinkSpeed (..), Color (..), ColorIntensity (..) , ConsoleIntensity (..), ConsoleLayer (..), SGR (..) , Underlining (..) ) -- | Updates to 'Styles' newtype StylesUpdate = StylesUpdate { stylesUpdate :: [(Style, StyleSpec)] } deriving (Eq, Show) -- | The first styles update overrides the second one. instance Semigroup StylesUpdate where -- See module "Data.IArray.Array" of package @array@: this depends on GHC's -- implementation of '(//)' being such that the last value specified for a -- duplicated index is used. StylesUpdate s1 <> StylesUpdate s2 = StylesUpdate (s2 <> s1) instance Monoid StylesUpdate where mempty = StylesUpdate [] mappend = (<>) -- This needs to be specified as, before package -- @base-4.11.0.0@ (GHC 8.4.2, March 2018), the default is -- 'mappend = (++)'. instance FromJSON StylesUpdate where parseJSON = withText "StylesUpdate" $ return . parseStylesUpdateFromString . T.unpack -- |Parse a string that is a colon-delimited sequence of key=value, where 'key' -- is a style name and 'value' is a semicolon-delimited list of 'ANSI' SGR -- (Select Graphic Rendition) control codes (in decimal). Keys that are not -- present in 'defaultStyles' are ignored. Items in the semicolon-delimited -- list that are not recognised as valid control codes are ignored. parseStylesUpdateFromString :: String -> StylesUpdate parseStylesUpdateFromString s = StylesUpdate $ mapMaybe process table where table = do w <- split ':' s let (k, v') = break (== '=') w case v' of '=' : v -> return (T.pack k, parseCodes v) _ -> [] process :: StyleSpec -> Maybe (Style, StyleSpec) process (k, sgrs) = do style <- lookup k styles return (style, (k, sgrs)) styles :: [(Text, Style)] styles = map (\(s, (k, _)) -> (k, s)) $ assocs defaultStyles parseCodes :: String -> [SGR] parseCodes [] = [] parseCodes s = parseCodes' c where s' = split ';' s c :: [Word8] c = mapMaybe readMaybe s' parseCodes' :: [Word8] -> [SGR] parseCodes' c = case codeToSGR c of (Nothing, []) -> [] (Just sgr, []) -> [sgr] (Nothing, cs) -> parseCodes' cs (Just sgr, cs) -> sgr : parseCodes' cs split :: Char -> String -> [String] split c s = case rest of [] -> [chunk] _:rest1 -> chunk : split c rest1 where (chunk, rest) = break (==c) s -- |This function is, essentially, the inverse of 'sgrToCode' exported by -- module "System.Console.ANSI.Codes" of the @ansi-terminal@ package. The -- \'ANSI\' standards refer to (1) standard ECMA-48 \`Control Functions for -- Coded Character Sets\' (5th edition, 1991); (2) extensions in ITU-T -- Recommendation (previously CCITT Recommendation) T.416 (03/93) \'Information -- Technology – Open Document Architecture (ODA) and Interchange Format: -- Character Content Architectures\` (also published as ISO/IEC International -- Standard 8613-6); and (3) further extensions used by \'XTerm\', a terminal -- emulator for the X Window System. The escape codes are described in a -- Wikipedia article at and -- those codes supported on current versions of Windows at -- . codeToSGR :: [Word8] -> (Maybe SGR, [Word8]) codeToSGR [] = (Nothing, []) codeToSGR (c:cs) | c == 0 = (Just Reset, cs) | c == 1 = (Just $ SetConsoleIntensity BoldIntensity, cs) | c == 2 = (Just $ SetConsoleIntensity FaintIntensity, cs) | c == 3 = (Just $ SetItalicized True, cs) | c == 4 = (Just $ SetUnderlining SingleUnderline, cs) | c == 5 = (Just $ SetBlinkSpeed SlowBlink, cs) | c == 6 = (Just $ SetBlinkSpeed RapidBlink, cs) | c == 7 = (Just $ SetSwapForegroundBackground True, cs) | c == 8 = (Just $ SetVisible False, cs) | c == 21 = (Just $ SetUnderlining DoubleUnderline, cs) | c == 22 = (Just $ SetConsoleIntensity NormalIntensity, cs) | c == 23 = (Just $ SetItalicized False, cs) | c == 24 = (Just $ SetUnderlining NoUnderline, cs) | c == 25 = (Just $ SetBlinkSpeed NoBlink, cs) | c == 27 = (Just $ SetSwapForegroundBackground False, cs) | c == 28 = (Just $ SetVisible True, cs) | c >= 30 && c <= 37 = (Just $ SetColor Foreground Dull $ codeToColor (c - 30), cs) | c == 38 = case codeToRGB cs of (Nothing, cs') -> (Nothing, cs') (Just color, cs') -> (Just $ SetRGBColor Foreground color, cs') | c >= 40 && c <= 47 = (Just $ SetColor Background Dull $ codeToColor (c - 40), cs) | c == 48 = case codeToRGB cs of (Nothing, cs') -> (Nothing, cs') (Just color, cs') -> (Just $ SetRGBColor Background color, cs') | c >= 90 && c <= 97 = (Just $ SetColor Foreground Vivid $ codeToColor (c - 90), cs) | c >= 100 && c <= 107 = (Just $ SetColor Background Vivid $ codeToColor (c - 100), cs) | otherwise = (Nothing, cs) -- |This function is, essentially, the inverse of 'colorToCode' exported by -- module "System.Console.ANSI.Codes" of the @ansi-terminal@ package. The -- \'ANSI\' standards refer to eight named colours in a specific order. The code -- is a 0-based index of those colours. codeToColor :: Word8 -> Color codeToColor c -- 'toEnum' is not used because the @ansi-terminal@ package does not -- /guarantee/ the order of the data constructors of type 'Color' will be the -- same as that of the \'ANSI\' standards (although it currently is). (The -- 'colorToCode' function itself does not use 'fromEnum'.) | c == 0 = Black | c == 1 = Red | c == 2 = Green | c == 3 = Yellow | c == 4 = Blue | c == 5 = Magenta | c == 6 = Cyan | c == 7 = White | otherwise = error "Error: codeToColor, code outside 0 to 7." codeToRGB :: [Word8] -> (Maybe (Colour Float), [Word8]) codeToRGB [] = (Nothing, []) codeToRGB (2:r:g:b:cs) = (Just $ sRGB24 r g b, cs) codeToRGB cs = (Nothing, cs) -- | Environment values with a styles update. -- -- @since 0.1.0.0 class HasStylesUpdate env where stylesUpdateL :: Lens' env StylesUpdate instance HasStylesUpdate StylesUpdate where stylesUpdateL = id