packages feed

chapelure-0.0.1.0: src/Chapelure/Style.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}

-- | Utilities for styling text for terminal output
module Chapelure.Style where

import Control.Applicative ((<|>))
import Data.Colour (Colour)
import Data.Maybe (catMaybes, listToMaybe, fromMaybe)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Data.Word (Word8)
import Prettyprinter (Doc, SimpleDocStream (..))
import System.Console.ANSI (Color, ColorIntensity, ConsoleIntensity, ConsoleLayer (Background, Foreground), SGR (SetColor, SetConsoleIntensity, SetDefaultColor, SetItalicized, SetPaletteColor, SetRGBColor, SetSwapForegroundBackground, SetUnderlining), Underlining, hSetSGR, setSGR, setSGRCode)
import System.IO (Handle)

-- | A collection of style information. Implements 'Monoid'
data Style = Style
  { _intensity :: !(Maybe ConsoleIntensity),
    _italicize :: !(Maybe Bool),
    _underline :: !(Maybe Underlining),
    _negative :: !(Maybe Bool),
    _colorFG :: !(Maybe StyleColor),
    _colorBG :: !(Maybe StyleColor)
  }
  deriving (Show, Eq)

-- | Set the intensity of a style
styleIntensity :: ConsoleIntensity -> Style
styleIntensity si = Style (Just si) Nothing Nothing Nothing Nothing Nothing

-- | Set whether or not a style is italicized
styleItalicized :: Bool -> Style
styleItalicized it = Style Nothing (Just it) Nothing Nothing Nothing Nothing

-- | Set the type of underling for a style
styleUnderline :: Underlining -> Style
styleUnderline ul = Style Nothing Nothing (Just ul) Nothing Nothing Nothing

-- | Sets negative mode for a style; this inverts the foreground and background colours
styleNegative :: Bool -> Style
styleNegative sn = Style Nothing Nothing Nothing (Just sn) Nothing Nothing

-- | Set the foreground color of a style
styleFG :: StyleColor -> Style
styleFG c = Style Nothing Nothing Nothing Nothing (Just c) Nothing

-- | Set the background color of a style
styleBG :: StyleColor -> Style
styleBG c = Style Nothing Nothing Nothing Nothing Nothing (Just c)

instance Semigroup Style where
  Style is it un ne fg bg <> Style is2 it2 un2 ne2 fg2 bg2 =
    Style (is <|> is2) (it <|> it2) (un <|> un2) (ne <|> ne2) (fg <|> fg2) (bg <|> bg2)

instance Monoid Style where
  mempty = Style Nothing Nothing Nothing Nothing Nothing Nothing

-- | Various kinds of colors that can be used in a terminal
data StyleColor
  = ColorDefault
  -- | A 16-colour palette: 8 hues and 2 intensities. The most commonly supported form of terminal colouring
  | Color16 !ColorIntensity !Color
  -- | A fixed 256-color palette
  | Color256 !Word8
  -- | Full 24-bit true colors
  | ColorRGB !(Colour Float)
  deriving (Show, Eq)

-- | Styled, pretty-printed 'Doc'uments
type DocText = Doc Style

-- | Converts a 'Style' to a list of Set Graphics Rendition 'SGR' codes
toSGR :: Style -> [SGR]
toSGR (Style is it un ne fg bg) =
  catMaybes
    [ SetConsoleIntensity <$> is,
      SetItalicized <$> it,
      SetUnderlining <$> un,
      SetSwapForegroundBackground <$> ne,
      go Foreground <$> fg,
      go Background <$> bg
    ]
  where
    go :: ConsoleLayer -> StyleColor -> SGR
    go l = \case
      ColorDefault -> SetDefaultColor l
      Color16 ci co -> SetColor l ci co
      Color256 wo -> SetPaletteColor l wo
      ColorRGB co -> SetRGBColor l co

-- | Converts a style to a string containing the escape codes to set that style
setStyleCode :: Style -> String
setStyleCode = setSGRCode . toSGR

-- | Sets the graphics rendition mode of standard out to a style
setStyle :: Style -> IO ()
setStyle = setSGR . toSGR

-- | Sets the graphics rendition mode of a handle to a style
hSetStyle :: Handle -> Style -> IO ()
hSetStyle h = hSetSGR h . toSGR

-- | Render a styled document stream to text, including ANSI escape codes to set the style
renderDoc :: SimpleDocStream Style -> T.Text
renderDoc = go []
  where
    go :: [Maybe Style] -> SimpleDocStream Style -> T.Text
    go st = \case
      SFail -> error "SFail left in pretty-printed output"
      SEmpty -> mempty
      SChar c sds -> T.singleton c <> go st sds
      SText _n txt sds -> txt <> go st sds
      SLine n sds -> T.singleton '\n' <> T.replicate n (T.singleton ' ') <> go st sds
      SAnnPush st' sds -> if st' == mempty
        then go (Nothing : st) sds
        else T.pack (setStyleCode (fromMaybe mempty (fromMaybe mempty (listToMaybe st)) <> st')) <> go (Just st' : st) sds
      SAnnPop sds -> case st of
        [] -> go [] sds
        [Nothing] -> go [] sds
        (Nothing : t : ss) -> go (t : ss) sds
        [Just _s] -> T.pack (setStyleCode mempty) <> go [] sds
        (Just _s : t : ss) -> maybe mempty (T.pack . setStyleCode) t <> go (t : ss) sds

-- | Output a styled document stream to standard out
putDocText :: SimpleDocStream Style -> IO ()
putDocText = go []
  where
    go :: [Maybe Style] -> SimpleDocStream Style -> IO ()
    go st = \case
      SFail -> error "SFail left in pretty-printed output"
      SEmpty -> pure ()
      SChar c sds -> putChar c >> go st sds
      SText _n txt sds -> T.putStr txt >> go st sds
      SLine n sds -> putChar '\n' >> T.putStr (T.replicate n (T.singleton ' ')) >> go st sds
      SAnnPush st' sds -> if st' == mempty
        then go (Nothing : st) sds
        else do
          setStyle (fromMaybe mempty (fromMaybe mempty (listToMaybe st)) <> st')
          go (Just st' : st) sds
      SAnnPop sds -> case st of
        [] -> go [] sds
        [Nothing] -> go [] sds
        (Nothing : t : ss) -> go (t : ss) sds
        [Just _s] -> setStyle mempty >> go [] sds
        (Just _s : t : ss) -> maybe (pure ()) setStyle t >> go (t : ss) sds