linenoise-0.7.0: src/Linenoise/FFI.hs
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE ForeignFunctionInterface #-}
-- | Just an FFI layer over the C library.
module Linenoise.FFI
( EditResult (..)
, EditSession
, InputResult (..)
, addHistory
, clearScreen
, feedEditSession
, freeEditSession
, getInputLine
, hideEditSession
, historyLoad
, historySave
, printKeycodes
, setCompletion
, setMultiline
, showEditSession
, startEditSession
, stopEditSession
, stifleHistory
, withEditSession
, withHiddenEditSession
)
where
import Control.Exception (bracket, bracket_)
import Control.Monad (unless)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Unsafe as BSU
import Data.Foldable (for_)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Foreign (FunPtr, Ptr, Storable (..), fromBool, nullPtr)
import Foreign.C.Error (eAGAIN, eNOENT, getErrno, resetErrno)
import Foreign.C.String (CString, newCString)
import Foreign.C.Types (CChar, CInt (..), CSize (..))
import Foreign.Marshal.Alloc (free, mallocBytes)
import Foreign.Marshal.Utils (copyBytes)
-- | Opaque C @struct linenoiseState@ pointer target.
data LinenoiseState
-- | Active non-blocking linenoise edit session.
--
-- The session owns the C edit state, input buffer, and copied prompt. Prefer
-- 'withEditSession' so raw terminal mode is stopped and memory is released.
data EditSession = EditSession
{ editSessionState :: !(Ptr LinenoiseState)
, editSessionBuffer :: !(Ptr CChar)
, editSessionPrompt :: !CString
, editSessionStopped :: !(IORef Bool)
}
foreign import ccall "linenoise.h linenoise"
linenoise :: CString -> IO CString
foreign import ccall "linenoise.h linenoiseFree"
linenoiseFree :: CString -> IO ()
foreign import ccall "linenoise.h linenoiseEditStart"
linenoiseEditStart :: Ptr LinenoiseState -> CInt -> CInt -> Ptr CChar -> CSize -> CString -> IO CInt
foreign import ccall "linenoise.h linenoiseEditFeed"
linenoiseEditFeed :: Ptr LinenoiseState -> IO CString
foreign import ccall "linenoise.h linenoiseEditStop"
linenoiseEditStop :: Ptr LinenoiseState -> IO ()
foreign import ccall "linenoise.h linenoiseHide"
linenoiseHide :: Ptr LinenoiseState -> IO ()
foreign import ccall "linenoise.h linenoiseShow"
linenoiseShow :: Ptr LinenoiseState -> IO ()
foreign import ccall "linenoise.h &linenoiseEditMore"
linenoiseEditMorePtr :: Ptr CString
foreign import ccall "linenoise.h linenoiseHistoryAdd"
linenoiseHistoryAdd :: Ptr CChar -> IO CInt
foreign import ccall "linenoise.h linenoiseHistorySetMaxLen"
linenoiseHistorySetMaxLen :: CInt -> IO CInt
foreign import ccall "linenoise.h linenoiseHistorySave"
linenoiseHistorySave :: CString -> IO ()
foreign import ccall "linenoise.h linenoiseHistoryLoad"
linenoiseHistoryLoad :: CString -> IO ()
foreign import ccall "linenoise.h linenoiseClearScreen"
linenoiseClearScreen :: IO ()
foreign import ccall "linenoise.h linenoiseSetMultiLine"
linenoiseSetMultiLine :: CInt -> IO ()
foreign import ccall "linenoise.h linenoisePrintKeyCodes"
linenoisePrintKeyCodes :: IO ()
foreign import ccall "linenoise.h linenoiseSetCompletionCallback"
linenoiseSetCompletionCallback :: FunPtr CompleteFunc -> IO ()
foreign import ccall "linenoise.h linenoiseAddCompletion"
linenoiseAddCompletion :: Completion -> CString -> IO ()
foreign import ccall "wrapper"
makeFunPtr :: CompleteFunc -> IO (FunPtr CompleteFunc)
data CompletionType = CompletionType CSize (Ptr (Ptr CChar))
deriving (Show, Eq)
type Completion = Ptr CompletionType
instance Storable CompletionType where
sizeOf _ = 8
alignment = sizeOf
peek ptr = do
a <- peekByteOff ptr 0
b <- peekByteOff ptr 4
pure (CompletionType a b)
poke = error "no poke"
-- Completion C callback
type CompleteFunc = (CString -> Completion -> IO ())
-- Make a completion function pointer.
makeCompletion :: (ByteString -> IO [ByteString]) -> (CString -> Completion -> IO ())
makeCompletion f buf lc = do
line <- BSU.unsafePackCString buf
comps <- f line
for_ comps (\c -> unless (BS.null c) (BSU.unsafeUseAsCString c (linenoiseAddCompletion lc)))
-- | Result of getInputLine.
data InputResult a
= -- | ctrl+c
InterruptResult
| -- | ctrl+d
EofResult
| LineResult !a -- Possibly empty line.
deriving (Eq, Show, Functor, Foldable, Traversable)
-- | Result of feeding one input event to a non-blocking edit session.
data EditResult a
= -- | Editing is still in progress; feed more input.
MoreResult
| -- | Editing finished with interrupt, EOF, or a completed line.
DoneResult !(InputResult a)
deriving (Eq, Show, Functor, Foldable, Traversable)
-- | Run the prompt, yielding a string.
getInputLine :: ByteString -> IO (InputResult ByteString)
getInputLine prompt = do
ptr <- BSU.unsafeUseAsCString prompt linenoise
if ptr == nullPtr
then do
errno <- getErrno
if errno == eAGAIN
then resetErrno >> pure InterruptResult
else pure EofResult
else do
line <- BS.packCString ptr
linenoiseFree ptr
pure (LineResult line)
-- | Start a non-blocking edit session with the given prompt.
--
-- The returned session must eventually be passed to 'stopEditSession' and
-- 'freeEditSession'. Prefer 'withEditSession' for scoped use.
startEditSession :: ByteString -> IO EditSession
startEditSession prompt = do
state <- mallocBytes linenoiseStateBytes
buffer <- mallocBytes editBufferBytes
prompt' <- copyByteString prompt
stopped <- newIORef False
result <- linenoiseEditStart state (-1) (-1) buffer (fromIntegral editBufferBytes) prompt'
if result == -1
then do
free state
free buffer
free prompt'
fail "linenoiseEditStart failed"
else
pure
EditSession
{ editSessionState = state
, editSessionBuffer = buffer
, editSessionPrompt = prompt'
, editSessionStopped = stopped
}
-- | Stop editing and restore normal terminal mode.
--
-- This operation is idempotent for a given 'EditSession'.
stopEditSession :: EditSession -> IO ()
stopEditSession EditSession {editSessionState = state, editSessionStopped = stopped} = do
alreadyStopped <- readIORef stopped
unless alreadyStopped $ do
linenoiseEditStop state
writeIORef stopped True
-- | Free memory owned by an edit session.
--
-- Call this only after 'stopEditSession'. Prefer 'withEditSession'.
freeEditSession :: EditSession -> IO ()
freeEditSession EditSession {editSessionState = state, editSessionBuffer = buffer, editSessionPrompt = prompt} = do
free state
free buffer
free prompt
-- | Run an action with a managed non-blocking edit session.
--
-- The session is stopped and freed when the action exits, including on
-- exceptions.
withEditSession :: ByteString -> (EditSession -> IO a) -> IO a
withEditSession prompt = bracket (startEditSession prompt) release
where
release session = stopEditSession session >> freeEditSession session
-- | Feed one input event to a non-blocking edit session.
--
-- Returns 'MoreResult' until the user completes a line, interrupts, or sends
-- EOF.
feedEditSession :: EditSession -> IO (EditResult ByteString)
feedEditSession EditSession {editSessionState = state} = do
ptr <- linenoiseEditFeed state
more <- peek linenoiseEditMorePtr
if ptr == more
then pure MoreResult
else
if ptr == nullPtr
then do
errno <- getErrno
if errno == eAGAIN
then resetErrno >> pure (DoneResult InterruptResult)
else
if errno == eNOENT
then resetErrno >> pure (DoneResult EofResult)
else pure (DoneResult EofResult)
else do
line <- BS.packCString ptr
linenoiseFree ptr
pure (DoneResult (LineResult line))
-- | Hide the current prompt and edit buffer.
--
-- Use this before writing asynchronous output to the terminal.
hideEditSession :: EditSession -> IO ()
hideEditSession = linenoiseHide . editSessionState
-- | Redraw the current prompt and edit buffer.
--
-- Use this after writing asynchronous output to the terminal.
showEditSession :: EditSession -> IO ()
showEditSession = linenoiseShow . editSessionState
-- | Run an action while the current prompt and edit buffer are hidden.
--
-- This is the primitive for prompt-safe asynchronous terminal output. Callers
-- that can write concurrently should still serialize access with a lock.
withHiddenEditSession :: EditSession -> IO a -> IO a
withHiddenEditSession session = bracket_ (hideEditSession session) (showEditSession session)
-- | Add to current history.
addHistory :: ByteString -> IO ()
addHistory bs =
unless (BS.null bs) $ BSU.unsafeUseAsCString bs $ \str -> do
_ <- linenoiseHistoryAdd str
pure ()
-- | Limit the maximum history length.
stifleHistory :: Int -> IO ()
stifleHistory len = do
_ <- linenoiseHistorySetMaxLen $ fromIntegral len
pure ()
-- | Save history to a file.
historySave :: FilePath -> IO ()
historySave fname = do
str <- newCString fname
linenoiseHistorySave str
-- | Load history from a file.
historyLoad :: FilePath -> IO ()
historyLoad fname = do
str <- newCString fname
linenoiseHistoryLoad str
-- | Clear the screen.
clearScreen :: IO ()
clearScreen = linenoiseClearScreen
-- | Enable/Disable multiline input.
setMultiline :: Bool -> IO ()
setMultiline = linenoiseSetMultiLine . fromBool
-- | Print keycodes.
printKeycodes :: IO ()
printKeycodes = linenoisePrintKeyCodes
-- | Set the current completion function.
setCompletion :: (ByteString -> IO [ByteString]) -> IO ()
setCompletion f = do
cb <- makeFunPtr (makeCompletion f)
linenoiseSetCompletionCallback cb
copyByteString :: ByteString -> IO CString
copyByteString bs =
BSU.unsafeUseAsCStringLen bs $ \(ptr, len) -> do
out <- mallocBytes (len + 1)
copyBytes out ptr len
pokeByteOff out len (0 :: CChar)
pure out
linenoiseStateBytes :: Int
linenoiseStateBytes = 4096
editBufferBytes :: Int
editBufferBytes = 1024 * 1024 + 1