vty-5.28: src/Graphics/Vty.hs
{-# LANGUAGE CPP #-}
-- | Vty provides interfaces for both terminal input and terminal
-- output.
--
-- - Input to the terminal is provided to the Vty application as a
-- sequence of 'Event's.
--
-- - Output is provided to Vty by the application in the form of a
-- 'Picture'. A 'Picture' is one or more layers of 'Image's.
-- 'Image' values can be built by the various constructors in
-- "Graphics.Vty.Image". Output can be syled using 'Attr' (attribute)
-- values in the "Graphics.Vty.Attributes" module.
--
-- Vty uses threads internally, so programs made with Vty need to be
-- compiled with the threaded runtime using the GHC @-threaded@ option.
--
-- @
-- import "Graphics.Vty"
--
-- main = do
-- cfg <- 'standardIOConfig'
-- vty <- 'mkVty' cfg
-- let line0 = 'string' ('defAttr' ` 'withForeColor' ` 'green') \"first line\"
-- line1 = 'string' ('defAttr' ` 'withBackColor' ` 'blue') \"second line\"
-- img = line0 '<->' line1
-- pic = 'picForImage' img
-- 'update' vty pic
-- e <- 'nextEvent' vty
-- 'shutdown' vty
-- 'print' (\"Last event was: \" '++' 'show' e)
-- @
module Graphics.Vty
( Vty(..)
, mkVty
, Mode(..)
, module Graphics.Vty.Config
, module Graphics.Vty.Input
, module Graphics.Vty.Output
, module Graphics.Vty.Output.Interface
, module Graphics.Vty.Picture
, module Graphics.Vty.Image
, module Graphics.Vty.Attributes
)
where
import Graphics.Vty.Config
import Graphics.Vty.Input
import Graphics.Vty.Output
import Graphics.Vty.Output.Interface
import Graphics.Vty.Picture
import Graphics.Vty.Image
import Graphics.Vty.Attributes
import Graphics.Vty.UnicodeWidthTable.IO
import Graphics.Vty.UnicodeWidthTable.Install
import qualified Control.Exception as E
import Control.Monad (when)
import Control.Concurrent.STM
import Data.IORef
#if !(MIN_VERSION_base(4,11,0))
import Data.Semigroup ((<>))
#endif
-- | A Vty value represents a handle to the Vty library that the
-- application must create in order to use Vty.
--
-- The use of Vty typically follows this process:
--
-- 1. Initialize vty with 'mkVty' (this takes control of the terminal).
--
-- 2. Use 'update' to display a picture.
--
-- 3. Use 'nextEvent' to get the next input event.
--
-- 4. Depending on the event, go to 2 or 5.
--
-- 5. Shutdown vty and restore the terminal state with 'shutdown'. At
-- this point the 'Vty' handle cannot be used again.
--
-- Operations on Vty handles are not thread-safe.
data Vty =
Vty { update :: Picture -> IO ()
-- ^ Outputs the given 'Picture'.
, nextEvent :: IO Event
-- ^ Return the next 'Event' or block until one becomes
-- available.
, nextEventNonblocking :: IO (Maybe Event)
-- ^ Non-blocking version of 'nextEvent'.
, inputIface :: Input
-- ^ The input interface. See 'Input'.
, outputIface :: Output
-- ^ The output interface. See 'Output'.
, refresh :: IO ()
-- ^ Refresh the display. If other programs output to the
-- terminal and mess up the display then the application might
-- want to force a refresh using this function.
, shutdown :: IO ()
-- ^ Clean up after vty. A call to this function is necessary to
-- cleanly restore the terminal state before application exit.
-- The above methods will throw an exception if executed after
-- this is executed. Idempotent.
, isShutdown :: IO Bool
}
-- | Create a Vty handle. At most one handle should be created at a time
-- for a given terminal device.
--
-- The specified configuration is added to the the configuration
-- loaded by 'userConfig' with the 'userConfig' configuration taking
-- precedence. See "Graphics.Vty.Config".
--
-- For most applications @mkVty defaultConfig@ is sufficient.
mkVty :: Config -> IO Vty
mkVty appConfig = do
config <- (<> appConfig) <$> userConfig
when (allowCustomUnicodeWidthTables config /= Just False) $
installCustomWidthTable config
input <- inputForConfig config
out <- outputForConfig config
internalMkVty input out
installCustomWidthTable :: Config -> IO ()
installCustomWidthTable c = do
let doLog s = case debugLog c of
Nothing -> return ()
Just path -> appendFile path $ "installWidthTable: " <> s <> "\n"
customInstalled <- isCustomTableReady
when (not customInstalled) $ do
mTerm <- currentTerminalName
case mTerm of
Nothing ->
doLog "No current terminal name available"
Just currentTerm ->
case lookup currentTerm (termWidthMaps c) of
Nothing ->
doLog "Current terminal not found in custom character width mapping list"
Just path -> do
tableResult <- E.try $ readUnicodeWidthTable path
case tableResult of
Left (e::E.SomeException) ->
doLog $ "Error reading custom character width table " <>
"at " <> show path <> ": " <> show e
Right (Left msg) ->
doLog $ "Error reading custom character width table " <>
"at " <> show path <> ": " <> msg
Right (Right table) -> do
installResult <- E.try $ installUnicodeWidthTable table
case installResult of
Left (e::E.SomeException) ->
doLog $ "Error installing unicode table (" <>
show path <> ": " <> show e
Right () ->
doLog $ "Successfully installed Unicode width table " <>
" from " <> show path
internalMkVty :: Input -> Output -> IO Vty
internalMkVty input out = do
reserveDisplay out
shutdownVar <- atomically $ newTVar False
let shutdownIo = do
alreadyShutdown <- atomically $ swapTVar shutdownVar True
when (not alreadyShutdown) $ do
shutdownInput input
releaseDisplay out
releaseTerminal out
let shutdownStatus = atomically $ readTVar shutdownVar
lastPicRef <- newIORef Nothing
lastUpdateRef <- newIORef Nothing
let innerUpdate inPic = do
b <- displayBounds out
mlastUpdate <- readIORef lastUpdateRef
updateData <- case mlastUpdate of
Nothing -> do
dc <- displayContext out b
outputPicture dc inPic
return (b, dc)
Just (lastBounds, lastContext) -> do
if b /= lastBounds
then do
dc <- displayContext out b
outputPicture dc inPic
return (b, dc)
else do
outputPicture lastContext inPic
return (b, lastContext)
writeIORef lastUpdateRef $ Just updateData
writeIORef lastPicRef $ Just inPic
let innerRefresh = do
writeIORef lastUpdateRef Nothing
bounds <- displayBounds out
dc <- displayContext out bounds
writeIORef (assumedStateRef $ contextDevice dc) initialAssumedState
mPic <- readIORef lastPicRef
maybe (return ()) innerUpdate mPic
let mkResize = uncurry EvResize <$> displayBounds out
gkey = do
k <- atomically $ readTChan $ _eventChannel input
case k of
(EvResize _ _) -> mkResize
_ -> return k
gkey' = do
k <- atomically $ tryReadTChan $ _eventChannel input
case k of
(Just (EvResize _ _)) -> Just <$> mkResize
_ -> return k
return $ Vty { update = innerUpdate
, nextEvent = gkey
, nextEventNonblocking = gkey'
, inputIface = input
, outputIface = out
, refresh = innerRefresh
, shutdown = shutdownIo
, isShutdown = shutdownStatus
}