module Graphics.Vty.Output.TerminfoBased
( reserveTerminal
)
where
import Control.Monad (when)
import qualified Data.ByteString as BS
import Data.ByteString.Internal (toForeignPtr)
import Data.Terminfo.Parse
import Data.Terminfo.Eval
import Graphics.Vty.Attributes
import Graphics.Vty.Image (DisplayRegion)
import Graphics.Vty.DisplayAttributes
import Graphics.Vty.Output.Interface
import Blaze.ByteString.Builder (Write, writeToByteString, writeStorable)
import Control.Monad.Trans
import Data.Bits ((.&.))
import Data.Foldable (foldMap)
import Data.IORef
import Data.Maybe (isJust, isNothing, fromJust)
import Data.Word
import Foreign.C.Types ( CInt(..), CLong(..) )
import Foreign.ForeignPtr (withForeignPtr)
import Foreign.Ptr (Ptr, plusPtr)
import qualified System.Console.Terminfo as Terminfo
import System.Posix.IO (fdWriteBuf)
import System.Posix.Types (Fd(..))
data TerminfoCaps = TerminfoCaps
{ smcup :: Maybe CapExpression
, rmcup :: Maybe CapExpression
, cup :: CapExpression
, cnorm :: Maybe CapExpression
, civis :: Maybe CapExpression
, supportsNoColors :: Bool
, useAltColorMap :: Bool
, setForeColor :: CapExpression
, setBackColor :: CapExpression
, setDefaultAttr :: CapExpression
, clearScreen :: CapExpression
, clearEol :: CapExpression
, displayAttrCaps :: DisplayAttrCaps
, ringBellAudio :: Maybe CapExpression
}
data DisplayAttrCaps = DisplayAttrCaps
{ setAttrStates :: Maybe CapExpression
, enterStandout :: Maybe CapExpression
, exitStandout :: Maybe CapExpression
, enterUnderline :: Maybe CapExpression
, exitUnderline :: Maybe CapExpression
, enterReverseVideo :: Maybe CapExpression
, enterDimMode :: Maybe CapExpression
, enterBoldMode :: Maybe CapExpression
}
fdWriteAll :: Fd -> Ptr Word8 -> Int -> Int -> IO Int
fdWriteAll outFd ptr len count
| len < 0 = fail "fdWriteAll: len is less than 0"
| len == 0 = return count
| otherwise = do
writeCount <- fromEnum <$> fdWriteBuf outFd ptr (toEnum len)
let len' = len writeCount
ptr' = ptr `plusPtr` writeCount
count' = count + writeCount
fdWriteAll outFd ptr' len' count'
sendCapToTerminal :: Output -> CapExpression -> [CapParam] -> IO ()
sendCapToTerminal t cap capParams = do
outputByteBuffer t $ writeToByteString $ writeCapExpr cap capParams
reserveTerminal :: ( Applicative m, MonadIO m ) => String -> Fd -> m Output
reserveTerminal termName outFd = liftIO $ do
ti <- Terminfo.setupTerm termName
msetaf <- probeCap ti "setaf"
msetf <- probeCap ti "setf"
let (noColors, useAlt, setForeCap)
= case msetaf of
Just setaf -> (False, False, setaf)
Nothing -> case msetf of
Just setf -> (False, True, setf)
Nothing -> (True, True, error $ "no fore color support for terminal " ++ termName)
msetab <- probeCap ti "setab"
msetb <- probeCap ti "setb"
let set_back_cap
= case msetab of
Nothing -> case msetb of
Just setb -> setb
Nothing -> error $ "no back color support for terminal " ++ termName
Just setab -> setab
terminfoCaps <- pure TerminfoCaps
<*> probeCap ti "smcup"
<*> probeCap ti "rmcup"
<*> requireCap ti "cup"
<*> probeCap ti "cnorm"
<*> probeCap ti "civis"
<*> pure noColors
<*> pure useAlt
<*> pure setForeCap
<*> pure set_back_cap
<*> requireCap ti "sgr0"
<*> requireCap ti "clear"
<*> requireCap ti "el"
<*> currentDisplayAttrCaps ti
<*> probeCap ti "bel"
newAssumedStateRef <- newIORef initialAssumedState
let t = Output
{ terminalID = termName
, releaseTerminal = liftIO $ do
sendCap setDefaultAttr []
maybeSendCap cnorm []
, supportsBell = return $ isJust $ ringBellAudio terminfoCaps
, ringTerminalBell = liftIO $ maybeSendCap ringBellAudio []
, reserveDisplay = liftIO $ do
maybeSendCap smcup []
sendCap clearScreen []
, releaseDisplay = liftIO $ do
maybeSendCap rmcup []
maybeSendCap cnorm []
, displayBounds = do
rawSize <- liftIO $ getWindowSize outFd
case rawSize of
(w, h) | w < 0 || h < 0 -> fail $ "getwinsize returned < 0 : " ++ show rawSize
| otherwise -> return (w,h)
, outputByteBuffer = \outBytes -> do
let (fptr, offset, len) = toForeignPtr outBytes
actualLen <- withForeignPtr fptr
$ \ptr -> fdWriteAll outFd (ptr `plusPtr` offset) len 0
when (toEnum len /= actualLen) $ fail $ "Graphics.Vty.Output: outputByteBuffer "
++ "length mismatch. " ++ show len ++ " /= " ++ show actualLen
++ " Please report this bug to vty project."
, contextColorCount
= case supportsNoColors terminfoCaps of
False -> case Terminfo.getCapability ti (Terminfo.tiGetNum "colors" ) of
Nothing -> 8
Just v -> toEnum v
True -> 1
, supportsCursorVisibility = isJust $ civis terminfoCaps
, supportsMode = const False
, setMode = const $ const $ return ()
, getModeStatus = const $ return False
, assumedStateRef = newAssumedStateRef
, mkDisplayContext = \tActual -> liftIO . terminfoDisplayContext tActual terminfoCaps
}
sendCap s = sendCapToTerminal t (s terminfoCaps)
maybeSendCap s = when (isJust $ s terminfoCaps) . sendCap (fromJust . s)
return t
requireCap :: (Applicative m, MonadIO m) => Terminfo.Terminal -> String -> m CapExpression
requireCap ti capName
= case Terminfo.getCapability ti (Terminfo.tiGetStr capName) of
Nothing -> fail $ "Terminal does not define required capability \"" ++ capName ++ "\""
Just capStr -> parseCap capStr
probeCap :: (Applicative m, MonadIO m) => Terminfo.Terminal -> String -> m (Maybe CapExpression)
probeCap ti capName
= case Terminfo.getCapability ti (Terminfo.tiGetStr capName) of
Nothing -> return Nothing
Just capStr -> Just <$> parseCap capStr
parseCap :: (Applicative m, MonadIO m) => String -> m CapExpression
parseCap capStr = do
case parseCapExpression capStr of
Left e -> fail $ show e
Right cap -> return cap
currentDisplayAttrCaps :: ( Applicative m, MonadIO m )
=> Terminfo.Terminal
-> m DisplayAttrCaps
currentDisplayAttrCaps ti
= pure DisplayAttrCaps
<*> probeCap ti "sgr"
<*> probeCap ti "smso"
<*> probeCap ti "rmso"
<*> probeCap ti "smul"
<*> probeCap ti "rmul"
<*> probeCap ti "rev"
<*> probeCap ti "dim"
<*> probeCap ti "bold"
foreign import ccall "gwinsz.h vty_c_get_window_size" c_getWindowSize :: Fd -> IO CLong
getWindowSize :: Fd -> IO (Int,Int)
getWindowSize fd = do
(a,b) <- (`divMod` 65536) `fmap` c_getWindowSize fd
return (fromIntegral b, fromIntegral a)
terminfoDisplayContext :: Output -> TerminfoCaps -> DisplayRegion -> IO DisplayContext
terminfoDisplayContext tActual terminfoCaps r = return dc
where dc = DisplayContext
{ contextDevice = tActual
, contextRegion = r
, writeMoveCursor = \x y -> writeCapExpr (cup terminfoCaps) [toEnum y, toEnum x]
, writeShowCursor = case cnorm terminfoCaps of
Nothing -> error "this terminal does not support show cursor"
Just c -> writeCapExpr c []
, writeHideCursor = case civis terminfoCaps of
Nothing -> error "this terminal does not support hide cursor"
Just c -> writeCapExpr c []
, writeSetAttr = terminfoWriteSetAttr dc terminfoCaps
, writeDefaultAttr = writeCapExpr (setDefaultAttr terminfoCaps) []
, writeRowEnd = writeCapExpr (clearEol terminfoCaps) []
, inlineHack = return ()
}
writeURLEscapes :: URLDiff -> Write
writeURLEscapes (LinkTo url) =
foldMap writeStorable (BS.unpack "\x1b]8;;") `mappend`
foldMap writeStorable (BS.unpack url) `mappend`
writeStorable (0x07 :: Word8)
writeURLEscapes EndLink =
foldMap writeStorable (BS.unpack "\x1b]8;;\a")
writeURLEscapes NoLinkChange =
mempty
terminfoWriteSetAttr :: DisplayContext -> TerminfoCaps -> FixedAttr -> Attr -> DisplayAttrDiff -> Write
terminfoWriteSetAttr dc terminfoCaps prevAttr reqAttr diffs = do
urlAttrs `mappend` case (foreColorDiff diffs == ColorToDefault) || (backColorDiff diffs == ColorToDefault) of
True -> do
case reqDisplayCapSeqFor (displayAttrCaps terminfoCaps)
(fixedStyle attr )
(styleToApplySeq $ fixedStyle attr) of
EnterExitSeq caps -> writeDefaultAttr dc
`mappend`
foldMap (\cap -> writeCapExpr cap []) caps
`mappend`
setColors
SetState state -> writeCapExpr (fromJust $ setAttrStates
$ displayAttrCaps
$ terminfoCaps
)
(sgrArgsForState state)
`mappend`
setColors
False -> do
case reqDisplayCapSeqFor (displayAttrCaps terminfoCaps)
(fixedStyle attr)
(styleDiffs diffs) of
EnterExitSeq caps -> foldMap (\cap -> writeCapExpr cap []) caps
`mappend`
writeColorDiff setForeColor (foreColorDiff diffs)
`mappend`
writeColorDiff setBackColor (backColorDiff diffs)
SetState state -> writeCapExpr (fromJust $ setAttrStates
$ displayAttrCaps terminfoCaps
)
(sgrArgsForState state)
`mappend` setColors
where
urlAttrs = writeURLEscapes (urlDiff diffs)
colorMap = case useAltColorMap terminfoCaps of
False -> ansiColorIndex
True -> altColorIndex
attr = fixDisplayAttr prevAttr reqAttr
setColors =
(case fixedForeColor attr of
Just c -> writeCapExpr (setForeColor terminfoCaps)
[toEnum $ colorMap c]
Nothing -> mempty)
`mappend`
(case fixedBackColor attr of
Just c -> writeCapExpr (setBackColor terminfoCaps)
[toEnum $ colorMap c]
Nothing -> mempty)
writeColorDiff _f NoColorChange
= mempty
writeColorDiff _f ColorToDefault
= error "ColorToDefault is not a possible case for applyColorDiffs"
writeColorDiff f (SetColor c)
= writeCapExpr (f terminfoCaps) [toEnum $ colorMap c]
ansiColorIndex :: Color -> Int
ansiColorIndex (ISOColor v) = fromEnum v
ansiColorIndex (Color240 v) = 16 + fromEnum v
altColorIndex :: Color -> Int
altColorIndex (ISOColor 0) = 0
altColorIndex (ISOColor 1) = 4
altColorIndex (ISOColor 2) = 2
altColorIndex (ISOColor 3) = 6
altColorIndex (ISOColor 4) = 1
altColorIndex (ISOColor 5) = 5
altColorIndex (ISOColor 6) = 3
altColorIndex (ISOColor 7) = 7
altColorIndex (ISOColor v) = fromEnum v
altColorIndex (Color240 v) = 16 + fromEnum v
data DisplayAttrSeq
= EnterExitSeq [CapExpression]
| SetState DisplayAttrState
data DisplayAttrState = DisplayAttrState
{ applyStandout :: Bool
, applyUnderline :: Bool
, applyReverseVideo :: Bool
, applyBlink :: Bool
, applyDim :: Bool
, applyBold :: Bool
}
sgrArgsForState :: DisplayAttrState -> [CapParam]
sgrArgsForState attrState = map (\b -> if b then 1 else 0)
[ applyStandout attrState
, applyUnderline attrState
, applyReverseVideo attrState
, applyBlink attrState
, applyDim attrState
, applyBold attrState
, False
, False
, False
]
reqDisplayCapSeqFor :: DisplayAttrCaps -> Style -> [StyleStateChange] -> DisplayAttrSeq
reqDisplayCapSeqFor caps s diffs
= case (any noEnterExitCap diffs, isJust $ setAttrStates caps) of
( False, _ ) -> EnterExitSeq $ map enterExitCap diffs
( True, False ) -> EnterExitSeq $ map enterExitCap
$ filter (not . noEnterExitCap) diffs
( True, True ) -> SetState $ stateForStyle s
where
noEnterExitCap ApplyStandout = isNothing $ enterStandout caps
noEnterExitCap RemoveStandout = isNothing $ exitStandout caps
noEnterExitCap ApplyUnderline = isNothing $ enterUnderline caps
noEnterExitCap RemoveUnderline = isNothing $ exitUnderline caps
noEnterExitCap ApplyReverseVideo = isNothing $ enterReverseVideo caps
noEnterExitCap RemoveReverseVideo = True
noEnterExitCap ApplyBlink = True
noEnterExitCap RemoveBlink = True
noEnterExitCap ApplyDim = isNothing $ enterDimMode caps
noEnterExitCap RemoveDim = True
noEnterExitCap ApplyBold = isNothing $ enterBoldMode caps
noEnterExitCap RemoveBold = True
enterExitCap ApplyStandout = fromJust $ enterStandout caps
enterExitCap RemoveStandout = fromJust $ exitStandout caps
enterExitCap ApplyUnderline = fromJust $ enterUnderline caps
enterExitCap RemoveUnderline = fromJust $ exitUnderline caps
enterExitCap ApplyReverseVideo = fromJust $ enterReverseVideo caps
enterExitCap ApplyDim = fromJust $ enterDimMode caps
enterExitCap ApplyBold = fromJust $ enterBoldMode caps
enterExitCap _ = error "enterExitCap applied to diff that was known not to have one."
stateForStyle :: Style -> DisplayAttrState
stateForStyle s = DisplayAttrState
{ applyStandout = isStyleSet standout
, applyUnderline = isStyleSet underline
, applyReverseVideo = isStyleSet reverseVideo
, applyBlink = isStyleSet blink
, applyDim = isStyleSet dim
, applyBold = isStyleSet bold
}
where isStyleSet = hasStyle s
styleToApplySeq :: Style -> [StyleStateChange]
styleToApplySeq s = concat
[ applyIfRequired ApplyStandout standout
, applyIfRequired ApplyUnderline underline
, applyIfRequired ApplyReverseVideo reverseVideo
, applyIfRequired ApplyBlink blink
, applyIfRequired ApplyDim dim
, applyIfRequired ApplyBlink bold
]
where
applyIfRequired op flag
= if 0 == (flag .&. s)
then []
else [op]