hs-sdl-term-emulator-0.1.0.4: src/System/Terminal/Emulator/SDL/LibMain.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module System.Terminal.Emulator.SDL.LibMain where
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception (bracket)
import Control.Lens
import Control.Monad (forM_, unless, when)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Conduit (runConduit, (.|))
import Data.Conduit.Attoparsec (conduitParser)
import Data.Conduit.Combinators (decodeUtf8Lenient)
import qualified Data.Conduit.Combinators as C
import Data.Conduit.TQueue (sinkTBQueue, sourceTBQueue)
import qualified Data.Vector.Unboxed as VU
import Data.Word (Word8)
import Foreign.C.Types (CInt)
import Linear (V2 (..), V3 (..), V4 (..))
import Linear.Affine (Point (..))
import qualified Linear.V4 as V4
import SDL (get, ($=))
import qualified SDL as SDL
import qualified System.Console.ANSI.Types as SGR
import System.Terminal.Emulator.Attrs (Cell, attrsBg, attrsFg, attrsIntensity, attrsUnderline)
import System.Terminal.Emulator.KeyboardInput.KeyPressToPty (keyPressToPty)
import System.Terminal.Emulator.Parsing (parseTermAtom)
import System.Terminal.Emulator.Parsing.Types (TermAtom)
import System.Terminal.Emulator.SDL.ImageFont (loadImageFont)
import System.Terminal.Emulator.SDL.KeyboardTranslate (translateSDLKey)
import System.Terminal.Emulator.SDL.Pty (launchPty)
import System.Terminal.Emulator.Term (activeScreen, altScreenActive, cursorPos, mkTerm, numRows, scrollBackLines, termGetKeyboardState, windowTitle)
import System.Terminal.Emulator.Term.Process (Term, TermLine, processTermAtoms)
import System.Terminal.Emulator.Term.Resize (resizeTerm)
import qualified System.Terminal.Emulator.TermLines as TL
import Prelude hiding (lines)
initialTerminalWidth, initialTerminalHeight :: CInt
initialTerminalWidth = 80
initialTerminalHeight = 30
initialWindowSize :: V2 CInt
initialWindowSize = V2 (initialTerminalWidth * cellWidth) (initialTerminalHeight * cellHeight)
main :: IO ()
main = do
SDL.initializeAll
bracket
( SDL.createWindow
"My SDL Application"
SDL.defaultWindow
{ SDL.windowBorder = True,
SDL.windowInitialSize = initialWindowSize,
SDL.windowResizable = True
}
)
SDL.destroyWindow
( \window -> do
renderer <- SDL.createRenderer window (-1) SDL.defaultRenderer
-- We need to resize the window again, because the window manager I
-- use (xmonad) creates it initially a bit too small
SDL.windowSize window $= initialWindowSize
fontSurface <- loadImageFont
fontTexture <- SDL.createTextureFromSurface renderer fontSurface
SDL.freeSurface fontSurface
terminalLoop window renderer fontTexture
)
terminalLoop :: SDL.Window -> SDL.Renderer -> SDL.Texture -> IO ()
terminalLoop window renderer fontTexture = do
V2 windowWidth windowHeight <- get $ SDL.windowSize window
let terminalInitialWidth = fromIntegral (windowWidth `div` cellWidth)
terminalInitialHeight = fromIntegral (windowHeight `div` cellHeight)
termOutputQueue <- newTBQueueIO 500
let initialTerm = mkTerm (terminalInitialWidth, terminalInitialHeight)
windowDirtyVar <- newTVarIO True
scrollBackVar <- newTVarIO 0
termVar :: TVar Term <- newTVarIO initialTerm
termInputBuffer :: TChan ByteString <- newTChanIO
termResizeEvents :: TChan (Int, Int) <- newTChanIO
termOutputStream :: TBQueue TermAtom <- newTBQueueIO 500
let streamInput :: IO ()
streamInput =
runConduit $
(sourceTBQueue termOutputQueue)
.| decodeUtf8Lenient
.| C.iterM (\t -> putStrLn $ "[RAW] " <> show t)
.| conduitParser parseTermAtom -- termAtom
-- .| C.iterM (\t -> putStrLn $ "[PARSED] " <> show t)
.| C.map snd
.| sinkTBQueue termOutputStream
let doTerminalResize :: (Int, Int) -> IO ()
doTerminalResize newSize = do
atomically $ do
modifyTVar' termVar (flip resizeTerm newSize')
writeTVar windowDirtyVar True
atomically $ writeTChan termResizeEvents newSize'
where
(width, height) = newSize
newSize' = (max 1 width, max 1 height)
let readInput :: IO ()
readInput = do
event <- interruptibleWaitEvent
case SDL.eventPayload event of
SDL.KeyboardEvent SDL.KeyboardEventData {SDL.keyboardEventKeyMotion = SDL.Pressed, SDL.keyboardEventKeysym = keysym} -> do
-- putStrLn $ "{KEYSYM} " <> show keysym
case translateSDLKey keysym of
Nothing -> pure ()
Just termInput -> atomically $ do
term <- readTVar termVar
writeTChan termInputBuffer (keyPressToPty (termGetKeyboardState term) termInput)
pure ()
SDL.WindowExposedEvent _ -> atomically $ writeTVar windowDirtyVar True
SDL.MouseWheelEvent SDL.MouseWheelEventData {SDL.mouseWheelEventPos = V2 _ scrollY} -> do
atomically $ do
let scrollMultiplier = 6
modifyTVar' scrollBackVar (\scrollBack -> max 0 (scrollBack + (scrollMultiplier * (fromIntegral scrollY))))
writeTVar windowDirtyVar True
SDL.MouseMotionEvent
SDL.MouseMotionEventData
{ SDL.mouseMotionEventState = [SDL.ButtonRight],
SDL.mouseMotionEventPos = P (V2 mouseX mouseY)
} -> do
let newTermWidth = fromIntegral mouseX `div` fromIntegral cellWidth
newTermHeight = fromIntegral mouseY `div` fromIntegral cellHeight
doTerminalResize (newTermWidth, newTermHeight)
SDL.MouseButtonEvent
SDL.MouseButtonEventData
{ SDL.mouseButtonEventButton = SDL.ButtonRight,
SDL.mouseButtonEventMotion = SDL.Pressed,
SDL.mouseButtonEventPos = P (V2 mouseX mouseY),
SDL.mouseButtonEventClicks = numClicks
} -> do
putStrLn $ "mouseClick: " <> show numClicks <> show (mouseX, mouseY)
let newTermWidth = fromIntegral mouseX `div` fromIntegral cellWidth
newTermHeight = fromIntegral mouseY `div` fromIntegral cellHeight
doTerminalResize (newTermWidth, newTermHeight)
SDL.WindowSizeChangedEvent
SDL.WindowSizeChangedEventData
{ SDL.windowSizeChangedEventSize = V2 newWindowWidth newWindowHeight
} ->
do
putStrLn $ "RESIZE: " <> show (newWindowWidth, newWindowHeight)
let newTermWidth = fromIntegral newWindowWidth `div` fromIntegral cellWidth
newTermHeight = fromIntegral newWindowHeight `div` fromIntegral cellHeight
doTerminalResize (newTermWidth, newTermHeight)
_ -> pure ()
readInput
let renderLoop :: IO ()
renderLoop = do
mbOutputBuf <-
atomically $
(readTBQueueAll termOutputStream >>= pure . Just)
`orElse` ( do
windowDirty <- readTVar windowDirtyVar
if windowDirty
then do
writeTVar windowDirtyVar False
pure Nothing
else retry
)
-- putStrLn $ "{OUTPUT} " <> show outputBuf
!(termWrite, !term') <- atomically $ do
term <- readTVar termVar
case mbOutputBuf of
Just outputBuf -> do
let (termWrite, term') = processTermAtoms outputBuf term
writeTVar termVar $! term'
pure (termWrite, term')
Nothing -> pure (B.empty, term)
unless (B.null termWrite) $
atomically $ writeTChan termInputBuffer termWrite
scrollBack <- atomically $ readTVar scrollBackVar
renderTerm window renderer fontTexture term' scrollBack
renderLoop
withAsync streamInput $ \streamInputAsync -> do
link streamInputAsync
withAsync readInput $ \readInputAsync -> do
link readInputAsync
withAsync
( renderLoop
)
$ \renderLoopAsync -> do
link renderLoopAsync
launchPty (terminalInitialWidth, terminalInitialHeight) (readTChan termInputBuffer) (readTChan termResizeEvents) $ \contents -> do
-- putStrLn $ "[CONTENTS] " ++ show contents
atomically $ writeTBQueue termOutputQueue contents
-- | Reads all the values in the 'TBQueue', or retry if it is empty
readTBQueueAll :: TBQueue a -> STM [a]
readTBQueueAll c = do
first <- readTBQueue c
rest <- tryReadTBQueueMultiple []
pure (first : rest)
where
tryReadTBQueueMultiple accum = do
mbItem <- tryReadTBQueue c
case mbItem of
Nothing -> pure (reverse accum)
Just item -> tryReadTBQueueMultiple (item : accum)
renderTerm :: SDL.Window -> SDL.Renderer -> SDL.Texture -> Term -> Int -> IO ()
renderTerm window renderer fontTexture term scrollBack = do
SDL.windowTitle window $= term ^. windowTitle
let visibleRows
| term ^. altScreenActive = term ^. activeScreen
| otherwise = (TL.take (term ^. numRows) (TL.takeLast scrollBack (term ^. scrollBackLines))) <> (TL.dropLast scrollBack (term ^. activeScreen))
SDL.rendererDrawColor renderer $= V4 255 32 255 255
SDL.clear renderer
_ <- (flip TL.traverseWithIndex) visibleRows $ \line termLine -> do
renderLine renderer fontTexture termLine line
renderGrid window renderer
-- Render the cursor
let (line, col) = term ^. cursorPos
SDL.rendererDrawColor renderer $= V4 255 255 128 255
SDL.drawRect renderer (Just (SDL.Rectangle (P (V2 (fromIntegral col * cellWidth) (fromIntegral (line + scrollBack) * cellHeight))) charSize))
SDL.present renderer
renderGrid :: SDL.Window -> SDL.Renderer -> IO ()
renderGrid window renderer = do
V2 windowWidth windowHeight <- get $ SDL.windowSize window
SDL.rendererDrawColor renderer $= V4 8 8 8 255
-- Horizontal lines
forM_ [0, cellHeight .. windowHeight] $ \y -> do
SDL.drawLine renderer (P (V2 0 y)) (P (V2 windowWidth y))
-- Vertical lines
forM_ [0, cellWidth .. windowWidth] $ \x -> do
SDL.drawLine renderer (P (V2 x 0)) (P (V2 x windowHeight))
renderLine :: SDL.Renderer -> SDL.Texture -> TermLine -> Int -> IO ()
renderLine renderer fontTexture termLine line = do
VU.iforM_ termLine $ \col cell -> do
renderChar renderer fontTexture cell line col
pure ()
renderChar :: SDL.Renderer -> SDL.Texture -> Cell -> Int -> Int -> IO ()
renderChar renderer fontTexture (char, attrs) line col = do
let fgColor :: (V3 Word8)
fgColor = case attrs ^. attrsFg of
Nothing -> V3 172 216 172
Just (SGR.Dull, SGR.Black) -> V3 0 0 0
Just (SGR.Dull, SGR.Red) -> V3 208 0 0
Just (SGR.Dull, SGR.Green) -> V3 0 208 0
Just (SGR.Dull, SGR.Yellow) -> V3 208 208 0
Just (SGR.Dull, SGR.Blue) -> V3 0 0 208
Just (SGR.Dull, SGR.Magenta) -> V3 208 0 208
Just (SGR.Dull, SGR.Cyan) -> V3 0 208 208
Just (SGR.Dull, SGR.White) -> V3 208 208 208
Just (SGR.Vivid, SGR.Black) -> V3 128 128 128
Just (SGR.Vivid, SGR.Red) -> V3 128 128 128
Just (SGR.Vivid, SGR.Green) -> V3 128 128 128
Just (SGR.Vivid, SGR.Yellow) -> V3 128 128 128
Just (SGR.Vivid, SGR.Blue) -> V3 128 128 128
Just (SGR.Vivid, SGR.Magenta) -> V3 128 128 128
Just (SGR.Vivid, SGR.Cyan) -> V3 128 128 128
Just (SGR.Vivid, SGR.White) -> V3 128 128 128
bgColor :: (V4 Word8)
bgColor = case attrs ^. attrsBg of
Nothing -> V4 0 0 0 255
Just (SGR.Dull, SGR.Black) -> V4 0 0 0 255
Just (SGR.Dull, SGR.Red) -> V4 208 0 0 255
Just (SGR.Dull, SGR.Green) -> V4 0 208 0 255
Just (SGR.Dull, SGR.Yellow) -> V4 208 208 0 255
Just (SGR.Dull, SGR.Blue) -> V4 0 0 208 255
Just (SGR.Dull, SGR.Magenta) -> V4 208 0 208 255
Just (SGR.Dull, SGR.Cyan) -> V4 0 208 208 255
Just (SGR.Dull, SGR.White) -> V4 208 208 208 255
Just (SGR.Vivid, SGR.Black) -> V4 255 128 128 255
Just (SGR.Vivid, SGR.Red) -> V4 255 128 128 255
Just (SGR.Vivid, SGR.Green) -> V4 255 128 128 255
Just (SGR.Vivid, SGR.Yellow) -> V4 255 128 128 255
Just (SGR.Vivid, SGR.Blue) -> V4 255 128 128 255
Just (SGR.Vivid, SGR.Magenta) -> V4 255 128 128 255
Just (SGR.Vivid, SGR.Cyan) -> V4 255 128 128 255
Just (SGR.Vivid, SGR.White) -> V4 255 128 128 255
SDL.rendererDrawColor renderer $= bgColor
SDL.fillRect renderer (Just (SDL.Rectangle (P (V2 (fromIntegral col * cellWidth) (fromIntegral line * cellHeight))) charSize))
SDL.textureColorMod fontTexture $= fgColor
SDL.copy
renderer
fontTexture
(Just (SDL.Rectangle (charPosition char) charSize))
(Just (SDL.Rectangle (P (V2 (fromIntegral col * cellWidth) (fromIntegral line * cellHeight))) charSize))
when (attrs ^. attrsIntensity == SGR.BoldIntensity) $ do
SDL.copy
renderer
fontTexture
(Just (SDL.Rectangle (charPosition char) charSize))
(Just (SDL.Rectangle (P (V2 (fromIntegral col * cellWidth - 1) (fromIntegral line * cellHeight))) charSize))
when (attrs ^. attrsUnderline == SGR.SingleUnderline) $ do
SDL.rendererDrawColor renderer $= (V4._w .~ 255) (V4.vector fgColor)
SDL.drawLine
renderer
(P (V2 (fromIntegral col * cellWidth) (fromIntegral line * cellHeight + cellHeight - 2)))
(P (V2 (fromIntegral col * cellWidth + cellWidth) (fromIntegral line * cellHeight + cellHeight - 2)))
fontCellWidth, fontCellHeight :: CInt
(fontCellWidth, fontCellHeight) = (16, 16)
cellWidth, cellHeight :: CInt
(cellWidth, cellHeight) = (14, 16)
charPosition :: Char -> Point V2 CInt
charPosition char = case fromEnum char of
charCode
| charCode < 256 -> P (V2 ((fromIntegral charCode `rem` 16) * fontCellWidth) ((fromIntegral charCode `div` 16) * fontCellHeight))
| otherwise -> invalidCharP
where
invalidCharP = P (V2 (15 * fontCellWidth) (7 * fontCellHeight))
charSize :: V2 CInt
charSize = V2 fontCellWidth fontCellHeight
interruptibleWaitEvent :: IO SDL.Event
interruptibleWaitEvent = do
result <- SDL.waitEventTimeout 50
case result of
Just e -> pure e
Nothing -> interruptibleWaitEvent