packages feed

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

{-# LANGUAGE CPP #-}

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

import Codec.Binary.UTF8.String (encodeChar)
import Control.Concurrent (yield)
import Control.Concurrent.STM (TChan, atomically, writeTChan)
import Control.Monad (foldM, when, void)
import Data.Bits (shiftL, (.&.), (.|.))
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as BS8
import Data.Word (Word8)
import Foreign.Ptr (castPtr)
import Foreign.Storable (Storable (..))
import GHC.Ptr (Ptr)
import Graphics.Vty.Input.Events (Event (EvResize), InternalEvent (InputEvent))
import System.IO (Handle, hPutBufNonBlocking)
import System.Win32.Console
  ( COORD (..),
    eNABLE_EXTENDED_FLAGS,
    eNABLE_PROCESSED_OUTPUT,
    eNABLE_VIRTUAL_TERMINAL_INPUT,
    eNABLE_VIRTUAL_TERMINAL_PROCESSING,
    getConsoleMode,
    setConsoleMode,
    setConsoleOutputCP,
    KEY_EVENT_RECORD(..),
    WINDOW_BUFFER_SIZE_RECORD(..),
    INPUT_RECORD(..),
    readConsoleInput
  )
import System.Win32.Event (waitForSingleObject)
import System.Win32.File
  ( createFile,
    fILE_SHARE_WRITE,
    gENERIC_READ,
    gENERIC_WRITE,
    oPEN_EXISTING,
  )
import System.Win32.Types (HANDLE, iNVALID_HANDLE_VALUE, withHandleToHANDLE)

-- | Write a 'ByteString' to a Handle
writeBuf :: Handle -> ByteString -> IO ()
writeBuf handle s =
  BS8.useAsCStringLen s $ \(buf, len) -> do
    void $ hPutBufNonBlocking handle (castPtr buf) (fromIntegral len)

-- | 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 INPUT_RECORD -> Handle -> Ptr Word8 -> Int -> IO Int
readBuf eventChannel inputEventPtr handle bufferPtr maxInputRecords = do
  ret <- withHandleToHANDLE handle (`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 INPUT_RECORD -> Handle -> Ptr Word8 -> Int -> IO Int
readBuf' eventChannel inputRecordPtr handle bufferPtr maxInputRecords = do
  withHandleToHANDLE handle $ \handle' -> do
    numEvents <- readConsoleInput handle' maxInputRecords inputRecordPtr
    (numBytes, _) <- foldM handleInputEvent (0, Nothing) [0 .. (fromIntegral numEvents - 1)]
    return numBytes
    where
      handleInputEvent :: (Int, Maybe Int) -> Int -> IO (Int, Maybe Int)
      handleInputEvent (offset, mSurrogateVal) idx = do
        inputRecord <- peekElemOff inputRecordPtr idx
        case inputRecord of
          KeyEvent (KEY_EVENT_RECORD 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)
          WindowBufferSizeEvent (WINDOW_BUFFER_SIZE_RECORD (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