packages feed

vty-windows-0.2.0.3: src/Graphics/Vty/Platform/Windows/WindowsInterfaces.hs

{-# LANGUAGE ForeignFunctionInterface, CPP #-}
-- | This module provides wrappers around Win32 API calls. These functions provide initialization,
-- shutdown, and input event handling.
module Graphics.Vty.Platform.Windows.WindowsInterfaces
  ( readBuf,
    configureInput,
    configureOutput
  ) where

#include "windows_cconv.h"

import Graphics.Vty.Platform.Windows.WindowsConsoleInput
import Graphics.Vty.Input.Events (Event(EvResize), InternalEvent(InputEvent))

import Codec.Binary.UTF8.String (encodeChar)
import Control.Concurrent (yield)
import Control.Concurrent.STM (TChan, atomically, writeTChan)
import Control.Monad (foldM, when)
import Data.Bits ((.|.), (.&.), shiftL)
import Data.Word (Word8)
import Foreign.Storable (Storable(..))
import GHC.Ptr (Ptr)
import System.IO (Handle)
import System.Win32.Types (DWORD, HANDLE, withHandleToHANDLE, iNVALID_HANDLE_VALUE) 
import System.Win32.Console
import System.Win32.File hiding (copyFile)

foreign import ccall "windows.h WaitForSingleObject" c_WaitForSingleObject :: HANDLE -> DWORD -> IO DWORD

-- | Read the contents of the Windows input buffer. The contents are parsed and either written to
-- the TChan queue for Window size events, or written to the Word8 buffer for keyboard events and 
-- VT sequences. Returns the # of bytes written to the Word8 buffer.
readBuf :: TChan InternalEvent -> Ptr WinConsoleInputEvent -> Handle -> Ptr Word8 -> Int -> IO Int
readBuf eventChannel inputEventPtr handle bufferPtr maxInputRecords = do
    ret <- withHandleToHANDLE handle (`c_WaitForSingleObject` 500)
    yield -- otherwise, the above foreign call causes the loop to never
          -- respond to the killThread
    if ret /= 0
    then readBuf eventChannel inputEventPtr handle bufferPtr maxInputRecords
    else readBuf' eventChannel inputEventPtr handle bufferPtr maxInputRecords

readBuf' :: TChan InternalEvent -> Ptr WinConsoleInputEvent -> Handle -> Ptr Word8 -> Int -> IO Int
readBuf' eventChannel inputEventPtr handle bufferPtr maxInputRecords = do
    inputEvents <- readConsoleInput inputEventPtr maxInputRecords handle
    (offset, _) <- foldM handleInputEvent (0, Nothing) inputEvents
    return offset
    where
        handleInputEvent :: (Int, Maybe Int) -> WinConsoleInputEvent -> IO (Int, Maybe Int)
        handleInputEvent (offset, mSurrogateVal) inputEvent = do
            case inputEvent of
                KeyEventRecordU (KeyEventRecordC isKeyDown _ _ _ cwChar _) -> do
                    -- Process the character if this is a 'key down' event,
                    -- AND the char is not NULL
                    if isKeyDown && cwChar /= 0
                    then processCWChar (offset, mSurrogateVal) $ fromEnum cwChar
                    else return (offset, Nothing)
                WindowBufferSizeRecordU (WindowBufferSizeRecordC (COORD x y)) -> do
                    let resize = EvResize (fromIntegral x) (fromIntegral y)
                    atomically $ writeTChan eventChannel (InputEvent resize)
                    return (offset, Nothing)
                _ -> return (offset, Nothing)

        processCWChar :: (Int, Maybe Int) -> Int -> IO (Int, Maybe Int)
        processCWChar (offset, Nothing) charVal = do
            if isSurrogate charVal
            then return (offset, Just charVal)
            else encodeAndWriteToBuf offset charVal
            where
                isSurrogate :: Int -> Bool
                isSurrogate c = 0xD800 <= c && c < 0xDC00
        processCWChar (offset, Just surogateVal) charVal = do
            let charVal' = (((surogateVal .&. 0x3FF) `shiftL` 10) .|. (charVal .&. 0x3FF)) + 0x10000
            encodeAndWriteToBuf offset charVal'

        encodeAndWriteToBuf :: Int -> Int -> IO (Int, Maybe Int)
        encodeAndWriteToBuf offset charVal = do
            let utf8Char = encodeChar $ toEnum charVal
            mapM_ (\(w, offset') -> pokeElemOff bufferPtr (offset + offset') w) $ zip utf8Char [0..]
            return (offset + length utf8Char, Nothing)

-- | Configure Windows to correctly handle input for a Vty application
configureInput :: Handle -> IO (IO (), IO ())
configureInput _ = do
    inHandle <- configureHandle "CONIN$"
    original <- getConsoleMode inHandle
    let setMode = setConsoleMode inHandle $ eNABLE_VIRTUAL_TERMINAL_INPUT .|. eNABLE_EXTENDED_FLAGS
    pure (setMode, setConsoleMode inHandle original)

-- | Configure Windows to correctly handle output for a Vty application
configureOutput :: Handle -> IO (IO ())
configureOutput _ = do
    outHandle <- configureHandle "CONOUT$"
    original <- getConsoleMode outHandle
    setConsoleOutputCP 65001
    setConsoleMode outHandle $ eNABLE_VIRTUAL_TERMINAL_PROCESSING .|. eNABLE_PROCESSED_OUTPUT
    pure (setConsoleMode outHandle original)

configureHandle :: String -> IO HANDLE
configureHandle handleName = do
    outHandle <- createFile handleName (gENERIC_WRITE .|. gENERIC_READ)
      fILE_SHARE_WRITE Nothing oPEN_EXISTING 0 Nothing
    when (outHandle == iNVALID_HANDLE_VALUE) $ fail $ "Unable to configure terminal for input/output with handle: " ++ handleName
    return outHandle