1{-# LANGUAGE RecordWildCards #-}
2{-# LANGUAGE FlexibleInstances #-}
3{-# LANGUAGE OverloadedStrings #-}
4{-# LANGUAGE CPP #-}
5{-# OPTIONS_GHC -D_XOPEN_SOURCE=500 -fno-warn-warnings-deprecations #-}
6{-# CFILES gwinsz.c #-}
7
8-- | Terminfo-based terminal output driver.
9--
10-- Copyright Corey O'Connor (coreyoconnor@gmail.com)
11module Graphics.Vty.Output.TerminfoBased
12  ( reserveTerminal
13  , setWindowSize
14  )
15where
16
17import Control.Monad (when)
18import Data.Bits (shiftL)
19import qualified Data.ByteString as BS
20import Data.ByteString.Internal (toForeignPtr)
21import Data.Terminfo.Parse
22import Data.Terminfo.Eval
23
24import Graphics.Vty.Attributes
25import Graphics.Vty.Image (DisplayRegion)
26import Graphics.Vty.DisplayAttributes
27import Graphics.Vty.Output.Interface
28
29import Blaze.ByteString.Builder (Write, writeToByteString, writeStorable)
30
31import Data.Bits ((.&.))
32import Data.IORef
33import Data.Maybe (isJust, isNothing, fromJust)
34import Data.Word
35
36#if !MIN_VERSION_base(4,8,0)
37import Data.Foldable (foldMap)
38#endif
39
40import Foreign.C.Types ( CInt(..), CLong(..) )
41import Foreign.ForeignPtr (withForeignPtr)
42import Foreign.Ptr (Ptr, plusPtr)
43
44import qualified System.Console.Terminfo as Terminfo
45import System.Posix.IO (fdWriteBuf)
46import System.Posix.Types (Fd(..))
47
48data TerminfoCaps = TerminfoCaps
49    { smcup :: Maybe CapExpression
50    , rmcup :: Maybe CapExpression
51    , cup :: CapExpression
52    , cnorm :: Maybe CapExpression
53    , civis :: Maybe CapExpression
54    , supportsNoColors :: Bool
55    , useAltColorMap :: Bool
56    , setForeColor :: CapExpression
57    , setBackColor :: CapExpression
58    , setDefaultAttr :: CapExpression
59    , clearScreen :: CapExpression
60    , clearEol :: CapExpression
61    , displayAttrCaps :: DisplayAttrCaps
62    , ringBellAudio :: Maybe CapExpression
63    }
64
65data DisplayAttrCaps = DisplayAttrCaps
66    { setAttrStates :: Maybe CapExpression
67    , enterStandout :: Maybe CapExpression
68    , exitStandout :: Maybe CapExpression
69    , enterItalic :: Maybe CapExpression
70    , exitItalic :: Maybe CapExpression
71    , enterStrikethrough :: Maybe CapExpression
72    , exitStrikethrough :: Maybe CapExpression
73    , enterUnderline :: Maybe CapExpression
74    , exitUnderline :: Maybe CapExpression
75    , enterReverseVideo :: Maybe CapExpression
76    , enterDimMode :: Maybe CapExpression
77    , enterBoldMode :: Maybe CapExpression
78    }
79
80-- kinda like:
81-- https://code.google.com/p/vim/source/browse/src/fileio.c#10422
82-- fdWriteBuf will throw on error. Unless the error is EINTR. On EINTR
83-- the write will be retried.
84fdWriteAll :: Fd -> Ptr Word8 -> Int -> Int -> IO Int
85fdWriteAll outFd ptr len count
86    | len <  0  = fail "fdWriteAll: len is less than 0"
87    | len == 0  = return count
88    | otherwise = do
89        writeCount <- fromEnum <$> fdWriteBuf outFd ptr (toEnum len)
90        let len' = len - writeCount
91            ptr' = ptr `plusPtr` writeCount
92            count' = count + writeCount
93        fdWriteAll outFd ptr' len' count'
94
95sendCapToTerminal :: Output -> CapExpression -> [CapParam] -> IO ()
96sendCapToTerminal t cap capParams = do
97    outputByteBuffer t $ writeToByteString $ writeCapExpr cap capParams
98
99-- | Constructs an output driver that uses terminfo for all control
100-- codes. While this should provide the most compatible terminal,
101-- terminfo does not support some features that would increase
102-- efficiency and improve compatibility:
103--
104--  * determining the character encoding supported by the terminal.
105--    Should this be taken from the LANG environment variable?
106--
107--  * Providing independent string capabilities for all display
108--    attributes.
109reserveTerminal :: String -> Fd -> IO Output
110reserveTerminal termName outFd = do
111    ti <- Terminfo.setupTerm termName
112    -- assumes set foreground always implies set background exists.
113    -- if set foreground is not set then all color changing style
114    -- attributes are filtered.
115    msetaf <- probeCap ti "setaf"
116    msetf <- probeCap ti "setf"
117    let (noColors, useAlt, setForeCap)
118            = case msetaf of
119                Just setaf -> (False, False, setaf)
120                Nothing -> case msetf of
121                    Just setf -> (False, True, setf)
122                    Nothing -> (True, True, error $ "no fore color support for terminal " ++ termName)
123    msetab <- probeCap ti "setab"
124    msetb <- probeCap ti "setb"
125    let set_back_cap
126            = case msetab of
127                Nothing -> case msetb of
128                    Just setb -> setb
129                    Nothing -> error $ "no back color support for terminal " ++ termName
130                Just setab -> setab
131
132    hyperlinkModeStatus <- newIORef False
133    newAssumedStateRef <- newIORef initialAssumedState
134
135    let terminfoSetMode m newStatus = do
136          curStatus <- terminfoModeStatus m
137          when (newStatus /= curStatus) $
138              case m of
139                  Hyperlink -> do
140                      writeIORef hyperlinkModeStatus newStatus
141                      writeIORef newAssumedStateRef initialAssumedState
142                  _ -> return ()
143        terminfoModeStatus m =
144            case m of
145                Hyperlink -> readIORef hyperlinkModeStatus
146                _ -> return False
147        terminfoModeSupported Hyperlink = True
148        terminfoModeSupported _ = False
149
150    terminfoCaps <- pure TerminfoCaps
151        <*> probeCap ti "smcup"
152        <*> probeCap ti "rmcup"
153        <*> requireCap ti "cup"
154        <*> probeCap ti "cnorm"
155        <*> probeCap ti "civis"
156        <*> pure noColors
157        <*> pure useAlt
158        <*> pure setForeCap
159        <*> pure set_back_cap
160        <*> requireCap ti "sgr0"
161        <*> requireCap ti "clear"
162        <*> requireCap ti "el"
163        <*> currentDisplayAttrCaps ti
164        <*> probeCap ti "bel"
165    let t = Output
166            { terminalID = termName
167            , releaseTerminal = do
168                sendCap setDefaultAttr []
169                maybeSendCap cnorm []
170            , supportsBell = return $ isJust $ ringBellAudio terminfoCaps
171            , supportsItalics = return $ (isJust $ enterItalic (displayAttrCaps terminfoCaps)) &&
172                                         (isJust $ exitItalic (displayAttrCaps terminfoCaps))
173            , supportsStrikethrough = return $ (isJust $ enterStrikethrough (displayAttrCaps terminfoCaps)) &&
174                                               (isJust $ exitStrikethrough (displayAttrCaps terminfoCaps))
175            , ringTerminalBell = maybeSendCap ringBellAudio []
176            , reserveDisplay = do
177                -- If there is no support for smcup: Clear the screen
178                -- and then move the mouse to the home position to
179                -- approximate the behavior.
180                maybeSendCap smcup []
181                sendCap clearScreen []
182            , releaseDisplay = do
183                maybeSendCap rmcup []
184                maybeSendCap cnorm []
185            , setDisplayBounds = \(w, h) ->
186                setWindowSize outFd (w, h)
187            , displayBounds = do
188                rawSize <- getWindowSize outFd
189                case rawSize of
190                    (w, h)  | w < 0 || h < 0 -> fail $ "getwinsize returned < 0 : " ++ show rawSize
191                            | otherwise      -> return (w,h)
192            , outputByteBuffer = \outBytes -> do
193                let (fptr, offset, len) = toForeignPtr outBytes
194                actualLen <- withForeignPtr fptr
195                             $ \ptr -> fdWriteAll outFd (ptr `plusPtr` offset) len 0
196                when (toEnum len /= actualLen) $ fail $ "Graphics.Vty.Output: outputByteBuffer "
197                  ++ "length mismatch. " ++ show len ++ " /= " ++ show actualLen
198                  ++ " Please report this bug to vty project."
199            , contextColorCount
200                = case supportsNoColors terminfoCaps of
201                    False -> case Terminfo.getCapability ti (Terminfo.tiGetNum "colors" ) of
202                        Nothing -> 8
203                        Just v -> toEnum v
204                    True -> 1
205            , supportsCursorVisibility = isJust $ civis terminfoCaps
206            , supportsMode = terminfoModeSupported
207            , setMode = terminfoSetMode
208            , getModeStatus = terminfoModeStatus
209            , assumedStateRef = newAssumedStateRef
210            -- I think fix would help assure tActual is the only
211            -- reference. I was having issues tho.
212            , mkDisplayContext = \tActual -> terminfoDisplayContext tActual terminfoCaps
213            }
214        sendCap s = sendCapToTerminal t (s terminfoCaps)
215        maybeSendCap s = when (isJust $ s terminfoCaps) . sendCap (fromJust . s)
216    return t
217
218requireCap :: Terminfo.Terminal -> String -> IO CapExpression
219requireCap ti capName
220    = case Terminfo.getCapability ti (Terminfo.tiGetStr capName) of
221        Nothing     -> fail $ "Terminal does not define required capability \"" ++ capName ++ "\""
222        Just capStr -> parseCap capStr
223
224probeCap :: Terminfo.Terminal -> String -> IO (Maybe CapExpression)
225probeCap ti capName
226    = case Terminfo.getCapability ti (Terminfo.tiGetStr capName) of
227        Nothing     -> return Nothing
228        Just capStr -> Just <$> parseCap capStr
229
230parseCap :: String -> IO CapExpression
231parseCap capStr = do
232    case parseCapExpression capStr of
233        Left e -> fail $ show e
234        Right cap -> return cap
235
236currentDisplayAttrCaps :: Terminfo.Terminal -> IO DisplayAttrCaps
237currentDisplayAttrCaps ti
238    =   pure DisplayAttrCaps
239    <*> probeCap ti "sgr"
240    <*> probeCap ti "smso"
241    <*> probeCap ti "rmso"
242    <*> probeCap ti "sitm"
243    <*> probeCap ti "ritm"
244    <*> probeCap ti "smxx"
245    <*> probeCap ti "rmxx"
246    <*> probeCap ti "smul"
247    <*> probeCap ti "rmul"
248    <*> probeCap ti "rev"
249    <*> probeCap ti "dim"
250    <*> probeCap ti "bold"
251
252foreign import ccall "gwinsz.h vty_c_get_window_size" c_getWindowSize :: Fd -> IO CLong
253
254getWindowSize :: Fd -> IO (Int,Int)
255getWindowSize fd = do
256    (a,b) <- (`divMod` 65536) `fmap` c_getWindowSize fd
257    return (fromIntegral b, fromIntegral a)
258
259foreign import ccall "gwinsz.h vty_c_set_window_size" c_setWindowSize :: Fd -> CLong -> IO ()
260
261setWindowSize :: Fd -> (Int, Int) -> IO ()
262setWindowSize fd (w, h) = do
263    let val = (h `shiftL` 16) + w
264    c_setWindowSize fd $ fromIntegral val
265
266terminfoDisplayContext :: Output -> TerminfoCaps -> DisplayRegion -> IO DisplayContext
267terminfoDisplayContext tActual terminfoCaps r = return dc
268    where dc = DisplayContext
269            { contextDevice = tActual
270            , contextRegion = r
271            , writeMoveCursor = \x y -> writeCapExpr (cup terminfoCaps) [toEnum y, toEnum x]
272            , writeShowCursor = case cnorm terminfoCaps of
273                Nothing -> error "this terminal does not support show cursor"
274                Just c -> writeCapExpr c []
275            , writeHideCursor = case civis terminfoCaps of
276                Nothing -> error "this terminal does not support hide cursor"
277                Just c -> writeCapExpr c []
278            , writeSetAttr = terminfoWriteSetAttr dc terminfoCaps
279            , writeDefaultAttr = \urlsEnabled ->
280                writeCapExpr (setDefaultAttr terminfoCaps) [] `mappend`
281                (if urlsEnabled then writeURLEscapes EndLink else mempty) `mappend`
282                (case exitStrikethrough $ displayAttrCaps terminfoCaps of
283                    Just cap -> writeCapExpr cap []
284                    Nothing -> mempty
285                )
286            , writeRowEnd = writeCapExpr (clearEol terminfoCaps) []
287            , inlineHack = return ()
288            }
289
290-- | Write the escape sequences that are used in some terminals to
291-- include embedded hyperlinks. As of yet, this information isn't
292-- included in termcap or terminfo, so this writes them directly
293-- instead of looking up the appropriate capabilities.
294writeURLEscapes :: URLDiff -> Write
295writeURLEscapes (LinkTo url) =
296    foldMap writeStorable (BS.unpack "\x1b]8;;") `mappend`
297    foldMap writeStorable (BS.unpack url) `mappend`
298    writeStorable (0x07 :: Word8)
299writeURLEscapes EndLink =
300    foldMap writeStorable (BS.unpack "\x1b]8;;\a")
301writeURLEscapes NoLinkChange =
302    mempty
303
304-- | Portably setting the display attributes is a giant pain in the ass.
305--
306-- If the terminal supports the sgr capability (which sets the on/off
307-- state of each style directly ; and, for no good reason, resets the
308-- colors to the default) this procedure is used:
309--
310--  0. set the style attributes. This resets the fore and back color.
311--
312--  1, If a foreground color is to be set then set the foreground color
313--
314--  2. likewise with the background color
315--
316-- If the terminal does not support the sgr cap then: if there is a
317-- change from an applied color to the default (in either the fore or
318-- back color) then:
319--
320--  0. reset all display attributes (sgr0)
321--
322--  1. enter required style modes
323--
324--  2. set the fore color if required
325--
326--  3. set the back color if required
327--
328-- Entering the required style modes could require a reset of the
329-- display attributes. If this is the case then the back and fore colors
330-- always need to be set if not default.
331--
332-- This equation implements the above logic.
333--
334-- Note that this assumes the removal of color changes in the
335-- display attributes is done as expected with noColors == True. See
336-- `limitAttrForDisplay`.
337--
338-- Note that this optimizes for fewer state changes followed by fewer
339-- bytes.
340terminfoWriteSetAttr :: DisplayContext -> TerminfoCaps -> Bool -> FixedAttr -> Attr -> DisplayAttrDiff -> Write
341terminfoWriteSetAttr dc terminfoCaps urlsEnabled prevAttr reqAttr diffs =
342    urlAttrs urlsEnabled `mappend` case (foreColorDiff diffs == ColorToDefault) || (backColorDiff diffs == ColorToDefault) of
343        -- The only way to reset either color, portably, to the default
344        -- is to use either the set state capability or the set default
345        -- capability.
346        True  -> do
347            case reqDisplayCapSeqFor (displayAttrCaps terminfoCaps)
348                                     (fixedStyle attr )
349                                     (styleToApplySeq $ fixedStyle attr) of
350                -- only way to reset a color to the defaults
351                EnterExitSeq caps -> writeDefaultAttr dc urlsEnabled
352                                     `mappend`
353                                     foldMap (\cap -> writeCapExpr cap []) caps
354                                     `mappend`
355                                     setColors
356                -- implicitly resets the colors to the defaults
357                SetState state -> writeCapExpr (fromJust $ setAttrStates
358                                                         $ displayAttrCaps
359                                                         $ terminfoCaps
360                                               )
361                                               (sgrArgsForState state)
362                                  `mappend` setItalics
363                                  `mappend` setStrikethrough
364                                  `mappend` setColors
365        -- Otherwise the display colors are not changing or changing
366        -- between two non-default points.
367        False -> do
368            -- Still, it could be the case that the change in display
369            -- attributes requires the colors to be reset because the
370            -- required capability was not available.
371            case reqDisplayCapSeqFor (displayAttrCaps terminfoCaps)
372                                     (fixedStyle attr)
373                                     (styleDiffs diffs) of
374                -- Really, if terminals were re-implemented with modern
375                -- concepts instead of bowing down to 40 yr old dumb
376                -- terminal requirements this would be the only case
377                -- ever reached! Changes the style and color states
378                -- according to the differences with the currently
379                -- applied states.
380                EnterExitSeq caps -> foldMap (\cap -> writeCapExpr cap []) caps
381                                     `mappend`
382                                     writeColorDiff setForeColor (foreColorDiff diffs)
383                                     `mappend`
384                                     writeColorDiff setBackColor (backColorDiff diffs)
385                -- implicitly resets the colors to the defaults
386                SetState state -> writeCapExpr (fromJust $ setAttrStates
387                                                         $ displayAttrCaps terminfoCaps
388                                               )
389                                               (sgrArgsForState state)
390                                  `mappend` setItalics
391                                  `mappend` setStrikethrough
392                                  `mappend` setColors
393    where
394        urlAttrs True = writeURLEscapes (urlDiff diffs)
395        urlAttrs False = mempty
396        colorMap = case useAltColorMap terminfoCaps of
397                        False -> ansiColorIndex
398                        True -> altColorIndex
399        attr = fixDisplayAttr prevAttr reqAttr
400
401        -- italics can't be set via SGR, so here we manually
402        -- apply the enter and exit sequences as needed after
403        -- changing the SGR
404        setItalics
405          | hasStyle (fixedStyle attr) italic
406          , Just sitm <- enterItalic (displayAttrCaps terminfoCaps)
407          = writeCapExpr sitm []
408          | otherwise = mempty
409        setStrikethrough
410          | hasStyle (fixedStyle attr) strikethrough
411          , Just smxx <- enterStrikethrough (displayAttrCaps terminfoCaps)
412          = writeCapExpr smxx []
413          | otherwise = mempty
414        setColors =
415            (case fixedForeColor attr of
416                Just c -> writeCapExpr (setForeColor terminfoCaps)
417                                       [toEnum $ colorMap c]
418                Nothing -> mempty)
419            `mappend`
420            (case fixedBackColor attr of
421                Just c -> writeCapExpr (setBackColor terminfoCaps)
422                                       [toEnum $ colorMap c]
423                Nothing -> mempty)
424        writeColorDiff _f NoColorChange
425            = mempty
426        writeColorDiff _f ColorToDefault
427            = error "ColorToDefault is not a possible case for applyColorDiffs"
428        writeColorDiff f (SetColor c)
429            = writeCapExpr (f terminfoCaps) [toEnum $ colorMap c]
430
431-- | The color table used by a terminal is a 16 color set followed by a
432-- 240 color set that might not be supported by the terminal.
433--
434-- This takes a Color which clearly identifies which pallete to use and
435-- computes the index into the full 256 color pallete.
436ansiColorIndex :: Color -> Int
437ansiColorIndex (ISOColor v) = fromEnum v
438ansiColorIndex (Color240 v) = 16 + fromEnum v
439
440-- | For terminals without setaf/setab
441--
442-- See table in `man terminfo`
443-- Will error if not in table.
444altColorIndex :: Color -> Int
445altColorIndex (ISOColor 0) = 0
446altColorIndex (ISOColor 1) = 4
447altColorIndex (ISOColor 2) = 2
448altColorIndex (ISOColor 3) = 6
449altColorIndex (ISOColor 4) = 1
450altColorIndex (ISOColor 5) = 5
451altColorIndex (ISOColor 6) = 3
452altColorIndex (ISOColor 7) = 7
453altColorIndex (ISOColor v) = fromEnum v
454altColorIndex (Color240 v) = 16 + fromEnum v
455
456{- | The sequence of terminfo caps to apply a given style are determined
457 - according to these rules.
458 -
459 -  1. The assumption is that it's preferable to use the simpler
460 -  enter/exit mode capabilities than the full set display attribute
461 -  state capability.
462 -
463 -  2. If a mode is supposed to be removed but there is not an exit
464 -  capability defined then the display attributes are reset to defaults
465 -  then the display attribute state is set.
466 -
467 -  3. If a mode is supposed to be applied but there is not an enter
468 -  capability defined then then display attribute state is set if
469 -  possible. Otherwise the mode is not applied.
470 -
471 -  4. If the display attribute state is being set then just update the
472 -  arguments to that for any apply/remove.
473 -}
474data DisplayAttrSeq
475    = EnterExitSeq [CapExpression]
476    | SetState DisplayAttrState
477
478data DisplayAttrState = DisplayAttrState
479    { applyStandout :: Bool
480    , applyUnderline :: Bool
481    , applyItalic :: Bool
482    , applyStrikethrough :: Bool
483    , applyReverseVideo :: Bool
484    , applyBlink :: Bool
485    , applyDim :: Bool
486    , applyBold :: Bool
487    }
488
489sgrArgsForState :: DisplayAttrState -> [CapParam]
490sgrArgsForState attrState = map (\b -> if b then 1 else 0)
491    [ applyStandout attrState
492    , applyUnderline attrState
493    , applyReverseVideo attrState
494    , applyBlink attrState
495    , applyDim attrState
496    , applyBold attrState
497    , False -- invis
498    , False -- protect
499    , False -- alt char set
500    ]
501
502reqDisplayCapSeqFor :: DisplayAttrCaps -> Style -> [StyleStateChange] -> DisplayAttrSeq
503reqDisplayCapSeqFor caps s diffs
504    -- if the state transition implied by any diff cannot be supported
505    -- with an enter/exit mode cap then either the state needs to be set
506    -- or the attribute change ignored.
507    = case (any noEnterExitCap diffs, isJust $ setAttrStates caps) of
508        -- If all the diffs have an enter-exit cap then just use those
509        ( False, _    ) -> EnterExitSeq $ map enterExitCap diffs
510        -- If not all the diffs have an enter-exit cap and there is no
511        -- set state cap then filter out all unsupported diffs and just
512        -- apply the rest
513        ( True, False ) -> EnterExitSeq $ map enterExitCap
514                                        $ filter (not . noEnterExitCap) diffs
515        -- if not all the diffs have an enter-exit can and there is a
516        -- set state cap then just use the set state cap.
517        ( True, True  ) -> SetState $ stateForStyle s
518    where
519        noEnterExitCap ApplyStrikethrough = isNothing $ enterStrikethrough caps
520        noEnterExitCap RemoveStrikethrough = isNothing $ exitStrikethrough caps
521        noEnterExitCap ApplyItalic = isNothing $ enterItalic caps
522        noEnterExitCap RemoveItalic = isNothing $ exitItalic caps
523        noEnterExitCap ApplyStandout = isNothing $ enterStandout caps
524        noEnterExitCap RemoveStandout = isNothing $ exitStandout caps
525        noEnterExitCap ApplyUnderline = isNothing $ enterUnderline caps
526        noEnterExitCap RemoveUnderline = isNothing $ exitUnderline caps
527        noEnterExitCap ApplyReverseVideo = isNothing $ enterReverseVideo caps
528        noEnterExitCap RemoveReverseVideo = True
529        noEnterExitCap ApplyBlink = True
530        noEnterExitCap RemoveBlink = True
531        noEnterExitCap ApplyDim = isNothing $ enterDimMode caps
532        noEnterExitCap RemoveDim = True
533        noEnterExitCap ApplyBold = isNothing $ enterBoldMode caps
534        noEnterExitCap RemoveBold = True
535        enterExitCap ApplyStrikethrough = fromJust $ enterStrikethrough caps
536        enterExitCap RemoveStrikethrough = fromJust $ exitStrikethrough caps
537        enterExitCap ApplyItalic = fromJust $ enterItalic caps
538        enterExitCap RemoveItalic = fromJust $ exitItalic caps
539        enterExitCap ApplyStandout = fromJust $ enterStandout caps
540        enterExitCap RemoveStandout = fromJust $ exitStandout caps
541        enterExitCap ApplyUnderline = fromJust $ enterUnderline caps
542        enterExitCap RemoveUnderline = fromJust $ exitUnderline caps
543        enterExitCap ApplyReverseVideo = fromJust $ enterReverseVideo caps
544        enterExitCap ApplyDim = fromJust $ enterDimMode caps
545        enterExitCap ApplyBold = fromJust $ enterBoldMode caps
546        enterExitCap _ = error "enterExitCap applied to diff that was known not to have one."
547
548stateForStyle :: Style -> DisplayAttrState
549stateForStyle s = DisplayAttrState
550    { applyStandout = isStyleSet standout
551    , applyUnderline = isStyleSet underline
552    , applyItalic = isStyleSet italic
553    , applyStrikethrough = isStyleSet strikethrough
554    , applyReverseVideo = isStyleSet reverseVideo
555    , applyBlink = isStyleSet blink
556    , applyDim = isStyleSet dim
557    , applyBold = isStyleSet bold
558    }
559    where isStyleSet = hasStyle s
560
561styleToApplySeq :: Style -> [StyleStateChange]
562styleToApplySeq s = concat
563    [ applyIfRequired ApplyStandout standout
564    , applyIfRequired ApplyUnderline underline
565    , applyIfRequired ApplyItalic italic
566    , applyIfRequired ApplyStrikethrough strikethrough
567    , applyIfRequired ApplyReverseVideo reverseVideo
568    , applyIfRequired ApplyBlink blink
569    , applyIfRequired ApplyDim dim
570    , applyIfRequired ApplyBold bold
571    ]
572    where
573        applyIfRequired op flag
574            = if 0 == (flag .&. s)
575                then []
576                else [op]
577