rainbow-0.16.0.0: lib/Rainbow/Types.hs
-- | The innards of Rainbow. Ordinarily you should not need this
-- module; instead, just import "Rainbow", which
-- re-exports the most useful names from this module.
module Rainbow.Types where
-- # Imports
import qualified Data.String as Str
import Data.Monoid
import Data.Text (Text)
import Data.Maybe (fromMaybe)
import qualified Data.Text as X
import qualified Data.Text.Lazy as XL
import qualified System.Console.Terminfo as T
import System.IO as IO
import System.Environment as Env
import Data.Word (Word8)
--
-- Terminal definitions
--
-- | Which terminal definition to use.
data Term
= Dumb
-- ^ Using this terminal should always succeed. This suppresses all
-- colors. Uesful if output is not going to a TTY, or if you just do
-- not like colors.
| TermName String
-- ^ Use the terminal with this given name. You might get this from
-- the TERM environment variable, or set it explicitly. A runtime
-- error will result if the terminfo database does not have a
-- definition for this terminal. If this terminal supports 256
-- colors, then 256 colors are used. If this terminal supports less
-- than 256 colors, but at least 8 colors, then 8 colors are
-- used. Otherwise, no colors are used.
deriving (Eq, Show)
-- | Gets the terminal definition from the environment. If the
-- environment does not have a TERM veriable, use 'Dumb'.
termFromEnv :: IO Term
termFromEnv = do
t <- fmap (lookup "TERM") Env.getEnvironment
return $ maybe Dumb TermName t
-- | Gets the terminal definition from the environment and a handle.
-- If the handle is not a terminal, 'Dumb' is returned. Otherwise,
-- the terminal is obtained from the environment.
--
-- /Changed in version 0.12.0.0/ - the type of this function was
-- different in previous versions.
smartTermFromEnv
:: IO.Handle
-- ^ Check this handle to see if it is a terminal (typically you
-- will use stdout).
-> IO Term
smartTermFromEnv h = IO.hIsTerminalDevice h >>= f
where
f isTerm | isTerm = termFromEnv
| otherwise = return Dumb
-- For Background8, Background256, Foreground8, Foreground256: the
-- Last wraps a Maybe (Terminfo Color). If the inner Maybe is Nothing,
-- use the default color.
type Background8 = Last Color8
type Background256 = Last Color256
type Foreground8 = Last Color8
type Foreground256 = Last Color256
--
-- Colors
--
-- | A simple enumeration for eight values.
data Enum8
= E0
| E1
| E2
| E3
| E4
| E5
| E6
| E7
deriving (Eq, Ord, Show, Bounded, Enum)
enum8toWord8 :: Enum8 -> Word8
enum8toWord8 e = case e of
E0 -> 0
E1 -> 1
E2 -> 2
E3 -> 3
E4 -> 4
E5 -> 5
E6 -> 6
E7 -> 7
-- | Color for an 8-color terminal. Does not affect 256-color
-- terminals.
newtype Color8 = Color8
{ unColor8 :: Maybe Enum8
-- ^ Nothing indicates to use the default color for the terminal;
-- otherwise, use the corresponding Terminfo 'T.Color'.
} deriving (Eq, Ord, Show)
color8toTerminfo :: Color8 -> Maybe T.Color
color8toTerminfo = fmap (T.ColorNumber . fromIntegral . enum8toWord8)
. unColor8
-- | Color for an 256-color terminal. Does not affect 8-color
-- terminals.
newtype Color256 = Color256
{ unColor256 :: Maybe Word8
-- ^ Nothing indicates to use the default color for the terminal;
-- otherwise, use the corresponding Terminfo 'T.Color'.
} deriving (Eq, Ord, Show)
color256toTerminfo :: Color256 -> Maybe T.Color
color256toTerminfo = fmap (T.ColorNumber . fromIntegral)
. unColor256
-- | Any color for an 8-color terminal can also be used in a
-- 256-color terminal.
to256 :: Color8 -> Color256
to256 (Color8 mayE) = Color256 $ fmap enum8toWord8 mayE
--
-- Styles
--
-- | Style elements that apply in both 8 and 256 color
-- terminals. However, the elements are described separately for 8 and
-- 256 color terminals, so that the text appearance can change
-- depending on how many colors a terminal has.
data StyleCommon = StyleCommon
{ scBold :: Last Bool
, scUnderline :: Last Bool
, scFlash :: Last Bool
, scInverse :: Last Bool
} deriving (Show, Eq, Ord)
instance Monoid StyleCommon where
mempty = StyleCommon (Last Nothing) (Last Nothing)
(Last Nothing) (Last Nothing)
mappend (StyleCommon b1 u1 f1 i1) (StyleCommon b2 u2 f2 i2)
= StyleCommon (b1 <> b2) (u1 <> u2) (f1 <> f2) (i1 <> i2)
-- | Describes text appearance (foreground and background colors, as
-- well as other attributes such as bold) for an 8 color terminal.
data Style8 = Style8
{ foreground8 :: Foreground8
, background8 :: Background8
, common8 :: StyleCommon
} deriving (Show, Eq, Ord)
instance Monoid Style8 where
mappend (Style8 fx bx cx) (Style8 fy by cy)
= Style8 (fx <> fy) (bx <> by) (cx <> cy)
mempty = Style8 mempty mempty mempty
-- | Describes text appearance (foreground and background colors, as
-- well as other attributes such as bold) for a 256 color terminal.
data Style256 = Style256
{ foreground256 :: Foreground256
, background256 :: Background256
, common256 :: StyleCommon
} deriving (Show, Eq, Ord)
instance Monoid Style256 where
mappend (Style256 fx bx cx) (Style256 fy by cy)
= Style256 (fx <> fy) (bx <> by) (cx <> cy)
mempty = Style256 mempty mempty mempty
--
-- TextSpec
--
-- | The TextSpec bundles together the styles for the 8 and 256 color
-- terminals, so that the text can be portrayed on any terminal.
data TextSpec = TextSpec
{ style8 :: Style8
, style256 :: Style256
} deriving (Show, Eq, Ord)
instance Monoid TextSpec where
mappend (TextSpec x1 x2) (TextSpec y1 y2)
= TextSpec (x1 <> y1) (x2 <> y2)
mempty = TextSpec mempty mempty
--
-- Chunks
--
-- | A chunk is some textual data coupled with a description of what
-- color the text is, attributes like whether it is bold or
-- underlined, etc. The chunk knows what foreground and background
-- colors and what attributes to use for both an 8 color terminal and
-- a 256 color terminal.
--
-- The text is held as a list of strict 'Text'.
data Chunk = Chunk
{ textSpec :: TextSpec
, text :: [Text]
} deriving (Eq, Show, Ord)
instance Str.IsString Chunk where
fromString s = Chunk mempty [(X.pack s)]
-- | Creates a 'Chunk' from a strict 'X.Text' with default colors
-- and no special effects.
fromText :: Text -> Chunk
fromText = Chunk mempty . (:[])
-- | Creates a 'Chunk' from a lazy 'XL.Text' with default colors and
-- no special effects.
fromLazyText :: XL.Text -> Chunk
fromLazyText = Chunk mempty . XL.toChunks
instance Monoid Chunk where
mempty = Chunk mempty mempty
mappend (Chunk s1 t1) (Chunk s2 t2) = Chunk (s1 <> s2) (t1 <> t2)
defaultColors :: T.Terminal -> T.TermOutput
defaultColors term =
fromMaybe mempty (T.getCapability term T.restoreDefaultColors)
commonAttrs :: T.Terminal -> StyleCommon -> T.TermOutput
commonAttrs t s =
let a = T.Attributes
{ T.standoutAttr = False
, T.underlineAttr = fromMaybe False
. getLast . scUnderline $ s
, T.reverseAttr = fromMaybe False
. getLast . scInverse $ s
, T.blinkAttr = fromMaybe False
. getLast . scFlash $ s
, T.dimAttr = False
, T.boldAttr = fromMaybe False
. getLast . scBold $ s
, T.invisibleAttr = False
, T.protectedAttr = False
}
in case T.getCapability t (T.setAttributes) of
Nothing -> error $ "Rainbow: commonAttrs: "
++ "capability failed; should never happen"
Just f -> f a
-- | Gets the right set of terminal codes to apply the desired
-- highlighting, bold, underlining, etc. Be sure to apply the
-- attributes first (bold, underlining, etc) and then the
-- colors. Setting the colors first and then the attributes seems to
-- reset the colors, giving blank output.
getTermCodes
:: T.Terminal
-> TextSpec
-> T.TermOutput
getTermCodes t ts = fromMaybe mempty $ do
cols <- T.getCapability t T.termColors
let TextSpec s8 s256 = ts
Style8 f8 b8 c8 = s8
Style256 f256 b256 c256 = s256
setFg <- T.getCapability t T.setForegroundColor
setBg <- T.getCapability t T.setBackgroundColor
(fg, bg, cm) <- case () of
_ | cols >= 256 -> Just $ ( fmap color256toTerminfo $ getLast f256
, fmap color256toTerminfo $ getLast b256
, c256)
| cols >= 8 -> Just ( fmap color8toTerminfo $ getLast f8
, fmap color8toTerminfo $ getLast b8
, c8)
| otherwise -> Nothing
let oFg = maybe mempty (maybe mempty setFg) fg
oBg = maybe mempty (maybe mempty setBg) bg
oCm = commonAttrs t cm
return $ mconcat [oCm, oFg, oBg]
hPrintChunk :: IO.Handle -> T.Terminal -> Chunk -> IO ()
hPrintChunk h t (Chunk ts xs) =
T.hRunTermOutput h t
. mconcat
$ defaultColors t : codes : (map (T.termText . X.unpack) $ xs)
where
codes = getTermCodes t ts
-- | Sends a list of chunks to the given handle for printing. Sets up
-- the terminal (this only needs to be done once.) Lazily processes
-- the list of Chunk. See 'putChunks' for notes on how many colors
-- are used.
hPutChunks :: IO.Handle -> Term -> [Chunk] -> IO ()
hPutChunks h t cs = do
let setup = case t of
Dumb -> T.setupTerm "dumb"
TermName s -> T.setupTerm s
term <- setup
mapM_ (hPrintChunk h term) cs
T.hRunTermOutput h term (defaultColors term)
T.hRunTermOutput h term
$ case T.getCapability term T.allAttributesOff of
Nothing -> error $ "Rainbow.putChunks: error: "
++ "allAttributesOff failed"
Just s -> s
-- | Sends a list of chunks to standard output for printing. Sets up
-- the terminal (this only needs to be done once.) Lazily processes
-- the list of Chunk.
--
-- Which colors are used depends upon the 'Term'. If it is 'Dumb',
-- then no colors are used on output. If the 'Term' is specified with
-- 'TermName', the UNIX terminfo library is used to determine how many
-- colors the terminal supports. If it supports at least 256 colors,
-- then 256 colors are used. If it supports at least 8 colors but less
-- than 256 colors, then 256 colors are used. Otherwise, no colors are
-- used. A runtime error will occur if the 'TermName' is not found in
-- the system terminal database.
putChunks :: Term -> [Chunk] -> IO ()
putChunks = hPutChunks IO.stdout
-- | Print one chunk at a time, to a handle
hPutChunk :: IO.Handle -> Chunk -> IO ()
hPutChunk h c = do
t <- termFromEnv
hPutChunks h t [c]
-- | Print one chunk at a time, to standard output
putChunk :: Chunk -> IO ()
putChunk = hPutChunk IO.stdout
-- | Print one chunk at a time, to a handle, append a newline
hPutChunkLn :: IO.Handle -> Chunk -> IO ()
hPutChunkLn h c = hPutChunk h c >> IO.hPutStr h "\n"
-- | Print one chunk at a time, to standard output, append a newline
putChunkLn :: Chunk -> IO ()
putChunkLn c = putChunk c >> putStr "\n"
bold8 :: Chunk
bold8 = x {
textSpec = (textSpec x) {
style8 = (style8 (textSpec x)) {
common8 = (common8 (style8 (textSpec x))) {
scBold = Last (Just True) }}}}
where
x = mempty
bold8off :: Chunk
bold8off = x {
textSpec = (textSpec x) {
style8 = (style8 (textSpec x)) {
common8 = (common8 (style8 (textSpec x))) {
scBold = Last (Just False) }}}}
where
x = mempty
underline8 :: Chunk
underline8 = x {
textSpec = (textSpec x) {
style8 = (style8 (textSpec x)) {
common8 = (common8 (style8 (textSpec x))) {
scUnderline = Last (Just True) }}}}
where
x = mempty
underline8off :: Chunk
underline8off = x {
textSpec = (textSpec x) {
style8 = (style8 (textSpec x)) {
common8 = (common8 (style8 (textSpec x))) {
scUnderline = Last (Just False) }}}}
where
x = mempty
flash8 :: Chunk
flash8 = x {
textSpec = (textSpec x) {
style8 = (style8 (textSpec x)) {
common8 = (common8 (style8 (textSpec x))) {
scFlash = Last (Just True) }}}}
where
x = mempty
flash8off :: Chunk
flash8off = x {
textSpec = (textSpec x) {
style8 = (style8 (textSpec x)) {
common8 = (common8 (style8 (textSpec x))) {
scFlash = Last (Just False) }}}}
where
x = mempty
inverse8 :: Chunk
inverse8 = x {
textSpec = (textSpec x) {
style8 = (style8 (textSpec x)) {
common8 = (common8 (style8 (textSpec x))) {
scInverse = Last (Just True) }}}}
where
x = mempty
inverse8off :: Chunk
inverse8off = x {
textSpec = (textSpec x) {
style8 = (style8 (textSpec x)) {
common8 = (common8 (style8 (textSpec x))) {
scInverse = Last (Just False) }}}}
where
x = mempty
underline256 :: Chunk
underline256 = x {
textSpec = (textSpec x) {
style256 = (style256 (textSpec x)) {
common256 = (common256 (style256 (textSpec x))) {
scUnderline = Last (Just True) }}}}
where
x = mempty
underline256off :: Chunk
underline256off = x {
textSpec = (textSpec x) {
style256 = (style256 (textSpec x)) {
common256 = (common256 (style256 (textSpec x))) {
scUnderline = Last (Just False) }}}}
where
x = mempty
bold256 :: Chunk
bold256 = x {
textSpec = (textSpec x) {
style256 = (style256 (textSpec x)) {
common256 = (common256 (style256 (textSpec x))) {
scBold = Last (Just True) }}}}
where
x = mempty
bold256off :: Chunk
bold256off = x {
textSpec = (textSpec x) {
style256 = (style256 (textSpec x)) {
common256 = (common256 (style256 (textSpec x))) {
scBold = Last (Just False) }}}}
where
x = mempty
inverse256 :: Chunk
inverse256 = x {
textSpec = (textSpec x) {
style256 = (style256 (textSpec x)) {
common256 = (common256 (style256 (textSpec x))) {
scInverse = Last (Just True) }}}}
where
x = mempty
inverse256off :: Chunk
inverse256off = x {
textSpec = (textSpec x) {
style256 = (style256 (textSpec x)) {
common256 = (common256 (style256 (textSpec x))) {
scInverse = Last (Just False) }}}}
where
x = mempty
flash256 :: Chunk
flash256 = x {
textSpec = (textSpec x) {
style256 = (style256 (textSpec x)) {
common256 = (common256 (style256 (textSpec x))) {
scFlash = Last (Just True) }}}}
where
x = mempty
flash256off :: Chunk
flash256off = x {
textSpec = (textSpec x) {
style256 = (style256 (textSpec x)) {
common256 = (common256 (style256 (textSpec x))) {
scFlash = Last (Just False) }}}}
where
x = mempty
--
-- All
--
-- | Bold. What actually happens when you use Bold is going to depend
-- on your terminal. For example, xterm allows you actually use a bold
-- font for bold, if you have one. Otherwise, it might simulate bold
-- by using overstriking. Another possibility is that your terminal
-- might use a different color to indicate bold. For more details (at
-- least for xterm), look at xterm (1) and search for @boldColors@.
--
-- If your terminal uses a different color for bold, this allows an
-- 8-color terminal to really have 16 colors.
bold :: Chunk
bold = bold8 <> bold256
boldOff :: Chunk
boldOff = bold8off <> bold256off
inverse :: Chunk
inverse = inverse8 <> inverse256
inverseOff :: Chunk
inverseOff = inverse8off <> inverse256off
flash :: Chunk
flash = flash8 <> flash256
flashOff :: Chunk
flashOff = flash8off <> flash256off
underline :: Chunk
underline = underline8 <> underline256
underlineOff :: Chunk
underlineOff = underline8off <> underline256off