packages feed

haskeline 0.8.4.1 → 0.8.5.0

raw patch · 13 files changed

+426/−89 lines, 13 filesdep +optparse-applicativePVP ok

version bump matches the API change (PVP)

Dependencies added: optparse-applicative

API changes (from Hackage documentation)

+ System.Console.Haskeline: keyseqTimeoutMs :: Prefs -> Word
+ System.Console.Haskeline: useTermHandles :: Handle -> Handle -> Behavior
+ System.Console.Haskeline: useTermHandlesWith :: String -> Handle -> Handle -> Behavior
+ System.Console.Haskeline.Internal: ArgMode :: Int -> s -> ArgMode s
+ System.Console.Haskeline.Internal: Backspace :: BaseKey
+ System.Console.Haskeline.Internal: Behavior :: IO RunTerm -> Behavior
+ System.Console.Haskeline.Internal: CEmpty :: CommandMode
+ System.Console.Haskeline.Internal: CMode :: [Grapheme] -> Grapheme -> [Grapheme] -> CommandMode
+ System.Console.Haskeline.Internal: Delete :: BaseKey
+ System.Console.Haskeline.Internal: DownKey :: BaseKey
+ System.Console.Haskeline.Internal: End :: BaseKey
+ System.Console.Haskeline.Internal: ErrorEvent :: SomeException -> Event
+ System.Console.Haskeline.Internal: EvalTerm :: (forall a. () => n a -> m a) -> (forall a. () => m a -> n a) -> EvalTerm (m :: Type -> Type)
+ System.Console.Haskeline.Internal: ExternalPrint :: String -> Event
+ System.Console.Haskeline.Internal: FileOps :: (forall (m :: Type -> Type) a. (MonadIO m, MonadMask m) => m a -> m a) -> (forall a. () => IO a -> IO a) -> MaybeT IO String -> MaybeT IO Char -> IO () -> FileOps
+ System.Console.Haskeline.Internal: FunKey :: Int -> BaseKey
+ System.Console.Haskeline.Internal: Home :: BaseKey
+ System.Console.Haskeline.Internal: IMode :: [Grapheme] -> [Grapheme] -> InsertMode
+ System.Console.Haskeline.Internal: Interrupt :: Interrupt
+ System.Console.Haskeline.Internal: Key :: Modifier -> BaseKey -> Key
+ System.Console.Haskeline.Internal: KeyChar :: Char -> BaseKey
+ System.Console.Haskeline.Internal: KeyInput :: [Key] -> Event
+ System.Console.Haskeline.Internal: KillLine :: BaseKey
+ System.Console.Haskeline.Internal: Layout :: Int -> Int -> Layout
+ System.Console.Haskeline.Internal: LeftKey :: BaseKey
+ System.Console.Haskeline.Internal: MaybeT :: m (Maybe a) -> MaybeT (m :: Type -> Type) a
+ System.Console.Haskeline.Internal: Message :: String -> Message
+ System.Console.Haskeline.Internal: Modifier :: Bool -> Bool -> Bool -> Modifier
+ System.Console.Haskeline.Internal: PageDown :: BaseKey
+ System.Console.Haskeline.Internal: PageUp :: BaseKey
+ System.Console.Haskeline.Internal: Password :: [Char] -> Maybe Char -> Password
+ System.Console.Haskeline.Internal: RightKey :: BaseKey
+ System.Console.Haskeline.Internal: RunTerm :: (String -> IO ()) -> Either TermOps FileOps -> (forall a (m :: Type -> Type). (MonadIO m, MonadMask m) => m a -> m a) -> IO () -> RunTerm
+ System.Console.Haskeline.Internal: SearchForward :: BaseKey
+ System.Console.Haskeline.Internal: SearchReverse :: BaseKey
+ System.Console.Haskeline.Internal: TermOps :: IO Layout -> (forall (m :: Type -> Type) a. CommandMonad m => (m Event -> m a) -> m a) -> (forall (m :: Type -> Type). CommandMonad m => EvalTerm m) -> ([Key] -> IO ()) -> (String -> IO ()) -> TermOps
+ System.Console.Haskeline.Internal: UpKey :: BaseKey
+ System.Console.Haskeline.Internal: WindowResize :: Event
+ System.Console.Haskeline.Internal: [argState] :: ArgMode s -> s
+ System.Console.Haskeline.Internal: [arg] :: ArgMode s -> Int
+ System.Console.Haskeline.Internal: [closeTerm] :: RunTerm -> IO ()
+ System.Console.Haskeline.Internal: [evalTerm] :: TermOps -> forall (m :: Type -> Type). CommandMonad m => EvalTerm m
+ System.Console.Haskeline.Internal: [externalPrint] :: TermOps -> String -> IO ()
+ System.Console.Haskeline.Internal: [getLayout] :: TermOps -> IO Layout
+ System.Console.Haskeline.Internal: [getLocaleChar] :: FileOps -> MaybeT IO Char
+ System.Console.Haskeline.Internal: [getLocaleLine] :: FileOps -> MaybeT IO String
+ System.Console.Haskeline.Internal: [hasControl] :: Modifier -> Bool
+ System.Console.Haskeline.Internal: [hasMeta] :: Modifier -> Bool
+ System.Console.Haskeline.Internal: [hasShift] :: Modifier -> Bool
+ System.Console.Haskeline.Internal: [height] :: Layout -> Int
+ System.Console.Haskeline.Internal: [maybeReadNewline] :: FileOps -> IO ()
+ System.Console.Haskeline.Internal: [messageText] :: Message -> String
+ System.Console.Haskeline.Internal: [passwordChar] :: Password -> Maybe Char
+ System.Console.Haskeline.Internal: [passwordState] :: Password -> [Char]
+ System.Console.Haskeline.Internal: [putStrOut] :: RunTerm -> String -> IO ()
+ System.Console.Haskeline.Internal: [runMaybeT] :: MaybeT (m :: Type -> Type) a -> m (Maybe a)
+ System.Console.Haskeline.Internal: [saveUnusedKeys] :: TermOps -> [Key] -> IO ()
+ System.Console.Haskeline.Internal: [termOps] :: RunTerm -> Either TermOps FileOps
+ System.Console.Haskeline.Internal: [width] :: Layout -> Int
+ System.Console.Haskeline.Internal: [withGetEvent] :: TermOps -> forall (m :: Type -> Type) a. CommandMonad m => (m Event -> m a) -> m a
+ System.Console.Haskeline.Internal: [withoutInputEcho] :: FileOps -> forall (m :: Type -> Type) a. (MonadIO m, MonadMask m) => m a -> m a
+ System.Console.Haskeline.Internal: [wrapFileInput] :: FileOps -> forall a. () => IO a -> IO a
+ System.Console.Haskeline.Internal: [wrapInterrupt] :: RunTerm -> forall a (m :: Type -> Type). (MonadIO m, MonadMask m) => m a -> m a
+ System.Console.Haskeline.Internal: addNum :: Int -> ArgMode s -> ArgMode s
+ System.Console.Haskeline.Internal: addPasswordChar :: Char -> Password -> Password
+ System.Console.Haskeline.Internal: afterChar :: (Char -> Bool) -> InsertMode -> Bool
+ System.Console.Haskeline.Internal: afterCursor :: LineState s => s -> [Grapheme]
+ System.Console.Haskeline.Internal: appendFromCommandMode :: CommandMode -> InsertMode
+ System.Console.Haskeline.Internal: applyArg :: (s -> s) -> ArgMode s -> s
+ System.Console.Haskeline.Internal: applyCmdArg :: (InsertMode -> InsertMode) -> ArgMode CommandMode -> CommandMode
+ System.Console.Haskeline.Internal: ask :: MonadReader r m => m r
+ System.Console.Haskeline.Internal: asks :: MonadReader r m => (r -> a) -> m a
+ System.Console.Haskeline.Internal: atEnd :: (Char -> Bool) -> InsertMode -> Bool
+ System.Console.Haskeline.Internal: atStart :: (Char -> Bool) -> InsertMode -> Bool
+ System.Console.Haskeline.Internal: baseChar :: Grapheme -> Char
+ System.Console.Haskeline.Internal: beforeChar :: (Char -> Bool) -> InsertMode -> Bool
+ System.Console.Haskeline.Internal: beforeCursor :: LineState s => Prefix -> s -> [Grapheme]
+ System.Console.Haskeline.Internal: bracketSet :: (MonadMask m, MonadIO m) => IO a -> (a -> IO ()) -> a -> m b -> m b
+ System.Console.Haskeline.Internal: class (MonadReader Prefs m, MonadReader Layout m, MonadIO m, MonadMask m) => CommandMonad (m :: Type -> Type)
+ System.Console.Haskeline.Internal: class LineState s
+ System.Console.Haskeline.Internal: class Monad m => MonadIO (m :: Type -> Type)
+ System.Console.Haskeline.Internal: class Monad m => MonadReader r (m :: Type -> Type)
+ System.Console.Haskeline.Internal: class Monad m => MonadState s (m :: Type -> Type)
+ System.Console.Haskeline.Internal: class forall (m :: Type -> Type). Monad m => Monad t m => MonadTrans (t :: Type -> Type -> Type -> Type)
+ System.Console.Haskeline.Internal: class Move s
+ System.Console.Haskeline.Internal: class LineState s => Result s
+ System.Console.Haskeline.Internal: class LineState s => Save s
+ System.Console.Haskeline.Internal: class (MonadReader Layout m, MonadIO m, MonadMask m) => Term (m :: Type -> Type)
+ System.Console.Haskeline.Internal: clearLayout :: Term m => m ()
+ System.Console.Haskeline.Internal: clearLine :: Term m => LineChars -> m ()
+ System.Console.Haskeline.Internal: ctrlChar :: Char -> Key
+ System.Console.Haskeline.Internal: ctrlKey :: Key -> Key
+ System.Console.Haskeline.Internal: data ArgMode s
+ System.Console.Haskeline.Internal: data BaseKey
+ System.Console.Haskeline.Internal: data Behavior
+ System.Console.Haskeline.Internal: data CommandMode
+ System.Console.Haskeline.Internal: data EvalTerm (m :: Type -> Type)
+ System.Console.Haskeline.Internal: data Event
+ System.Console.Haskeline.Internal: data FileOps
+ System.Console.Haskeline.Internal: data Grapheme
+ System.Console.Haskeline.Internal: data InsertMode
+ System.Console.Haskeline.Internal: data Interrupt
+ System.Console.Haskeline.Internal: data Key
+ System.Console.Haskeline.Internal: data Layout
+ System.Console.Haskeline.Internal: data Modifier
+ System.Console.Haskeline.Internal: data Password
+ System.Console.Haskeline.Internal: data ReaderT r (m :: Type -> Type) a
+ System.Console.Haskeline.Internal: data RunTerm
+ System.Console.Haskeline.Internal: data StateT s (m :: Type -> Type) a
+ System.Console.Haskeline.Internal: data TermOps
+ System.Console.Haskeline.Internal: deleteChar :: CommandMode -> CommandMode
+ System.Console.Haskeline.Internal: deleteNext :: InsertMode -> InsertMode
+ System.Console.Haskeline.Internal: deletePasswordChar :: Password -> Password
+ System.Console.Haskeline.Internal: deletePrev :: InsertMode -> InsertMode
+ System.Console.Haskeline.Internal: drawLine :: Term m => LineChars -> m ()
+ System.Console.Haskeline.Internal: drawLineDiff :: Term m => LineChars -> LineChars -> m ()
+ System.Console.Haskeline.Internal: emptyIM :: InsertMode
+ System.Console.Haskeline.Internal: enterCommandMode :: InsertMode -> CommandMode
+ System.Console.Haskeline.Internal: enterCommandModeRight :: InsertMode -> CommandMode
+ System.Console.Haskeline.Internal: evalStateT' :: Monad m => s -> StateT s m a -> m a
+ System.Console.Haskeline.Internal: flushEventQueue :: (String -> IO ()) -> TChan Event -> IO ()
+ System.Console.Haskeline.Internal: get :: MonadState s m => m s
+ System.Console.Haskeline.Internal: gets :: MonadState s m => (s -> a) -> m a
+ System.Console.Haskeline.Internal: goLeft :: Move s => s -> s
+ System.Console.Haskeline.Internal: goLeftUntil :: (InsertMode -> Bool) -> InsertMode -> InsertMode
+ System.Console.Haskeline.Internal: goRight :: Move s => s -> s
+ System.Console.Haskeline.Internal: goRightUntil :: (InsertMode -> Bool) -> InsertMode -> InsertMode
+ System.Console.Haskeline.Internal: graphemesToString :: [Grapheme] -> String
+ System.Console.Haskeline.Internal: guardedEOF :: (Handle -> IO a) -> Handle -> MaybeT IO a
+ System.Console.Haskeline.Internal: hGetByte :: Handle -> MaybeT IO Word8
+ System.Console.Haskeline.Internal: hGetLocaleLine :: Handle -> MaybeT IO ByteString
+ System.Console.Haskeline.Internal: hMaybeReadNewline :: Handle -> IO ()
+ System.Console.Haskeline.Internal: hWithBinaryMode :: (MonadIO m, MonadMask m) => Handle -> m a -> m a
+ System.Console.Haskeline.Internal: insertChar :: Char -> InsertMode -> InsertMode
+ System.Console.Haskeline.Internal: insertFromCommandMode :: CommandMode -> InsertMode
+ System.Console.Haskeline.Internal: insertGraphemes :: [Grapheme] -> InsertMode -> InsertMode
+ System.Console.Haskeline.Internal: insertString :: String -> InsertMode -> InsertMode
+ System.Console.Haskeline.Internal: isTerminalStyle :: RunTerm -> Bool
+ System.Console.Haskeline.Internal: keyEventLoop :: IO [Event] -> TChan Event -> IO Event
+ System.Console.Haskeline.Internal: lengthToEnd :: LineChars -> Int
+ System.Console.Haskeline.Internal: lift :: (MonadTrans t, Monad m) => m a -> t m a
+ System.Console.Haskeline.Internal: liftIO :: MonadIO m => IO a -> m a
+ System.Console.Haskeline.Internal: lineChars :: LineState s => Prefix -> s -> LineChars
+ System.Console.Haskeline.Internal: listRestore :: Save s => [Grapheme] -> s
+ System.Console.Haskeline.Internal: listSave :: Save s => s -> [Grapheme]
+ System.Console.Haskeline.Internal: mapBaseChars :: (Char -> Char) -> [Grapheme] -> [Grapheme]
+ System.Console.Haskeline.Internal: mapEvalTerm :: (forall a. () => n a -> m a) -> (forall a. () => m a -> n a) -> EvalTerm n -> EvalTerm m
+ System.Console.Haskeline.Internal: mapReaderT :: (m a -> n b) -> ReaderT r m a -> ReaderT r n b
+ System.Console.Haskeline.Internal: mapStateT :: (m (a, s) -> n (b, s)) -> StateT s m a -> StateT s n b
+ System.Console.Haskeline.Internal: matchInit :: Eq a => [a] -> [a] -> ([a], [a])
+ System.Console.Haskeline.Internal: metaChar :: Char -> Key
+ System.Console.Haskeline.Internal: metaKey :: Key -> Key
+ System.Console.Haskeline.Internal: modify :: MonadState s m => (s -> s) -> m ()
+ System.Console.Haskeline.Internal: modifyBaseChar :: (Char -> Char) -> Grapheme -> Grapheme
+ System.Console.Haskeline.Internal: moveToEnd :: Move s => s -> s
+ System.Console.Haskeline.Internal: moveToNextLine :: Term m => LineChars -> m ()
+ System.Console.Haskeline.Internal: moveToStart :: Move s => s -> s
+ System.Console.Haskeline.Internal: newtype MaybeT (m :: Type -> Type) a
+ System.Console.Haskeline.Internal: newtype Message
+ System.Console.Haskeline.Internal: noModifier :: Modifier
+ System.Console.Haskeline.Internal: orElse :: Monad m => MaybeT m a -> m a -> m a
+ System.Console.Haskeline.Internal: overChar :: (Char -> Bool) -> InsertMode -> Bool
+ System.Console.Haskeline.Internal: parseKey :: String -> Maybe Key
+ System.Console.Haskeline.Internal: pasteGraphemesAfter :: [Grapheme] -> CommandMode -> CommandMode
+ System.Console.Haskeline.Internal: pasteGraphemesBefore :: [Grapheme] -> CommandMode -> CommandMode
+ System.Console.Haskeline.Internal: printLines :: Term m => [String] -> m ()
+ System.Console.Haskeline.Internal: put :: MonadState s m => s -> m ()
+ System.Console.Haskeline.Internal: replaceChar :: Char -> CommandMode -> CommandMode
+ System.Console.Haskeline.Internal: replaceCharIM :: Char -> InsertMode -> InsertMode
+ System.Console.Haskeline.Internal: reposition :: Term m => Layout -> LineChars -> m ()
+ System.Console.Haskeline.Internal: restore :: Save s => InsertMode -> s
+ System.Console.Haskeline.Internal: returnOnEOF :: MonadMask m => a -> m a -> m a
+ System.Console.Haskeline.Internal: ringBell :: Term m => Bool -> m ()
+ System.Console.Haskeline.Internal: runCompletion :: CommandMonad m => (String, String) -> m (String, [Completion])
+ System.Console.Haskeline.Internal: runReaderT' :: r -> ReaderT r m a -> m a
+ System.Console.Haskeline.Internal: save :: Save s => s -> InsertMode
+ System.Console.Haskeline.Internal: saveKeys :: TChan Event -> [Key] -> IO ()
+ System.Console.Haskeline.Internal: setControlBits :: Char -> Char
+ System.Console.Haskeline.Internal: simpleChar :: Char -> Key
+ System.Console.Haskeline.Internal: simpleKey :: BaseKey -> Key
+ System.Console.Haskeline.Internal: skipLeft :: (Char -> Bool) -> InsertMode -> InsertMode
+ System.Console.Haskeline.Internal: skipRight :: (Char -> Bool) -> InsertMode -> InsertMode
+ System.Console.Haskeline.Internal: startArg :: Int -> s -> ArgMode s
+ System.Console.Haskeline.Internal: stringToGraphemes :: String -> [Grapheme]
+ System.Console.Haskeline.Internal: toResult :: Result s => s -> String
+ System.Console.Haskeline.Internal: transposeChars :: InsertMode -> InsertMode
+ System.Console.Haskeline.Internal: type LineChars = ([Grapheme], [Grapheme])
+ System.Console.Haskeline.Internal: type Prefix = [Grapheme]
+ System.Console.Haskeline.Internal: update :: MonadState s m => (s -> (a, s)) -> m a
+ System.Console.Haskeline.Internal: withCommandMode :: (InsertMode -> InsertMode) -> CommandMode -> CommandMode

