vty-windows 0.2.0.3 → 0.2.0.4
raw patch · 5 files changed
+240/−378 lines, 5 filesdep ~Win32
Dependency ranges changed: Win32
Files
- CHANGELOG.md +5/−1
- src/Graphics/Vty/Platform/Windows/Input/Loop.hs +128/−118
- src/Graphics/Vty/Platform/Windows/WindowsConsoleInput.hsc +0/−185
- src/Graphics/Vty/Platform/Windows/WindowsInterfaces.hs +105/−70
- vty-windows.cabal +2/−4
CHANGELOG.md view
@@ -38,4 +38,8 @@ 0.2.0.3 ------- * Now vty-windows supports running in MSYS/MSYS2 terminals. This was achieved by modifying - the way the input and output buffers are initialized.+ the way the input and output buffers are initialized. + +0.2.0.4 +------- +* Use readConsoleInput API provided by Win32 library.
src/Graphics/Vty/Platform/Windows/Input/Loop.hs view
@@ -1,70 +1,69 @@ {-# LANGUAGE CPP #-} -{-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE NoMonomorphismRestriction #-} {-# OPTIONS_HADDOCK hide #-} -- | The input layer forks a thread to read input data via the -- Windows console API. Key presses, mouse events, and window -- resize events are all obtained by calling ReadConsoleInputW. module Graphics.Vty.Platform.Windows.Input.Loop - ( initInput + ( initInput, ) where -import Graphics.Vty.Input - -import Graphics.Vty.Config (VtyUserConfig(..)) - -import Graphics.Vty.Platform.Windows.Input.Classify ( classify ) -import Graphics.Vty.Platform.Windows.Input.Classify.Types -import Graphics.Vty.Platform.Windows.WindowsConsoleInput ( WinConsoleInputEvent ) -import Graphics.Vty.Platform.Windows.WindowsInterfaces (readBuf) - -import Control.Applicative ( Alternative(many) ) +import Control.Applicative (Alternative (many)) import Control.Concurrent - ( ThreadId, forkOS, killThread, newEmptyMVar, putMVar, takeMVar ) -import Control.Concurrent.STM ( atomically, writeTChan, newTChan ) -import Control.Exception (mask, try, catch, SomeException) -import Lens.Micro ( over, ASetter, ASetter' ) -import Lens.Micro.Mtl ( (.=), use ) -import Control.Monad (unless, mzero, forM_) + ( ThreadId, + forkOS, + killThread, + newEmptyMVar, + putMVar, + takeMVar, + ) +import Control.Concurrent.STM (atomically, newTChan, writeTChan) +import Control.Exception (SomeException, catch, mask, try) +import Control.Monad (forM_, mzero, unless) import Control.Monad.IO.Class (liftIO) -import Control.Monad.Trans (lift) -import Control.Monad.Trans.State (StateT(..), evalStateT) import Control.Monad.State.Class (MonadState, modify) -import Control.Monad.Trans.Reader (ReaderT(..), asks, ask) - -import Lens.Micro.TH ( makeLenses ) - -import qualified Data.ByteString.Char8 as BS8 +import Control.Monad.Trans (lift) +import Control.Monad.Trans.Reader (ReaderT (..), ask, asks) +import Control.Monad.Trans.State (StateT (..), evalStateT) import qualified Data.ByteString as BS import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as BS8 import Data.Word (Word8) - import Foreign (allocaArray) import Foreign.Ptr (Ptr, castPtr) - +import Graphics.Vty.Config (VtyUserConfig (..)) +import Graphics.Vty.Input +import Graphics.Vty.Platform.Windows.Input.Classify (classify) +import Graphics.Vty.Platform.Windows.Input.Classify.Types +import Graphics.Vty.Platform.Windows.WindowsInterfaces (readBuf) +import Lens.Micro (ASetter, ASetter', over) +import Lens.Micro.Mtl (use, (.=)) +import Lens.Micro.TH (makeLenses) import System.Environment (getEnv) -import System.IO ( Handle ) +import System.IO (Handle) +import System.Win32.Console (INPUT_RECORD) data InputBuffer = InputBuffer - { _ptr :: Ptr Word8 - , _inputRecordPtr :: Ptr WinConsoleInputEvent - , _consoleEventBufferSize :: Int - } + { _ptr :: Ptr Word8, + _inputRecordPtr :: Ptr INPUT_RECORD, + _consoleEventBufferSize :: Int + } makeLenses ''InputBuffer data InputState = InputState - { _unprocessedBytes :: ByteString - , _classifierState :: ClassifierState - , _inputHandle :: Handle - , _originalInput :: Input - , _inputBuffer :: InputBuffer - , _classifier :: ClassifierState -> ByteString -> KClass - } + { _unprocessedBytes :: ByteString, + _classifierState :: ClassifierState, + _inputHandle :: Handle, + _originalInput :: Input, + _inputBuffer :: InputBuffer, + _classifier :: ClassifierState -> ByteString -> KClass + } makeLenses ''InputState @@ -72,117 +71,128 @@ logMsg :: String -> InputM () logMsg msg = do - i <- use originalInput - liftIO $ inputLogMsg i msg + i <- use originalInput + liftIO $ inputLogMsg i msg -- this must be run on an OS thread dedicated to this input handling. -- otherwise the terminal timing read behavior will block the execution -- of the lightweight threads. loopInputProcessor :: InputM () loopInputProcessor = do - readFromDevice >>= addBytesToProcess - validEvents <- many parseEvent - forM_ validEvents emit - dropInvalid - loopInputProcessor + readFromDevice >>= addBytesToProcess + validEvents <- many parseEvent + forM_ validEvents emit + dropInvalid + loopInputProcessor addBytesToProcess :: ByteString -> InputM () addBytesToProcess block = unprocessedBytes <>= block emit :: Event -> InputM () emit event = do - logMsg $ "parsed event: " ++ show event - lift (asks eventChannel) >>= liftIO . atomically . flip writeTChan (InputEvent event) + logMsg $ "parsed event: " ++ show event + lift (asks eventChannel) >>= liftIO . atomically . flip writeTChan (InputEvent event) -- Precondition: Under the threaded runtime. Only current use is from a -- forkOS thread. That case satisfies precondition. readFromDevice :: InputM ByteString readFromDevice = do - handle <- use inputHandle + handle <- use inputHandle - bufferPtr <- use $ inputBuffer.ptr - winRecordPtr <- use $ inputBuffer.inputRecordPtr - maxInputRecords <- use $ inputBuffer.consoleEventBufferSize + bufferPtr <- use $ inputBuffer . ptr + winRecordPtr <- use $ inputBuffer . inputRecordPtr + maxInputRecords <- use $ inputBuffer . consoleEventBufferSize - input <- lift ask - stringRep <- liftIO $ do - bytesRead <- readBuf (eventChannel input) winRecordPtr handle bufferPtr maxInputRecords - if bytesRead > 0 - then BS.packCStringLen (castPtr bufferPtr, fromIntegral bytesRead) - else return BS.empty - unless (BS.null stringRep) $ logMsg $ "input bytes: " ++ show (BS8.unpack stringRep) - return stringRep + input <- lift ask + stringRep <- liftIO $ do + bytesRead <- readBuf (eventChannel input) winRecordPtr handle bufferPtr maxInputRecords + if bytesRead > 0 + then BS.packCStringLen (castPtr bufferPtr, fromIntegral bytesRead) + else return BS.empty + unless (BS.null stringRep) $ logMsg $ "input bytes: " ++ show (BS8.unpack stringRep) + return stringRep parseEvent :: InputM Event parseEvent = do - c <- use classifier - s <- use classifierState - b <- use unprocessedBytes - case c s b of - Valid e remaining -> do - logMsg $ "valid parse: " ++ show e - logMsg $ "remaining: " ++ show remaining - classifierState .= ClassifierStart - unprocessedBytes .= remaining - return e - _ -> mzero + c <- use classifier + s <- use classifierState + b <- use unprocessedBytes + case c s b of + Valid e remaining -> do + logMsg $ "valid parse: " ++ show e + logMsg $ "remaining: " ++ show remaining + classifierState .= ClassifierStart + unprocessedBytes .= remaining + return e + _ -> mzero dropInvalid :: InputM () dropInvalid = do - c <- use classifier - s <- use classifierState - b <- use unprocessedBytes - case c s b of - Chunk -> do - classifierState .= - case s of - ClassifierStart -> ClassifierInChunk b [] - ClassifierInChunk p bs -> ClassifierInChunk p (b:bs) - unprocessedBytes .= BS8.empty - Invalid -> do - logMsg "dropping input bytes" - classifierState .= ClassifierStart - unprocessedBytes .= BS8.empty - _ -> return () + c <- use classifier + s <- use classifierState + b <- use unprocessedBytes + case c s b of + Chunk -> do + classifierState + .= case s of + ClassifierStart -> ClassifierInChunk b [] + ClassifierInChunk p bs -> ClassifierInChunk p (b : bs) + unprocessedBytes .= BS8.empty + Invalid -> do + logMsg "dropping input bytes" + classifierState .= ClassifierStart + unprocessedBytes .= BS8.empty + _ -> return () runInputProcessorLoop :: ClassifyMap -> Input -> Handle -> IO () runInputProcessorLoop classifyTable input handle = do let bufferSize = 1024 -- A key event could require 4 bytes of UTF-8. let maxKeyEvents = bufferSize `div` 4 - allocaArray maxKeyEvents $ \(inputRecordBuf :: Ptr WinConsoleInputEvent) -> do - allocaArray bufferSize $ \(bufferPtr :: Ptr Word8) -> do - let s0 = InputState BS8.empty ClassifierStart - handle - input - (InputBuffer bufferPtr inputRecordBuf maxKeyEvents) - (classify classifyTable) - runReaderT (evalStateT loopInputProcessor s0) input + allocaArray maxKeyEvents $ \(inputRecordBuf :: Ptr INPUT_RECORD) -> do + allocaArray bufferSize $ \(bufferPtr :: Ptr Word8) -> do + let s0 = + InputState + BS8.empty + ClassifierStart + handle + input + (InputBuffer bufferPtr inputRecordBuf maxKeyEvents) + (classify classifyTable) + runReaderT (evalStateT loopInputProcessor s0) input initInput :: VtyUserConfig -> Handle -> ClassifyMap -> IO Input initInput userConfig handle classifyTable = do - stopSync <- newEmptyMVar - mDefaultLog <- catch - (do debugLog <- getEnv "VTY_DEBUG_LOG" - return $ Just debugLog) - (\(_ :: IOError) -> return Nothing) - input <- Input <$> atomically newTChan - <*> pure (return ()) - <*> pure (return ()) - <*> maybe (return $ append mDefaultLog) - (return . appendFile) - (configDebugLog userConfig) - inputThread <- forkOSFinally (runInputProcessorLoop classifyTable input handle) - (\_ -> putMVar stopSync ()) - let killAndWait = do - killThread inputThread - takeMVar stopSync - return $ input { shutdownInput = killAndWait } - where - append mDebugLog msg = - case mDebugLog of - Just debugLog -> appendFile debugLog $ msg ++ "\n" - Nothing -> return () + stopSync <- newEmptyMVar + mDefaultLog <- + catch + ( do + debugLog <- getEnv "VTY_DEBUG_LOG" + return $ Just debugLog + ) + (\(_ :: IOError) -> return Nothing) + input <- + Input + <$> atomically newTChan + <*> pure (return ()) + <*> pure (return ()) + <*> maybe + (return $ append mDefaultLog) + (return . appendFile) + (configDebugLog userConfig) + inputThread <- + forkOSFinally + (runInputProcessorLoop classifyTable input handle) + (\_ -> putMVar stopSync ()) + let killAndWait = do + killThread inputThread + takeMVar stopSync + return $ input {shutdownInput = killAndWait} + where + append mDebugLog msg = + case mDebugLog of + Just debugLog -> appendFile debugLog $ msg ++ "\n" + Nothing -> return () forkOSFinally :: IO a -> (Either SomeException a -> IO ()) -> IO ThreadId forkOSFinally action and_then = @@ -191,5 +201,5 @@ (<>=) :: (MonadState s m, Monoid a) => ASetter' s a -> a -> m () l <>= a = modify (l <>~ a) -(<>~) :: Monoid a => ASetter s t a a -> a -> s -> t -l <>~ n = over l (`mappend` n) +(<>~) :: (Monoid a) => ASetter s t a a -> a -> s -> t +l <>~ n = over l (`mappend` n)
− src/Graphics/Vty/Platform/Windows/WindowsConsoleInput.hsc
@@ -1,185 +0,0 @@-{-# LANGUAGE ForeignFunctionInterface, CPP #-} - --- | This module provides a function to obtain input events from the Win32 API -module Graphics.Vty.Platform.Windows.WindowsConsoleInput - (KeyEventRecord(..), - MouseEventRecord(..), - WindowBufferSizeRecord(..), - MenuEventRecord(..), - FocusEventRecord(..), - WinConsoleInputEvent(..), - readConsoleInput) where - -import Control.Monad (foldM) -import Foreign.C.Types ( CWchar(..) ) -import Foreign.Marshal.Alloc (alloca) -import Foreign.Storable (Storable(..)) -import GHC.Ptr ( Ptr ) -import System.Win32.Console -import System.Win32.Types -import System.IO (Handle) - -#include <windows.h> - -foreign import ccall unsafe "windows.h ReadConsoleInputW" cReadConsoleInput :: HANDLE -> Ptr WinConsoleInputEvent -> DWORD -> LPDWORD -> IO BOOL - --- | This type represents a keyboard input event. The structure is documented here: --- https://learn.microsoft.com/en-us/windows/console/key-event-record-str -data KeyEventRecord = KeyEventRecordC - { keyDown :: BOOL - , repeatCount :: WORD - , virtualKeyCode :: WORD - , virtualScanCode :: WORD - , uChar :: CWchar - , controlKeyStateK :: DWORD - } deriving (Eq, Show) - --- | This type represents a mouse event. The structure is documented here: --- https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str -data MouseEventRecord = MouseEventRecordC - { mousePosition :: COORD - , buttonState :: DWORD - , controlKeyStateM :: DWORD - , eventFlags :: DWORD - } deriving (Eq, Show) - --- | This type represents a window size change event. The structure is documented here: --- https://learn.microsoft.com/en-us/windows/console/window-buffer-size-record-str -newtype WindowBufferSizeRecord = WindowBufferSizeRecordC - { windowSize :: COORD - } deriving (Eq, Show) - --- | This type represents a window menu event. (Current ignored by VTY). The structure --- is documented here: https://learn.microsoft.com/en-us/windows/console/menu-event-record-str -newtype MenuEventRecord = MenuEventRecordC - { commandId :: UINT - } deriving (Eq, Show) - --- | This type represents a window focus change event. The structure is documented here: --- https://learn.microsoft.com/en-us/windows/console/focus-event-record-str -newtype FocusEventRecord = FocusEventRecordC - { setFocus :: BOOL - } deriving (Eq, Show) - --- | Description of a Windows console input event. Documented here: --- https://learn.microsoft.com/en-us/windows/console/input-record-str -data WinConsoleInputEvent = - KeyEventRecordU KeyEventRecord - | MouseEventRecordU MouseEventRecord - | WindowBufferSizeRecordU WindowBufferSizeRecord - | MenuEventRecordU MenuEventRecord - | FocusEventRecordU FocusEventRecord - deriving (Eq, Show) - --- | A wrapper for the ReadConsoleInput Win32 API as documented here: --- https://learn.microsoft.com/en-us/windows/console/readconsoleinput -readConsoleInput :: Ptr WinConsoleInputEvent -> Int -> Handle -> IO [WinConsoleInputEvent] -readConsoleInput inputRecordPtr maxEvents handle = withHandleToHANDLE handle (readConsoleInput' inputRecordPtr maxEvents) - -readConsoleInput' :: Ptr WinConsoleInputEvent -> Int -> HANDLE -> IO [WinConsoleInputEvent] -readConsoleInput' inputRecordPtr maxEvents handle' = - alloca $ \numEventsReadPtr -> do - poke numEventsReadPtr 1 - _ <- cReadConsoleInput handle' inputRecordPtr (fromIntegral maxEvents) numEventsReadPtr - numEvents <- peek numEventsReadPtr - foldM addNextRecord [] [(fromIntegral numEvents - 1), (fromIntegral numEvents - 2)..0] - where - addNextRecord inputRecords idx = do - inputRecord <- peekElemOff inputRecordPtr idx - return (inputRecord:inputRecords) - -instance Storable KeyEventRecord where - sizeOf = const #{size KEY_EVENT_RECORD} - alignment _ = #alignment KEY_EVENT_RECORD - poke buf input = do - (#poke KEY_EVENT_RECORD, bKeyDown) buf (keyDown input) - (#poke KEY_EVENT_RECORD, wRepeatCount) buf (repeatCount input) - (#poke KEY_EVENT_RECORD, wVirtualKeyCode) buf (virtualKeyCode input) - (#poke KEY_EVENT_RECORD, wVirtualScanCode) buf (virtualScanCode input) - (#poke KEY_EVENT_RECORD, uChar) buf (uChar input) - (#poke KEY_EVENT_RECORD, dwControlKeyState) buf (controlKeyStateK input) - peek buf = do - keyDown' <- (#peek KEY_EVENT_RECORD, bKeyDown) buf - repeatCount' <- (#peek KEY_EVENT_RECORD, wRepeatCount) buf - virtualKeyCode' <- (#peek KEY_EVENT_RECORD, wVirtualKeyCode) buf - virtualScanCode' <- (#peek KEY_EVENT_RECORD, wVirtualScanCode) buf - uChar' <- (#peek KEY_EVENT_RECORD, uChar) buf - controlKeyStateK' <- (#peek KEY_EVENT_RECORD, dwControlKeyState) buf - return $ KeyEventRecordC keyDown' repeatCount' virtualKeyCode' virtualScanCode' uChar' controlKeyStateK' - -instance Storable MouseEventRecord where - sizeOf = const #{size MOUSE_EVENT_RECORD} - alignment _ = #alignment MOUSE_EVENT_RECORD - poke buf input = do - (#poke MOUSE_EVENT_RECORD, dwMousePosition) buf (mousePosition input) - (#poke MOUSE_EVENT_RECORD, dwButtonState) buf (buttonState input) - (#poke MOUSE_EVENT_RECORD, dwControlKeyState) buf (controlKeyStateM input) - (#poke MOUSE_EVENT_RECORD, dwEventFlags) buf (eventFlags input) - peek buf = do - mousePosition' <- (#peek MOUSE_EVENT_RECORD, dwMousePosition) buf - buttonState' <- (#peek MOUSE_EVENT_RECORD, dwButtonState) buf - controlKeyStateM' <- (#peek MOUSE_EVENT_RECORD, dwControlKeyState) buf - eventFlags' <- (#peek MOUSE_EVENT_RECORD, dwEventFlags) buf - return $ MouseEventRecordC mousePosition' buttonState' controlKeyStateM' eventFlags' - -instance Storable WindowBufferSizeRecord where - sizeOf = const #{size WINDOW_BUFFER_SIZE_RECORD} - alignment _ = #alignment WINDOW_BUFFER_SIZE_RECORD - poke buf input = do - (#poke WINDOW_BUFFER_SIZE_RECORD, dwSize) buf (windowSize input) - peek buf = do - size' <- (#peek WINDOW_BUFFER_SIZE_RECORD, dwSize) buf - return $ WindowBufferSizeRecordC size' - -instance Storable MenuEventRecord where - sizeOf = const #{size MENU_EVENT_RECORD} - alignment _ = #alignment MENU_EVENT_RECORD - poke buf input = do - (#poke MENU_EVENT_RECORD, dwCommandId) buf (commandId input) - peek buf = do - commandId' <- (#peek MENU_EVENT_RECORD, dwCommandId) buf - return $ MenuEventRecordC commandId' - -instance Storable FocusEventRecord where - sizeOf = const #{size FOCUS_EVENT_RECORD} - alignment _ = #alignment FOCUS_EVENT_RECORD - poke buf input = do - (#poke FOCUS_EVENT_RECORD, bSetFocus) buf (setFocus input) - peek buf = do - setFocus' <- (#peek FOCUS_EVENT_RECORD, bSetFocus) buf - return $ FocusEventRecordC setFocus' - -instance Storable WinConsoleInputEvent where - sizeOf = const #{size INPUT_RECORD} - alignment _ = #alignment INPUT_RECORD - - poke buf (KeyEventRecordU key) = do - (#poke INPUT_RECORD, EventType) buf (#{const KEY_EVENT} :: WORD) - (#poke INPUT_RECORD, Event) buf key - poke buf (MouseEventRecordU mouse) = do - (#poke INPUT_RECORD, EventType) buf (#{const MOUSE_EVENT} :: WORD) - (#poke INPUT_RECORD, Event) buf mouse - poke buf (WindowBufferSizeRecordU window) = do - (#poke INPUT_RECORD, EventType) buf (#{const WINDOW_BUFFER_SIZE_EVENT} :: WORD) - (#poke INPUT_RECORD, Event) buf window - poke buf (MenuEventRecordU menu) = do - (#poke INPUT_RECORD, EventType) buf (#{const MENU_EVENT} :: WORD) - (#poke INPUT_RECORD, Event) buf menu - poke buf (FocusEventRecordU focus) = do - (#poke INPUT_RECORD, EventType) buf (#{const FOCUS_EVENT} :: WORD) - (#poke INPUT_RECORD, Event) buf focus - - peek buf = do - event <- (#peek INPUT_RECORD, EventType) buf :: IO WORD - case event of - #{const KEY_EVENT} -> - KeyEventRecordU `fmap` (#peek INPUT_RECORD, Event) buf - #{const MOUSE_EVENT} -> - MouseEventRecordU `fmap` (#peek INPUT_RECORD, Event) buf - #{const WINDOW_BUFFER_SIZE_EVENT} -> - WindowBufferSizeRecordU `fmap` (#peek INPUT_RECORD, Event) buf - #{const MENU_EVENT} -> - MenuEventRecordU `fmap` (#peek INPUT_RECORD, Event) buf - #{const FOCUS_EVENT} -> - FocusEventRecordU `fmap` (#peek INPUT_RECORD, Event) buf - _ -> error $ "Unknown input event type " ++ show event
src/Graphics/Vty/Platform/Windows/WindowsInterfaces.hs view
@@ -1,103 +1,138 @@-{-# LANGUAGE ForeignFunctionInterface, CPP #-} +{-# 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 - ( readBuf, + ( writeBuf, + readBuf, configureInput, - configureOutput - ) where - -#include "windows_cconv.h" - -import Graphics.Vty.Platform.Windows.WindowsConsoleInput -import Graphics.Vty.Input.Events (Event(EvResize), InternalEvent(InputEvent)) + 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) -import Data.Bits ((.|.), (.&.), shiftL) +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.Storable (Storable(..)) +import Foreign.Ptr (castPtr) +import Foreign.Storable (Storable (..)) import GHC.Ptr (Ptr) -import System.IO (Handle) -import System.Win32.Types (DWORD, HANDLE, withHandleToHANDLE, iNVALID_HANDLE_VALUE) +import Graphics.Vty.Input.Events (Event (EvResize), InternalEvent (InputEvent)) +import System.IO (Handle, hPutBufNonBlocking) import System.Win32.Console -import System.Win32.File hiding (copyFile) + ( 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) -foreign import ccall "windows.h WaitForSingleObject" c_WaitForSingleObject :: HANDLE -> DWORD -> IO DWORD +-- | 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 +-- 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 :: TChan InternalEvent -> Ptr INPUT_RECORD -> 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 + 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 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 +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) -> 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) + 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' + 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) + 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) + 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) + 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+ 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
vty-windows.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: vty-windows -version: 0.2.0.3 +version: 0.2.0.4 license: BSD-3-Clause license-file: LICENSE author: Chris hackett @@ -11,7 +11,6 @@ build-type: Simple extra-doc-files: CHANGELOG.md extra-source-files: cbits/win_pseudo_console.c --- documentation: True source-repository head type: git @@ -42,7 +41,6 @@ Graphics.Vty.Platform.Windows.Output.Color Graphics.Vty.Platform.Windows.Settings Graphics.Vty.Platform.Windows.WindowsCapabilities - Graphics.Vty.Platform.Windows.WindowsConsoleInput Graphics.Vty.Platform.Windows.WindowsInterfaces build-depends: base >= 4.9 && < 5, blaze-builder, @@ -61,6 +59,6 @@ utf8-string, vector, vty >= 6.1, - Win32 >= 2.8.5.0, + Win32 >= 2.14.2 && < 2.14.3, hs-source-dirs: src default-language: Haskell2010