packages feed

sectile-0.1.0.0: src/Data/Sectile/Tmux.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module        : Data.Sectile.Tmux
-- Copyright     : Gautier DI FOLCO
-- License       : ISC
--
-- Maintainer    : Gautier DI FOLCO <foss@difolco.dev>
-- Stability     : Stable
-- Portability   : Portable
module Data.Sectile.Tmux
  ( Chunk (..),
    ChunkStyle (..),
    Colour (..),
    TerminalColour (..),
    Brightness (..),
    ConsoleIntensity (..),
    Underlining (..),
    Blinking (..),
    noStyle,
    chunkWidth,
    TerminalCapabilities (..),
    renderChunksUtf8BSBuilder,
    renderChunkStyleUtf8BSBuilder,
    parseAnsiChunks,
    renderColour,
  )
where

import qualified Data.ByteString.Builder as B
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Word (Word8)
import Numeric (showHex)

-- | The eight named terminal colours.
data TerminalColour = Black | Red | Green | Yellow | Blue | Magenta | Cyan | White
  deriving (Show, Eq, Ord)

-- | Dull or bright variant of a 'TerminalColour'.
data Brightness = Bright | Dull
  deriving (Show, Eq, Ord)

-- | A colour: either an 8-colour ('Brightness' + 'TerminalColour') or a 24-bit RGB triple.
data Colour
  = Colour8 Brightness TerminalColour
  | Colour24Bit Word8 Word8 Word8
  deriving (Show, Eq, Ord)

-- | Text emphasis: bold, faint, or normal.
data ConsoleIntensity = BoldIntensity | FaintIntensity | NormalIntensity
  deriving (Show, Eq, Ord)

-- | Underlining style: single, double, or none.
data Underlining = SingleUnderline | DoubleUnderline | NoUnderline
  deriving (Show, Eq, Ord)

-- | Blinking style: slow, rapid, or none.
data Blinking = SlowBlinking | RapidBlinking | NoBlinking
  deriving (Show, Eq, Ord)

-- | Styling attributes for a chunk; every attribute is optional.
--
-- Fields cover foreground\/background colours, italic, strikethrough,
-- reversed, concealed, overlined, console intensity, underlining,
-- blinking, and hyperlink URL.
data ChunkStyle = ChunkStyle
  { chunkStyleForeground :: Maybe Colour,
    chunkStyleBackground :: Maybe Colour,
    chunkStyleItalic :: Maybe Bool,
    chunkStyleStrikethrough :: Maybe Bool,
    chunkStyleSwapForegroundBackground :: Maybe Bool,
    chunkStyleConcealed :: Maybe Bool,
    chunkStyleOverlined :: Maybe Bool,
    chunkStyleConsoleIntensity :: Maybe ConsoleIntensity,
    chunkStyleUnderlining :: Maybe Underlining,
    chunkStyleBlinking :: Maybe Blinking,
    chunkStyleHyperlink :: Maybe Text
  }
  deriving (Show, Eq, Ord)

-- | A 'ChunkStyle' with no styling applied.
noStyle :: ChunkStyle
noStyle =
  ChunkStyle
    { chunkStyleForeground = Nothing,
      chunkStyleBackground = Nothing,
      chunkStyleItalic = Nothing,
      chunkStyleStrikethrough = Nothing,
      chunkStyleSwapForegroundBackground = Nothing,
      chunkStyleConcealed = Nothing,
      chunkStyleOverlined = Nothing,
      chunkStyleConsoleIntensity = Nothing,
      chunkStyleUnderlining = Nothing,
      chunkStyleBlinking = Nothing,
      chunkStyleHyperlink = Nothing
    }

-- | A piece of rendered text and its style.
data Chunk = Chunk
  { chunkText :: Text,
    chunkStyle :: ChunkStyle
  }

-- | Length (in code points) of the chunk's text.
chunkWidth :: Chunk -> Int
chunkWidth = T.length . chunkText

-- | Colour support of the target terminal.
data TerminalCapabilities
  = WithoutColours
  | With8Colours
  | With8BitColours
  | With24BitColours
  deriving (Show, Eq, Ord)

-- | Wrap text as a single chunk carrying the given base style;
-- no ANSI sequence processing is performed.
parseAnsiChunks :: ChunkStyle -> Text -> (ChunkStyle, [Chunk])
parseAnsiChunks style txt = (style, [Chunk txt style])

-- | Render chunks as a tmux style-prefixed UTF-8 'B.Builder'.
renderChunksUtf8BSBuilder :: TerminalCapabilities -> [Chunk] -> B.Builder
renderChunksUtf8BSBuilder cap = foldMap renderChunk
  where
    renderChunk c =
      let txt = chunkText c
       in case renderChunkStyleUtf8BSBuilder cap (chunkStyle c) of
            Nothing -> B.byteString (T.encodeUtf8 txt)
            Just renderedStyle -> renderedStyle <> B.byteString (T.encodeUtf8 txt) <> "#[default]"

-- | Render a style as a tmux style specification, or 'Nothing' when the
-- terminal has no colour support or the style is empty.
renderChunkStyleUtf8BSBuilder :: TerminalCapabilities -> ChunkStyle -> Maybe B.Builder
renderChunkStyleUtf8BSBuilder cap style =
  if cap == WithoutColours || null attrs
    then Nothing
    else Just $ "#[" <> B.byteString (T.encodeUtf8 $ T.intercalate "," attrs) <> "]"
  where
    fg = case chunkStyleForeground style of
      Nothing -> []
      Just col -> ["fg=" <> renderColour col]
    bg = case chunkStyleBackground style of
      Nothing -> []
      Just col -> ["bg=" <> renderColour col]
    bold = case chunkStyleConsoleIntensity style of
      Just BoldIntensity -> ["bold"]
      Just FaintIntensity -> ["dim"]
      _ -> []
    italic = case chunkStyleItalic style of
      Just True -> ["italics"]
      _ -> []
    underlined = case chunkStyleUnderlining style of
      Just SingleUnderline -> ["underscore"]
      Just DoubleUnderline -> ["underscore"]
      _ -> []
    blink = case chunkStyleBlinking style of
      Just SlowBlinking -> ["blink"]
      Just RapidBlinking -> ["blink"]
      _ -> []
    reverse' = case chunkStyleSwapForegroundBackground style of
      Just True -> ["reverse"]
      _ -> []
    hidden = case chunkStyleConcealed style of
      Just True -> ["hidden"]
      _ -> []
    strike = case chunkStyleStrikethrough style of
      Just True -> ["strikethrough"]
      _ -> []
    attrs = mconcat [fg, bg, bold, italic, underlined, blink, reverse', hidden, strike]

-- | Render a 'Colour' as a tmux colour name or hex value.
renderColour :: Colour -> Text
renderColour =
  \case
    Colour8 _ Black -> "black"
    Colour8 _ Red -> "red"
    Colour8 _ Green -> "green"
    Colour8 _ Yellow -> "yellow"
    Colour8 _ Blue -> "blue"
    Colour8 _ Magenta -> "magenta"
    Colour8 _ Cyan -> "cyan"
    Colour8 _ White -> "white"
    Colour24Bit r g b ->
      let hex = pad (showHex r "") <> pad (showHex g "") <> pad (showHex b "")
          pad s
            | length s == 1 = "0" <> s
            | otherwise = s
       in "#" <> T.pack hex