{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
module System.Console.Repline
(
HaskelineT,
runHaskelineT,
evalRepl,
ReplOpts (..),
evalReplOpts,
Cmd,
Options,
WordCompleter,
LineCompleter,
CompleterStyle (..),
Command,
ExitDecision (..),
MultiLine (..),
CompletionFunc,
fallbackCompletion,
wordCompleter,
listCompleter,
fileCompleter,
listWordCompleter,
runMatcher,
trimComplete,
abort,
tryAction,
dontCrash,
)
where
import Control.Monad.Catch
import Control.Monad.Fail as Fail
import Control.Monad.Reader
import Control.Monad.State.Strict
import Data.List (isPrefixOf)
import qualified System.Console.Haskeline as H
import System.Console.Haskeline.Completion
newtype HaskelineT (m :: * -> *) a = HaskelineT {unHaskeline :: H.InputT m a}
deriving
( Monad,
Functor,
Applicative,
MonadIO,
MonadFix,
MonadTrans,
MonadHaskeline,
MonadThrow,
MonadCatch,
MonadMask
)
runHaskelineT :: (MonadMask m, MonadIO m) => H.Settings m -> HaskelineT m a -> m a
runHaskelineT s m = H.runInputT s (H.withInterrupt (unHaskeline m))
class MonadCatch m => MonadHaskeline m where
getInputLine :: String -> m (Maybe String)
getInputChar :: String -> m (Maybe Char)
outputStr :: String -> m ()
outputStrLn :: String -> m ()
instance (MonadMask m, MonadIO m) => MonadHaskeline (H.InputT m) where
getInputLine = H.getInputLine
getInputChar = H.getInputChar
outputStr = H.outputStr
outputStrLn = H.outputStrLn
instance Fail.MonadFail m => Fail.MonadFail (HaskelineT m) where
fail = lift . Fail.fail
instance MonadState s m => MonadState s (HaskelineT m) where
get = lift get
put = lift . put
instance MonadReader r m => MonadReader r (HaskelineT m) where
ask = lift ask
local f (HaskelineT m) = HaskelineT $ H.mapInputT (local f) m
instance (MonadHaskeline m) => MonadHaskeline (StateT s m) where
getInputLine = lift . getInputLine
getInputChar = lift . getInputChar
outputStr = lift . outputStr
outputStrLn = lift . outputStrLn
type Cmd m = String -> m ()
type Options m = [(String, Cmd m)]
type Command m = String -> m ()
type WordCompleter m = (String -> m [String])
type LineCompleter m = (String -> String -> m [Completion])
tryAction :: (MonadMask m, MonadIO m) => HaskelineT m a -> HaskelineT m a
tryAction (HaskelineT f) = HaskelineT (H.withInterrupt loop)
where
loop = handle (\H.Interrupt -> loop) f
dontCrash :: (MonadIO m, MonadCatch m) => m () -> m ()
dontCrash m = catch m (\e@SomeException {} -> liftIO (print e))
abort :: MonadThrow m => HaskelineT m a
abort = throwM H.Interrupt
replLoop ::
(Functor m, MonadMask m, MonadIO m) =>
(MultiLine -> HaskelineT m String) ->
Command (HaskelineT m) ->
Options (HaskelineT m) ->
Maybe Char ->
Maybe String ->
HaskelineT m ExitDecision ->
HaskelineT m ()
replLoop banner cmdM opts optsPrefix multiCommand finalz = loop
where
loop = do
prefix <- banner SingleLine
minput <- H.handleInterrupt (return (Just "")) $ getInputLine prefix
handleCommands minput
handleCommands minput =
case minput of
Nothing ->
finalz >>= \case
Continue -> loop
Exit -> exit
Just "" -> loop
Just (prefix_ : cmds)
| null cmds -> handleInput [prefix_] >> loop
| Just prefix_ == optsPrefix ->
case words cmds of
[] -> loop
(cmd : _)
| Just cmd == multiCommand -> do
outputStrLn "-- Entering multi-line mode. Press <Ctrl-D> to finish."
loopMultiLine []
(cmd : _) -> do
let
arguments = drop (1 + length cmd) cmds
let optAction = optMatcher cmd opts arguments
result <- H.handleInterrupt (return Nothing) $ Just <$> optAction
maybe exit (const loop) result
Just input -> do
handleInput input
loop
loopMultiLine prevs = do
prefix <- banner MultiLine
minput <- H.handleInterrupt (return (Just "")) $ getInputLine prefix
case minput of
Nothing -> handleCommands . Just . unlines $ reverse prevs
Just x -> loopMultiLine $ x : prevs
handleInput input = H.handleInterrupt exit $ cmdM input
exit = return ()
optMatcher :: MonadHaskeline m => String -> Options m -> String -> m ()
optMatcher s [] _ = outputStrLn $ "No such command :" ++ s
optMatcher s ((x, m) : xs) args
| s `isPrefixOf` x = m args
| otherwise = optMatcher s xs args
data ExitDecision
=
Continue
|
Exit
data MultiLine = MultiLine | SingleLine deriving (Eq, Show)
data ReplOpts m = ReplOpts
{
banner :: MultiLine -> HaskelineT m String,
command :: Command (HaskelineT m),
options :: Options (HaskelineT m),
prefix :: Maybe Char,
multilineCommand :: Maybe String,
tabComplete :: CompleterStyle m,
initialiser :: HaskelineT m (),
finaliser :: HaskelineT m ExitDecision
}
evalReplOpts :: (MonadMask m, MonadIO m) => ReplOpts m -> m ()
evalReplOpts ReplOpts {..} =
evalRepl
banner
command
options
prefix
multilineCommand
tabComplete
initialiser
finaliser
evalRepl ::
(MonadMask m, MonadIO m) =>
(MultiLine -> HaskelineT m String) ->
Command (HaskelineT m) ->
Options (HaskelineT m) ->
Maybe Char ->
Maybe String ->
CompleterStyle m ->
HaskelineT m a ->
HaskelineT m ExitDecision ->
m ()
evalRepl banner cmd opts optsPrefix multiCommand comp initz finalz = runHaskelineT _readline (initz >> monad)
where
monad = replLoop banner cmd opts optsPrefix multiCommand finalz
_readline =
H.Settings
{ H.complete = mkCompleter comp,
H.historyFile = Just ".history",
H.autoAddHistory = True
}
data CompleterStyle m
=
Word (WordCompleter m)
|
Word0 (WordCompleter m)
|
Cursor (LineCompleter m)
|
File
|
Prefix
(CompletionFunc m)
[(String, CompletionFunc m)]
|
Combine (CompleterStyle m) (CompleterStyle m)
|
Custom (CompletionFunc m)
mkCompleter :: MonadIO m => CompleterStyle m -> CompletionFunc m
mkCompleter (Word f) = completeWord (Just '\\') " \t()[]" (_simpleComplete f)
mkCompleter (Word0 f) = completeWord (Just '\\') " \t()[]" (_simpleCompleteNoSpace f)
mkCompleter (Cursor f) = completeWordWithPrev (Just '\\') " \t()[]" (unRev0 f)
mkCompleter File = completeFilename
mkCompleter (Prefix def opts) = runMatcher opts def
mkCompleter (Combine a b) = fallbackCompletion (mkCompleter a) (mkCompleter b)
mkCompleter (Custom f) = f
unRev0 :: LineCompleter m -> LineCompleter m
unRev0 f x = f (reverse x)
trimComplete :: String -> Completion -> Completion
trimComplete prefix (Completion a b c) = Completion (drop (length prefix) a) b c
_simpleComplete :: (Monad m) => (String -> m [String]) -> String -> m [Completion]
_simpleComplete f word = map simpleCompletion <$> f word
_simpleCompleteNoSpace :: (Monad m) => (String -> m [String]) -> String -> m [Completion]
_simpleCompleteNoSpace f word = map completionNoSpace <$> f word
completionNoSpace :: String -> Completion
completionNoSpace str = Completion str str False
wordCompleter :: Monad m => WordCompleter m -> CompletionFunc m
wordCompleter f (start, n) = completeWord (Just '\\') " \t()[]" (_simpleComplete f) (start, n)
listCompleter :: Monad m => [String] -> CompletionFunc m
listCompleter names (start, n) = completeWord (Just '\\') " \t()[]" (_simpleComplete (completeAux names)) (start, n)
listWordCompleter :: Monad m => [String] -> WordCompleter m
listWordCompleter = completeAux
fileCompleter :: MonadIO m => CompletionFunc m
fileCompleter = completeFilename
completeAux :: Monad m => [String] -> WordCompleter m
completeAux names n = return $ filter (isPrefixOf n) names
completeMatcher ::
(Monad m) =>
CompletionFunc m ->
String ->
[(String, CompletionFunc m)] ->
CompletionFunc m
completeMatcher def _ [] args = def args
completeMatcher def [] _ args = def args
completeMatcher def s ((x, f) : xs) args
| x `isPrefixOf` s = f args
| otherwise = completeMatcher def s xs args
runMatcher ::
Monad m =>
[(String, CompletionFunc m)] ->
CompletionFunc m ->
CompletionFunc m
runMatcher opts def (start, n) =
completeMatcher def (n ++ reverse start) opts (start, n)