sectile-0.1.0.0: src/Data/Sectile/Style.hs
-- |
-- Module : Data.Sectile.Style
-- Copyright : Gautier DI FOLCO
-- License : ISC
--
-- Maintainer : Gautier DI FOLCO <foss@difolco.dev>
-- Stability : Stable
-- Portability : Portable
module Data.Sectile.Style
( -- * Style combinators
between,
changeStyle,
forceStyle,
-- * Style transformations
resetStyle,
swapForegroundBackgroundStyle,
-- * Combinators
warnIf,
GradientSource (..),
parseTextGradient,
scaleGradient,
ratioGradient,
gradient,
-- * Style optics
styleItalic,
styleStrikethrough,
styleSwapForegroundBackground,
styleConcealed,
styleOverlined,
styleConsoleIntensity,
styleUnderlining,
styleBlinking,
styleForeground,
styleBackground,
styleHyperlink,
)
where
import qualified Data.Aeson as Aeson
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Sectile.Tmux as Colour
import Data.Sectile.Types
import qualified Data.Text as T
import Data.Word (Word8)
import qualified Optics.Core as Optics
-- | Wrap a list of segments between a start and end segment.
--
-- Example:
--
-- > import Data.Sectile
-- >
-- > wrapped :: [Segment IO]
-- > wrapped = between (string "[") (string "]") [string "a", string "b"]
-- > -- Produces: [string "[", string "a", string "b", string "]"]
between :: Segment m -> Segment m -> [Segment m] -> [Segment m]
between start end ss = start : (ss <> [end])
-- | Modify the incoming style before it reaches a segment.
--
-- The style transformation is applied to the style passed *into* the segment,
-- but does not affect the rendered output retroactively.
--
-- Example:
--
-- > import Data.Sectile
-- > import qualified Data.Sectile.Tmux as Colour
-- >
-- > boldSegment :: Segment IO -> Segment IO
-- > boldSegment = changeStyle (\s -> s {Colour.chunkStyleConsoleIntensity = Just Colour.BoldIntensity})
changeStyle :: (Functor m) => (Colour.ChunkStyle -> Colour.ChunkStyle) -> Segment m -> Segment m
changeStyle c (Segment s) = Segment $ fmap transform s
where
transform action = do
_ <- updateStyle c
action
-- | Force a style transformation on all chunks in a segment's output.
--
-- Unlike 'changeStyle', this modifies every chunk in the rendered output,
-- the final style, and the explanation renderer.
--
-- Example:
--
-- > import Data.Sectile
-- > import qualified Data.Sectile.Tmux as Colour
-- >
-- > makeItalic :: Segment IO -> Segment IO
-- > makeItalic = forceStyle (\s -> s {Colour.chunkStyleItalic = Just True})
forceStyle :: (Functor m) => (Colour.ChunkStyle -> Colour.ChunkStyle) -> Segment m -> Segment m
forceStyle c (Segment s) = Segment $ fmap transform s
where
transform action = do
formatted <- action
_ <- updateStyle c
pure
formatted
{ rendered = updateChunk <$> formatted.rendered,
explain = \renderSyle renderChunks ->
formatted.explain renderSyle $ renderChunks . map updateChunk
}
updateChunk chunk = chunk {Colour.chunkStyle = c $ Colour.chunkStyle chunk}
-- | Reset a style to the default (no styling).
--
-- Example:
--
-- > import Data.Sectile
-- >
-- > plain :: Segment IO -> Segment IO
-- > plain = changeStyle resetStyle
resetStyle :: Colour.ChunkStyle -> Colour.ChunkStyle
resetStyle = const Colour.noStyle
-- | Swap foreground and background colours in a style.
--
-- Example:
--
-- > import Data.Sectile
-- >
-- > inverted :: Segment IO -> Segment IO
-- > inverted = forceStyle swapForegroundBackgroundStyle
swapForegroundBackgroundStyle :: Colour.ChunkStyle -> Colour.ChunkStyle
swapForegroundBackgroundStyle s =
s
{ Colour.chunkStyleForeground = Colour.chunkStyleBackground s,
Colour.chunkStyleBackground = Colour.chunkStyleForeground s
}
-- | Apply a style if the segment text matches a predicate.
--
-- Example:
--
-- > import Data.Sectile
-- > import Data.Sectile.Style
-- > import qualified Data.Sectile.Tmux as Colour
-- > import qualified Data.Text as T
-- >
-- > alert :: Segment IO -> Segment IO
-- > alert = warnIf (\t -> "Error" `T.isInfixOf` t) (Colour.noStyle {Colour.chunkStyleForeground = Just (Colour.Colour8 Colour.Bright Colour.Red)})
warnIf :: (Functor m) => (T.Text -> Bool) -> Colour.ChunkStyle -> Segment m -> Segment m
warnIf p warnStyle (Segment s) = Segment $ fmap transform s
where
transform action = do
formatted <- action
let txt = mconcat $ map Colour.chunkText formatted.rendered
applyWarn c = c {Colour.chunkStyle = warnStyle}
if p txt
then
pure
formatted
{ rendered = map applyWarn formatted.rendered,
explain = \renderSyle renderChunks ->
formatted.explain renderSyle $ renderChunks . map applyWarn
}
else pure formatted
-- | A source of gradient input: extracts a value from the segment
-- bindings or the rendered text.
newtype GradientSource = GradientSource (HashMap.HashMap T.Text Aeson.Value -> T.Text -> Maybe Double)
-- | Build a 'GradientSource' by parsing the segment's rendered text.
parseTextGradient :: (T.Text -> Maybe Double) -> GradientSource
parseTextGradient f = GradientSource $ \_ txt -> f txt
-- | Build a 'GradientSource' from a numeric binding stored under @key@.
scaleGradient :: T.Text -> GradientSource
scaleGradient key = GradientSource $ \bnds _ ->
case HashMap.lookup key bnds of
Just (Aeson.Number n) -> Just (realToFrac n)
_ -> Nothing
-- | Build a 'GradientSource' from the ratio of two numeric bindings.
ratioGradient :: T.Text -> T.Text -> GradientSource
ratioGradient k1 k2 = GradientSource $ \bnds _ ->
case (HashMap.lookup k1 bnds, HashMap.lookup k2 bnds) of
(Just (Aeson.Number n1), Just (Aeson.Number n2)) | n2 /= 0 -> Just (realToFrac (n1 / n2))
_ -> Nothing
-- | Apply a color gradient based on a parsed value.
gradient ::
(Functor m) =>
(Colour.Colour -> Colour.ChunkStyle -> Colour.ChunkStyle) ->
(Word8, Word8, Word8) ->
(Word8, Word8, Word8) ->
GradientSource ->
Segment m ->
Segment m
gradient applyColor (r1, g1, b1) (r2, g2, b2) source (Segment s) =
Segment $ fmap transform s
where
transform action = do
formatted <- action
bnds <- currentBindings
let txt = mconcat $ map Colour.chunkText formatted.rendered
let GradientSource gradFn = source
let mPct = gradFn bnds txt
case mPct of
Just pct -> do
let p = max 0 (min 1 pct)
r = round $ fromIntegral r1 * (1 - p) + fromIntegral r2 * p
g = round $ fromIntegral g1 * (1 - p) + fromIntegral g2 * p
b = round $ fromIntegral b1 * (1 - p) + fromIntegral b2 * p
col = Colour.Colour24Bit r g b
applyGrad = applyColor col
_ <- updateStyle applyGrad
let applyGradChunk chunk =
chunk
{ Colour.chunkStyle = applyGrad $ Colour.chunkStyle chunk
}
pure
formatted
{ rendered = map applyGradChunk formatted.rendered,
explain = \renderSyle renderChunks ->
formatted.explain renderSyle $ renderChunks . map applyGradChunk
}
Nothing -> pure formatted
-- | Lens for the italic flag of a 'Colour.ChunkStyle'.
styleItalic :: Optics.Lens' Colour.ChunkStyle (Maybe Bool)
styleItalic = Optics.lens Colour.chunkStyleItalic (\s a -> s {Colour.chunkStyleItalic = a})
-- | Lens for the strikethrough flag of a 'Colour.ChunkStyle'.
styleStrikethrough :: Optics.Lens' Colour.ChunkStyle (Maybe Bool)
styleStrikethrough = Optics.lens Colour.chunkStyleStrikethrough (\s a -> s {Colour.chunkStyleStrikethrough = a})
-- | Lens for the swap-foreground-background flag of a 'Colour.ChunkStyle'.
styleSwapForegroundBackground :: Optics.Lens' Colour.ChunkStyle (Maybe Bool)
styleSwapForegroundBackground = Optics.lens Colour.chunkStyleSwapForegroundBackground (\s a -> s {Colour.chunkStyleSwapForegroundBackground = a})
-- | Lens for the concealed flag of a 'Colour.ChunkStyle'.
styleConcealed :: Optics.Lens' Colour.ChunkStyle (Maybe Bool)
styleConcealed = Optics.lens Colour.chunkStyleConcealed (\s a -> s {Colour.chunkStyleConcealed = a})
-- | Lens for the overlined flag of a 'Colour.ChunkStyle'.
styleOverlined :: Optics.Lens' Colour.ChunkStyle (Maybe Bool)
styleOverlined = Optics.lens Colour.chunkStyleOverlined (\s a -> s {Colour.chunkStyleOverlined = a})
-- | Lens for the console intensity of a 'Colour.ChunkStyle'.
styleConsoleIntensity :: Optics.Lens' Colour.ChunkStyle (Maybe Colour.ConsoleIntensity)
styleConsoleIntensity = Optics.lens Colour.chunkStyleConsoleIntensity (\s a -> s {Colour.chunkStyleConsoleIntensity = a})
-- | Lens for the underlining of a 'Colour.ChunkStyle'.
styleUnderlining :: Optics.Lens' Colour.ChunkStyle (Maybe Colour.Underlining)
styleUnderlining = Optics.lens Colour.chunkStyleUnderlining (\s a -> s {Colour.chunkStyleUnderlining = a})
-- | Lens for the blinking of a 'Colour.ChunkStyle'.
styleBlinking :: Optics.Lens' Colour.ChunkStyle (Maybe Colour.Blinking)
styleBlinking = Optics.lens Colour.chunkStyleBlinking (\s a -> s {Colour.chunkStyleBlinking = a})
-- | Lens for the foreground colour of a 'Colour.ChunkStyle'.
styleForeground :: Optics.Lens' Colour.ChunkStyle (Maybe Colour.Colour)
styleForeground = Optics.lens Colour.chunkStyleForeground (\s a -> s {Colour.chunkStyleForeground = a})
-- | Lens for the background colour of a 'Colour.ChunkStyle'.
styleBackground :: Optics.Lens' Colour.ChunkStyle (Maybe Colour.Colour)
styleBackground = Optics.lens Colour.chunkStyleBackground (\s a -> s {Colour.chunkStyleBackground = a})
-- | Lens for the hyperlink URL of a 'Colour.ChunkStyle'.
styleHyperlink :: Optics.Lens' Colour.ChunkStyle (Maybe T.Text)
styleHyperlink = Optics.lens Colour.chunkStyleHyperlink (\s a -> s {Colour.chunkStyleHyperlink = a})