Files

Changelog view
@@ -1,3 +1,17 @@+Changed in version 8.4.5.0:++   * System.Console.Haskeline.Internal now exposes far more internals of+     Haskeline.++   * Added `useTermHandles` and `useTermHandlesWith` Behaviors for driving+     Haskeline against caller-supplied input/output handles (e.g. a serial+     console or a PTY pair other than the controlling terminal).  POSIX only.++   * On POSIX, fixed escape sequences breaking when their bytes arrive in+     separate reads (e.g. on slow serial links).  Added `keyseqTimeoutMs`+     to `Prefs` to configure the wait, mirroring GNU Readline's+     `keyseq-timeout`.+ Changed in version 0.8.4.1:     * Implemented ; and , movements and enabled them for d, c, and y actions.
System/Console/Haskeline.hs view
@@ -39,6 +39,10 @@                     defaultBehavior,                     useFileHandle,                     useFile,+#ifndef MINGW+                    useTermHandles,+                    useTermHandlesWith,+#endif                     preferTerm,                     -- * User interaction functions                     -- ** Reading user input@@ -60,6 +64,7 @@                     setComplete,                     -- ** User preferences                     Prefs(),+                    keyseqTimeoutMs,                     readPrefs,                     defaultPrefs,                     runInputTWithPrefs,
System/Console/Haskeline/Backend.hs view
@@ -23,27 +23,39 @@ terminalRunTerm :: IO RunTerm terminalRunTerm = directTTY `orElse` fileHandleRunTerm stdin +#ifndef MINGW+useTermHandlesRunTerm :: Maybe String -> Handle -> Handle -> IO RunTerm+useTermHandlesRunTerm termtype input output =+    explicitTTY termtype input output `orElse` fileHandleRunTerm input+#endif+ stdinTTY :: MaybeT IO RunTerm #ifdef MINGW stdinTTY = win32TermStdin #else-stdinTTY = stdinTTYHandles >>= runDraw+stdinTTY = stdinTTYHandles >>= runDraw Nothing #endif  directTTY :: MaybeT IO RunTerm #ifdef MINGW directTTY = win32Term #else-directTTY = ttyHandles >>= runDraw+directTTY = ttyHandles >>= runDraw Nothing #endif +#ifndef MINGW+explicitTTY :: Maybe String -> Handle -> Handle -> MaybeT IO RunTerm+explicitTTY termtype input output =+    explicitTTYHandles input output >>= runDraw termtype+#endif + #ifndef MINGW-runDraw :: Handles -> MaybeT IO RunTerm+runDraw :: Maybe String -> Handles -> MaybeT IO RunTerm #ifndef TERMINFO-runDraw = runDumbTerm+runDraw _termtype = runDumbTerm #else-runDraw h = runTerminfoDraw h `mplus` runDumbTerm h+runDraw termtype h = runTerminfoDraw termtype h `mplus` runDumbTerm h #endif #endif 
System/Console/Haskeline/Backend/Posix.hsc view
@@ -11,6 +11,7 @@                         mapLines,                         stdinTTYHandles,                         ttyHandles,+                        explicitTTYHandles,                         posixRunTerm,                         fileRunTerm                  ) where@@ -201,16 +202,55 @@             = metaKey k : ks lexKeys baseMap (c:cs) = simpleChar c : lexKeys baseMap cs -lookupChars :: TreeMap Char Key -> [Char] -> Maybe (Key,[Char])-lookupChars _ [] = Nothing-lookupChars (TreeMap tm) (c:cs) = case Map.lookup c tm of-    Nothing -> Nothing-    Just (Nothing,t) -> lookupChars t cs-    Just (Just k, t@(TreeMap tm2))-                | not (null cs) && not (Map.null tm2) -- ?? lookup d tm2?-                    -> lookupChars t cs-                | otherwise -> Just (k, cs)+-- | Walk @cs@ through the tree, returning the /longest/ key found along+-- the path together with whatever bytes follow it.+--+-- If a longer match is attempted but doesn't pan out (the next byte+-- isn't in the deeper subtree), we fall back to the most recent shorter+-- key we passed.  This avoids the well-known sharp edge where input+-- like @\\ESC[AC@ — with @\\ESC[A@ registered (up-arrow) and the @A@+-- node having children that don't include @C@ — would previously be+-- reported as no match at all, instead of @up-arrow + 'C'@.+--+-- 'Nothing' is returned only when no key is reached anywhere along the+-- walk.+lookupChars :: TreeMap Char Key -> [Char] -> Maybe (Key, [Char])+lookupChars _    [] = Nothing+lookupChars tree cs = go Nothing tree cs+  where+    -- 'best' holds the longest committed match so far (or Nothing if+    -- we haven't passed a key node yet).  When the next byte doesn't+    -- extend the path we return 'best'.+    go best (TreeMap m) (c:cs') = case Map.lookup c m of+        Nothing          -> best+        Just (mKey, sub) ->+            let best' = maybe best (\k -> Just (k, cs')) mKey+            in case cs' of+                _:_ -> go best' sub cs'+                []  -> best'+    go best _ [] = best  -- unreachable: top-level [] handled above +-- | Greedily peel complete keys off the front of @cs@.  Returns the+-- decoded keys (in forward order) and the leftover bytes that didn't+-- (yet) form a complete tree match.+peelCompleteKeys :: TreeMap Char Key -> [Char] -> ([Key], [Char])+peelCompleteKeys tree = go id+  where+    go acc cs = case lookupChars tree cs of+        Just (k, rest) -> go (acc . (k:)) rest+        Nothing        -> (acc [], cs)++-- | True if @cs@ matches a /strict/ (non-terminal) prefix of some path+-- in the @TreeMap@ — i.e. it could still be extended into a complete+-- key sequence if more bytes arrive.+isStrictTreePrefix :: TreeMap Char b -> [Char] -> Bool+isStrictTreePrefix _ [] = False+isStrictTreePrefix (TreeMap m) (c:cs) = case Map.lookup c m of+    Nothing                    -> False+    Just (_, sub@(TreeMap sm)) -> case cs of+        [] -> not (Map.null sm)+        _  -> isStrictTreePrefix sub cs+ -----------------------------  withPosixGetEvent :: (MonadIO m, MonadMask m, MonadReader Prefs m)@@ -218,8 +258,9 @@                 -> (m Event -> m a) -> m a withPosixGetEvent eventChan h termKeys f = wrapTerminalOps h $ do     baseMap <- getKeySequences (ehIn h) termKeys+    timeoutMs <- asks (fromIntegral . keyseqTimeoutMs :: Prefs -> Int)     withWindowHandler eventChan-        $ f $ liftIO $ getEvent (ehIn h) baseMap eventChan+        $ f $ liftIO $ getEvent (ehIn h) timeoutMs baseMap eventChan  withWindowHandler :: (MonadIO m, MonadMask m) => TChan Event -> m a -> m a withWindowHandler eventChan = withHandler windowChange $@@ -237,27 +278,62 @@     old_handler <- liftIO $ installHandler signal handler Nothing     f `finally` liftIO (installHandler signal old_handler Nothing) -getEvent :: Handle -> TreeMap Char Key -> TChan Event -> IO Event-getEvent h baseMap = keyEventLoop $ do-        cs <- getBlockOfChars h-        return [KeyInput $ lexKeys baseMap cs]+getEvent :: Handle -> Int -> TreeMap Char Key -> TChan Event -> IO Event+getEvent h timeoutMs baseMap = keyEventLoop $ do+        ks <- getBlockOfKeys h baseMap timeoutMs+        return [KeyInput ks] --- Read at least one character of input, and more if immediately--- available.  In particular the characters making up a control sequence--- will all be available at once, so they can be processed together--- (with Posix.lexKeys).-getBlockOfChars :: Handle -> IO String-getBlockOfChars h = do+-- | Read at least one key of input, and more if available.+--+-- A multi-byte control sequence (e.g. @\\ESC[A@ for arrow-up) may not+-- arrive in a single read on a slow link such as a low-bandwidth serial+-- port: the @\\ESC@ can land before the @[A@.  When the buffer ends in+-- bytes that are a strict prefix of some known key sequence, we wait up+-- to @timeoutMs@ for the rest before returning.  Otherwise we return as+-- soon as nothing more is buffered.  This mirrors GNU Readline's+-- @keyseq-timeout@.+--+-- The loop tracks bytes and decoded keys side by side: every time we+-- run out of immediately-available input we peel complete keys from the+-- front of the byte buffer and shrink it down to just its trailing+-- (possibly partial) prefix.  That keeps the per-decision work bounded+-- by the size of the in-flight sequence rather than the whole accrued+-- buffer, which matters when bytes trickle in slowly.+--+-- Win32 is unaffected: that backend reads structured @INPUT_RECORD@+-- key events rather than raw bytes, so escape sequences are never+-- fragmented in the first place.+getBlockOfKeys :: Handle -> TreeMap Char Key -> Int -> IO [Key]+getBlockOfKeys h baseMap timeoutMs = do     c <- hGetChar h-    loop [c]+    loop [c] []   where-    loop cs = do-        isReady <- hReady h-        if not isReady-            then return $ reverse cs-            else do-                    c <- hGetChar h-                    loop (c:cs)+    -- Both lists hold values in reverse (newest first) so that consing a+    -- new element is O(1).  We reverse only at decision points or on+    -- return.+    loop bytesRev keysRev = do+        ready <- hWaitForInput h 0+        if ready+            then do+                c <- hGetChar h+                loop (c:bytesRev) keysRev+            else+                -- Drain whatever complete keys are sitting at the head of+                -- the byte buffer; what's left is either empty (done) or a+                -- still-in-flight partial sequence.+                let (peeled, partial) = peelCompleteKeys baseMap (reverse bytesRev)+                    keysRev'          = reverse peeled ++ keysRev+                in if null partial+                    then pure (reverse keysRev')+                    else if isStrictTreePrefix baseMap partial+                        then do+                            arrived <- hWaitForInput h timeoutMs+                            if arrived+                                then do+                                    c <- hGetChar h+                                    loop (c : reverse partial) keysRev'+                                else pure (reverse keysRev' ++ lexKeys baseMap partial)+                        else pure (reverse keysRev' ++ lexKeys baseMap partial)  stdinTTYHandles, ttyHandles :: MaybeT IO Handles stdinTTYHandles = do@@ -286,6 +362,15 @@ openTerm mode = handle (\(_::IOException) -> mzero)             $ liftIO $ openInCodingMode "/dev/tty" mode +explicitTTYHandles :: Handle -> Handle -> MaybeT IO Handles+explicitTTYHandles h_in h_out = do+    isInTerm <- liftIO $ hIsTerminalDevice h_in+    guard isInTerm+    return Handles+            { hIn = externalHandle h_in+            , hOut = externalHandle h_out+            , closeHandles = return ()+            }  posixRunTerm ::     Handles
System/Console/Haskeline/Backend/Terminfo.hs view
@@ -125,9 +125,9 @@                             . unDraw    -runTerminfoDraw :: Handles -> MaybeT IO RunTerm-runTerminfoDraw h = do-    mterm <- liftIO $ Exception.try setupTermFromEnv+runTerminfoDraw :: Maybe String -> Handles -> MaybeT IO RunTerm+runTerminfoDraw termtype h = do+    mterm <- liftIO $ Exception.try $ maybe setupTermFromEnv setupTerm termtype     case mterm of         Left (_::SetupTermError) -> mzero         Right term -> do
System/Console/Haskeline/InputT.hs view
@@ -216,6 +216,70 @@ preferTerm :: Behavior preferTerm = Behavior terminalRunTerm +#ifndef MINGW+-- | Use terminal-style interaction on the given input and output handles,+-- taking the terminal type from the @TERM@ environment variable.+--+-- This behavior is for driving Haskeline against a terminal that is not the+-- process's controlling terminal — for example, a serial console, a PTY pair+-- you opened yourself, or a socket-backed TTY.  The caller is responsible for+-- closing @input@ and @output@ after use.  Not available on Windows.+--+-- See 'useTermHandlesWith' to override the terminal type.+useTermHandles :: Handle -> Handle -> Behavior+useTermHandles input output =+    Behavior $ useTermHandlesRunTerm Nothing input output++-- | Like 'useTermHandles', but with the terminal type given explicitly+-- (e.g. @\"xterm-256color\"@ or @\"vt100\"@) instead of read from the @TERM@+-- environment variable.+--+-- The terminal type is only consulted when haskeline is built with terminfo+-- support; in non-terminfo builds it is ignored and a dumb terminal is used.+--+-- ==== __Example: a Haskeline session over a WebSocket__+--+-- Bridge a WebSocket to the master end of a PTY pair and run Haskeline+-- against the slave end.  Uses the @websockets@ and @unix@ packages.+-- Pair this with a browser-side terminal emulator such as+-- <https://xtermjs.org/ Xterm.js> for an in-browser shell.+--+-- > import qualified Network.WebSockets as WS+-- > import System.Posix.Terminal (openPseudoTerminal)+-- > import System.Posix.IO (dup, fdToHandle)+-- > import Control.Applicative ((<|>))+-- > import Control.Concurrent.Async (Concurrently(..), runConcurrently)+-- > import Control.Monad (forever)+-- > import qualified Data.ByteString as BS+-- > import System.IO (hSetBuffering, BufferMode(..))+-- > import System.Console.Haskeline+-- >+-- > websocketUI :: WS.Connection -> IO ()+-- > websocketUI conn = do+-- >     (master, slave) <- openPseudoTerminal+-- >     slaveDup <- dup slave+-- >     masterH  <- fdToHandle master+-- >     slaveIn  <- fdToHandle slave+-- >     slaveOut <- fdToHandle slaveDup+-- >     hSetBuffering masterH NoBuffering+-- >     -- Whichever of the three actions finishes first cancels the others.+-- >     runConcurrently+-- >         $   Concurrently (forever $ WS.receiveData conn >>= BS.hPut masterH)+-- >         <|> Concurrently (forever $ BS.hGetSome masterH 4096 >>= WS.sendBinaryData conn)+-- >         <|> Concurrently (runInputTBehavior+-- >                              (useTermHandlesWith "vt100" slaveIn slaveOut)+-- >                              defaultSettings loop)+-- >   where+-- >     loop = do+-- >         minput <- getInputLine "% "+-- >         case minput of+-- >             Nothing     -> return ()+-- >             Just "quit" -> return ()+-- >             Just s      -> outputStrLn ("got: " ++ s) >> loop+useTermHandlesWith :: String -> Handle -> Handle -> Behavior+useTermHandlesWith termtype input output =+    Behavior $ useTermHandlesRunTerm (Just termtype) input output+#endif  -- | Read 'Prefs' from @$XDG_CONFIG_HOME/haskeline/haskeline@ if present -- ortherwise @~/.haskeline.@ If there is an error reading the file,
System/Console/Haskeline/Internal.hs view
@@ -1,5 +1,12 @@+-- | A module containing semi-public internals. The functions here are not+-- stable. module System.Console.Haskeline.Internal-    ( debugTerminalKeys ) where+    ( module System.Console.Haskeline.Monads+    , module System.Console.Haskeline.Term+    , module System.Console.Haskeline.Key+    , module System.Console.Haskeline.LineState+    , Behavior (..)+    , debugTerminalKeys ) where  import System.Console.Haskeline (defaultSettings, outputStrLn) import System.Console.Haskeline.Command@@ -8,6 +15,7 @@ import System.Console.Haskeline.Monads import System.Console.Haskeline.RunCommand import System.Console.Haskeline.Term+import System.Console.Haskeline.Key  import Control.Monad ((>=>)) 
System/Console/Haskeline/Key.hs view
@@ -8,7 +8,8 @@             ctrlChar,             metaKey,             ctrlKey,-            parseKey+            parseKey,+            setControlBits             ) where  import Data.Bits
System/Console/Haskeline/Prefs.hs view
@@ -14,6 +14,7 @@ import Data.Char(isSpace,toLower) import Data.List(foldl') import qualified Data.Map as Map+import Data.Word (Word) import System.Console.Haskeline.Key  {- |@@ -48,7 +49,16 @@                         -- presses @TAB@ again.                      customBindings :: Map.Map Key [Key],                         -- (termName, keysequence, key)-                     customKeySequences :: [(Maybe String, String,Key)]+                     customKeySequences :: [(Maybe String, String,Key)],+                     keyseqTimeoutMs :: !Word+                        -- ^ After an @ESC@ byte is read, how long to wait+                        -- (in milliseconds) for the rest of an escape+                        -- sequence before treating the @ESC@ as a+                        -- standalone keypress.  Matters on slow links+                        -- (e.g. low-bandwidth serial), where the bytes of+                        -- a sequence such as @\\ESC[A@ may not arrive in+                        -- a single read.  Mirrors GNU Readline's+                        -- @keyseq-timeout@.                      }                         deriving Show @@ -77,7 +87,8 @@                       listCompletionsImmediately = True,                       historyDuplicates = AlwaysAdd,                       customBindings = Map.empty,-                      customKeySequences = []+                      customKeySequences = [],+                      keyseqTimeoutMs = 50                     }  mkSettor :: Read a => (a -> Prefs -> Prefs) -> String -> Prefs -> Prefs@@ -98,6 +109,7 @@           ,("completionpromptlimit", mkSettor $ \x p -> p {completionPromptLimit = x})           ,("listcompletionsimmediately", mkSettor $ \x p -> p {listCompletionsImmediately = x})           ,("historyduplicates", mkSettor $ \x p -> p {historyDuplicates = x})+          ,("keyseqtimeoutms", mkSettor $ \x p -> p {keyseqTimeoutMs = x})           ,("bind", addCustomBinding)           ,("keyseq", addCustomKeySequence)           ]
examples/Test.hs view
@@ -1,41 +1,138 @@+{-# LANGUAGE CPP #-} module Main where  import System.Console.Haskeline-import System.Environment+import Options.Applicative+import Data.Monoid ((<>))+#ifndef MINGW+import System.IO (openFile, hClose, IOMode(..))+#endif  {-- Testing the line-input functions and their interaction with ctrl-c signals.  Usage:-./Test          (line input)-./Test chars    (character input)-./Test password (no masking characters)-./Test password \*-./Test initial  (use initial text in the prompt)+  ./Test                                  (line input, default Behavior)+  ./Test chars                            (character input)+  ./Test password                         (no masking)+  ./Test password \*                      (masking with '*')+  ./Test initial                          (initial text in the prompt)++  ./Test --mode useTermHandles            (POSIX only)+  ./Test --mode useTermHandlesWith --term-type vt100+                                          (POSIX only)++The --mode flag selects the haskeline Behavior; positional INPUT_ARG+selects which prompt function to use, and works with any --mode. --} +data Mode+    = UseTerm                    -- ^ defaultBehavior+    | UseTermHandles             -- ^ useTermHandles on /dev/tty+    | UseTermHandlesWith String  -- ^ useTermHandlesWith TERM on /dev/tty++data InputMode+    = LineInput+    | CharInput+    | PasswordInput (Maybe Char)+    | InitialInput++data Opts = Opts { optMode :: Mode, optInput :: InputMode }+ mySettings :: Settings IO mySettings = defaultSettings {historyFile = Just "myhist"}  main :: IO () main = do-        args <- getArgs-        let inputFunc = case args of-                ["chars"] -> fmap (fmap (\c -> [c])) . getInputChar-                ["password"] -> getPassword Nothing-                ["password", [c]] -> getPassword (Just c)-                ["initial"] -> flip getInputLineWithInitial ("left ", "right")-                _ -> getInputLine-        runInputT mySettings $ withInterrupt $ loop inputFunc 0-    where-        loop :: (String -> InputT IO (Maybe String)) -> Int -> InputT IO ()-        loop inputFunc n = do-            minput <-  handleInterrupt (return (Just "Caught interrupted"))-                        $ inputFunc (show n ++ ":")-            case minput of-                Nothing -> return ()-                Just "quit" -> return ()-                Just "q" -> return ()-                Just s -> do-                            outputStrLn ("line " ++ show n ++ ":" ++ s)-                            loop inputFunc (n+1)+    Opts {optMode = m, optInput = im} <- execParser optsInfo+    case m of+        UseTerm ->+            runInputT mySettings (runAction im)+#ifndef MINGW+        UseTermHandles ->+            runWithHandles Nothing im+        UseTermHandlesWith t ->+            runWithHandles (Just t) im+#else+        _ -> error "useTermHandles[With] is not available on Windows"+#endif++runAction :: InputMode -> InputT IO ()+runAction im = withInterrupt $ loop (inputFunc im) 0++inputFunc :: InputMode -> String -> InputT IO (Maybe String)+inputFunc LineInput          = getInputLine+inputFunc CharInput          = fmap (fmap (\c -> [c])) . getInputChar+inputFunc (PasswordInput mc) = getPassword mc+inputFunc InitialInput       = flip getInputLineWithInitial ("left ", "right")++loop :: (String -> InputT IO (Maybe String)) -> Int -> InputT IO ()+loop f n = do+    minput <- handleInterrupt (return (Just "Caught interrupted"))+                $ f (show n ++ ":")+    case minput of+        Nothing     -> return ()+        Just "quit" -> return ()+        Just "q"    -> return ()+        Just s      -> do+            outputStrLn ("line " ++ show n ++ ":" ++ s)+            loop f (n+1)++#ifndef MINGW+-- Drive Haskeline against /dev/tty (the controlling terminal) via the+-- useTermHandles[With] Behaviors.  This still happens to be the controlling+-- tty in our test setup, but it goes through the new code path.+runWithHandles :: Maybe String -> InputMode -> IO ()+runWithHandles mTerm im = do+    input  <- openFile "/dev/tty" ReadMode+    output <- openFile "/dev/tty" WriteMode+    let beh = case mTerm of+            Nothing -> useTermHandles input output+            Just t  -> useTermHandlesWith t input output+    runInputTBehavior beh mySettings (runAction im)+    hClose input+    hClose output+#endif++----------------------------------------------------------------+-- Command-line parsing++optsInfo :: ParserInfo Opts+optsInfo = info (optsP <**> helper)+                (fullDesc <> progDesc "Haskeline test program")++optsP :: Parser Opts+optsP = Opts <$> modeP <*> inputModeP++modeP :: Parser Mode+modeP = mkMode+    <$> strOption+            ( long "mode"+           <> value "useTerm"+           <> showDefault+           <> metavar "MODE"+           <> help "useTerm | useTermHandles | useTermHandlesWith" )+    <*> optional+            (strOption+                ( long "term-type"+               <> metavar "TERM"+               <> help "term type for --mode useTermHandlesWith" ))+  where+    mkMode "useTerm"            _         = UseTerm+    mkMode "useTermHandles"     _         = UseTermHandles+    mkMode "useTermHandlesWith" (Just t)  = UseTermHandlesWith t+    mkMode "useTermHandlesWith" Nothing   =+        error "--mode useTermHandlesWith requires --term-type"+    mkMode other                _         =+        error ("unknown --mode: " ++ other)++inputModeP :: Parser InputMode+inputModeP = mkInput <$> many (argument str (metavar "INPUT_ARG"))+  where+    mkInput []                  = LineInput+    mkInput ["chars"]           = CharInput+    mkInput ["password"]        = PasswordInput Nothing+    mkInput ["password", [c]]   = PasswordInput (Just c)+    mkInput ["initial"]         = InitialInput+    mkInput xs                  =+        error ("unrecognized positional args: " ++ show xs)
haskeline.cabal view
@@ -1,6 +1,6 @@ Name:           haskeline Cabal-Version:  >=1.10-Version:        0.8.4.1+Version:        0.8.5.0 Category:       User Interfaces License:        BSD3 License-File:   LICENSE@@ -169,7 +169,7 @@     if !flag(examples) {         buildable: False     }-    Build-depends: base, containers, haskeline+    Build-depends: base, containers, haskeline, optparse-applicative     Default-Language: Haskell2010     hs-source-dirs: examples     Main-Is: Test.hs
tests/RunTTY.hs view
@@ -23,11 +23,18 @@  data Invocation = Invocation {             prog :: FilePath-            , progArgs :: [String]+              -- | Mode-selection flags passed before any positional args+              -- (e.g. @[\"--mode\", \"useTermHandles\"]@).+            , progModeArgs :: [String]+              -- | Positional input-mode args (e.g. @[\"chars\"]@).+            , progInputArgs :: [String]             , runInTTY :: Bool             , environment :: [(String,String)]             } +progArgs :: Invocation -> [String]+progArgs Invocation{..} = progModeArgs ++ progInputArgs+ setEnv :: String -> String -> Invocation -> Invocation setEnv var val Invocation {..} = Invocation{         environment = (var,val) : Prelude.filter ((/=var).fst) environment@@ -51,11 +58,11 @@                         -- simulate real user input and prevent Haskeline                         -- from coalescing the changes.)         -> IO [B.ByteString]-runInvocation Invocation {..} inputs-    | runInTTY = runCommandInPty prog progArgs (Just environment) inputs+runInvocation inv@Invocation {..} inputs+    | runInTTY = runCommandInPty prog (progArgs inv) (Just environment) inputs     | otherwise = do     (Just inH, Just outH, Nothing, ph)-        <- createProcess (proc prog progArgs)+        <- createProcess (proc prog (progArgs inv))                             { env = Just environment                             , std_in = CreatePipe                             , std_out = CreatePipe
tests/Unit.hs view
@@ -49,24 +49,55 @@     let i = setTerm "xterm"             Invocation {                 prog = p,-                progArgs = [],+                progModeArgs = [],+                progInputArgs = [],                 runInTTY = True,                 environment = []             }     makeTestFiles-    result <- runTestTT $ test [interactionTests i, fileStyleTests i]+    result <- runTestTT $ test+        [ interactionTests UseTerm i+        , interactionTests UseTermHandles i+        , interactionTests (UseTermHandlesWith "xterm") i+        -- dumbTests sets TERM=dumb in env to drive Haskeline into the dumb+        -- backend.  That works for UseTerm and UseTermHandles (both consult+        -- env), but not for UseTermHandlesWith — it ignores env and uses its+        -- explicit term type — so we skip it for that mode.+        , dumbTests UseTerm i+        , dumbTests UseTermHandles i+        , fileStyleTests i+        ]     when (errors result > 0 || failures result > 0) exitFailure -interactionTests :: Invocation -> Test-interactionTests i = "interaction" ~: test-    [ unicodeEncoding i-    , unicodeMovement i-    , tabCompletion i-    , incorrectInput i-    , historyTests i-    , inputChar $ setCharInput i-    , dumbTests $ setTerm "dumb" i+-- | Which haskeline Behavior the test program should run under.+data Mode+    = UseTerm                     -- ^ runInputT (defaultBehavior)+    | UseTermHandles              -- ^ useTermHandles on /dev/tty+    | UseTermHandlesWith String   -- ^ useTermHandlesWith TERM on /dev/tty++modeName :: Mode -> String+modeName UseTerm                 = "useTerm"+modeName UseTermHandles          = "useTermHandles"+modeName (UseTermHandlesWith t)  = "useTermHandlesWith=" ++ t++setMode :: Mode -> Invocation -> Invocation+setMode m i = i { progModeArgs = modeArgs m }+  where+    modeArgs UseTerm                 = []+    modeArgs UseTermHandles          = ["--mode", "useTermHandles"]+    modeArgs (UseTermHandlesWith t)  = ["--mode", "useTermHandlesWith", "--term-type", t]++interactionTests :: Mode -> Invocation -> Test+interactionTests m i = ("interaction (" ++ modeName m ++ ")") ~: test+    [ unicodeEncoding ii+    , unicodeMovement ii+    , tabCompletion ii+    , incorrectInput ii+    , historyTests ii+    , inputChar $ setCharInput ii     ]+  where+    ii = setMode m i  unicodeEncoding :: Invocation -> Test unicodeEncoding i = "Unicode encoding (valid)" ~:@@ -195,7 +226,7 @@     ]  setCharInput :: Invocation -> Invocation-setCharInput i = i { progArgs = ["chars"] }+setCharInput i = i { progInputArgs = ["chars"] }   fileStyleTests :: Invocation -> Test@@ -255,18 +286,18 @@ -- If all the above tests work for the terminfo backend, -- then we just need to make sure the dumb term plugs into everything -- correctly, i.e., encodes the input/output and doesn't double-encode.-dumbTests :: Invocation -> Test-dumbTests i = "dumb term" ~:-    [ "line input" ~: utf8Test i+dumbTests :: Mode -> Invocation -> Test+dumbTests m i = ("dumb term (" ++ modeName m ++ ")") ~:+    [ "line input" ~: utf8Test ii         [ utf8 "xαβγy" ]         [ prompt' 0, utf8 "xαβγy" ]-    , "line input wide movement" ~: utf8Test i+    , "line input wide movement" ~: utf8Test ii         [ utf8 wideChar, raw [1], raw [5] ]         [ prompt' 0, utf8 wideChar         , utf8 (T.replicate 60 "\b")         , utf8 wideChar         ]-    , "line char input" ~: utf8Test (setCharInput i)+    , "line char input" ~: utf8Test (setCharInput ii)         [utf8 "xαβ"]         [ prompt' 0, utf8 "x" <> dumbnl <> output 0 (utf8 "x")           <> prompt' 1 <> utf8 "α" <> dumbnl <> output 1 (utf8 "α")@@ -275,6 +306,7 @@         ]     ]   where+    ii = setMode m $ setTerm "dumb" i     dumbnl = raw [13,10]     wideChar = T.concat $ replicate 10 $ "안기영"