repline 0.3.0.0 → 0.4.0.0
raw patch · 9 files changed
+588/−285 lines, 9 filesdep ~basedep ~containersdep ~mtlPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: base, containers, mtl, process
API changes (from Hackage documentation)
+ System.Console.Repline: Continue :: ExitDecision
+ System.Console.Repline: Exit :: ExitDecision
+ System.Console.Repline: MultiLine :: MultiLine
+ System.Console.Repline: SingleLine :: MultiLine
+ System.Console.Repline: [finaliser] :: ReplOpts m -> HaskelineT m ExitDecision
+ System.Console.Repline: [multilineCommand] :: ReplOpts m -> Maybe String
+ System.Console.Repline: data ExitDecision
+ System.Console.Repline: data MultiLine
+ System.Console.Repline: instance GHC.Classes.Eq System.Console.Repline.MultiLine
+ System.Console.Repline: instance GHC.Show.Show System.Console.Repline.MultiLine
- System.Console.Repline: ReplOpts :: HaskelineT m String -> Command (HaskelineT m) -> Options (HaskelineT m) -> Maybe Char -> CompleterStyle m -> HaskelineT m () -> ReplOpts m
+ System.Console.Repline: ReplOpts :: (MultiLine -> HaskelineT m String) -> Command (HaskelineT m) -> Options (HaskelineT m) -> Maybe Char -> Maybe String -> CompleterStyle m -> HaskelineT m () -> HaskelineT m ExitDecision -> ReplOpts m
- System.Console.Repline: [banner] :: ReplOpts m -> HaskelineT m String
+ System.Console.Repline: [banner] :: ReplOpts m -> MultiLine -> HaskelineT m String
- System.Console.Repline: evalRepl :: (MonadMask m, MonadIO m) => HaskelineT m String -> Command (HaskelineT m) -> Options (HaskelineT m) -> Maybe Char -> CompleterStyle m -> HaskelineT m a -> m ()
+ System.Console.Repline: 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 ()
- System.Console.Repline: type Cmd m = [String] -> m ()
+ System.Console.Repline: type Cmd m = String -> m ()
Files
- ChangeLog.md +6/−0
- LICENSE +1/−1
- README.md +66/−0
- examples/Multiline.hs +84/−0
- examples/Prefix.hs +10/−7
- examples/Simple.hs +26/−5
- examples/Stateful.hs +20/−9
- repline.cabal +83/−70
- src/System/Console/Repline.hs +292/−193
ChangeLog.md view
@@ -1,6 +1,12 @@ HEAD ==== +0.4.0.0+=======++- Add multi-line input support+- Add finaliser option to control REPL exit on <Ctrl-D>+ 0.3.0.0 =======
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2016-2019 Stephen Diehl+Copyright (c) 2016-2020 Stephen Diehl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
README.md view
@@ -18,6 +18,72 @@ * [Simple](examples/Simple.hs) * [Prefix](examples/Prefix.hs) * [Stateful](examples/Stateful.hs)+* [Multiline](examples/MultiLine.hs)++Migration from 0.3.x+--------------------++This release adds two parameters to the `ReplOpts` constructor and `evalRepl` function.++* `finaliser`+* `multilineCommand`++The `finaliser` function is a function run when the Repl monad is is exited.++```haskell+-- | Decide whether to exit the REPL or not+data ExitDecision+ = Continue -- | Keep the REPL open+ | Exit -- | Close the REPL and exit+```++For example:++```haskell+final :: Repl ExitDecision+final = do+ liftIO $ putStrLn "Goodbye!"+ return Exit+```++The `multilineCommand` argument takes a command which invokes a multiline edit+mode in which the user can paste/enter text across multiple lines terminating+with a Ctrl-D / EOF. This can be used in conjunction with a customBanner+function to indicate the entry mode.++```haskell+customBanner :: MultiLine -> Repl String+customBanner SingleLine = pure ">>> "+customBanner MultiLine = pure "| "+```++See [Multiline](examples/MultiLine.hs) for a complete example.++Migration from 0.2.x+--------------------++The underlying `haskeline` library that provides readline support had a breaking+API change in 0.8.0.0 which removed the bespoke+`System.Console.Haskeline.MonadException` module in favour of using the+`exceptions` package. This is a *much* better design and I strongly encourage+upgrading. To migrate simply add the following bounds to your Cabal file.++```yaml+build-depends:+ repline >= 0.3.0.0+ haskeline >= 0.8.0.0+```++You may also need to add the following to your `stack.yaml` file if using Stack.++```yaml+resolver: lts-15.0+packages:+ - .+extra-deps:+ - haskeline-0.8.0.0+ - repline-0.3.0.0+``` Usage -----
+ examples/Multiline.hs view
@@ -0,0 +1,84 @@+module Main (main, repl) where++import Control.Monad.Trans+import Data.List (isPrefixOf)+import System.Console.Repline+import System.Process (callCommand)++type Repl a = HaskelineT IO a++-- Evaluation : handle each line user inputs+cmd :: String -> Repl ()+cmd input = liftIO $ print input++-- Commands+help :: [String] -> Repl ()+help args = liftIO $ print $ "Help: " ++ show args++say :: String -> Repl ()+say args = do+ _ <- liftIO $ callCommand $ "cowsay" ++ " " ++ args+ return ()++load :: FilePath -> Repl ()+load args = do+ contents <- liftIO $ readFile args+ liftIO $ putStrLn contents++-- Options+opts :: [(String, String -> Repl ())]+opts =+ [ ("help", help . words), -- :help+ ("load", load), -- :load+ ("say", say) -- :say+ ]++-- Tab Completion: return a completion for partial words entered+completer :: Monad m => WordCompleter m+completer n = do+ let names = ["kirk", "spock", "mccoy"]+ return $ filter (isPrefixOf n) names++-- Completer+defaultMatcher :: (MonadIO m) => [([Char], CompletionFunc m)]+defaultMatcher =+ [ -- Commands+ (":load", fileCompleter),+ (":help", wordCompleter completer)+ ]++byWord :: Monad m => WordCompleter m+byWord n = do+ let names = fmap ((":" <>) . fst) opts+ return $ filter (isPrefixOf n) names++-- Initialiser function+ini :: Repl ()+ini = liftIO $ putStrLn "Welcome!"++-- Finaliser function+final :: Repl ExitDecision+final = do+ liftIO $ putStrLn "Goodbye!"+ return Exit++customBanner :: MultiLine -> Repl String+customBanner SingleLine = pure ">>> "+customBanner MultiLine = pure "| "++repl :: IO ()+repl =+ evalReplOpts $+ ReplOpts+ { banner = customBanner,+ command = cmd,+ options = opts,+ prefix = Just ':',+ multilineCommand = Just "paste",+ tabComplete = (Prefix (wordCompleter byWord) defaultMatcher),+ initialiser = ini,+ finaliser = final+ }++main :: IO ()+main = pure ()
examples/Prefix.hs view
@@ -32,17 +32,17 @@ let names = ["picard", "riker", "data", ":file", ":holiday"] return $ filter (isPrefixOf n) names -files :: [String] -> Repl ()+files :: String -> Repl () files args = liftIO $ do- contents <- readFile (unwords args)+ contents <- readFile args putStrLn contents -holidays :: [String] -> Repl ()-holidays [] = liftIO $ putStrLn "Enter a holiday."+holidays :: String -> Repl ()+holidays "" = liftIO $ putStrLn "Enter a holiday." holidays xs = liftIO $ do- putStrLn $ "Happy " ++ unwords xs ++ "!"+ putStrLn $ "Happy " ++ xs ++ "!" -opts :: [(String, [String] -> Repl ())]+opts :: [(String, String -> Repl ())] opts = [ ("file", files), ("holiday", holidays)@@ -51,8 +51,11 @@ inits :: Repl () inits = return () +final :: Repl ExitDecision+final = return Exit+ repl :: IO ()-repl = evalRepl (pure ">>> ") cmd opts Nothing (Prefix (wordCompleter byWord) defaultMatcher) inits+repl = evalRepl (const $ pure ">>> ") cmd opts Nothing Nothing (Prefix (wordCompleter byWord) defaultMatcher) inits final main :: IO () main = pure ()
examples/Simple.hs view
@@ -21,22 +21,43 @@ help :: [String] -> Repl () help args = liftIO $ print $ "Help: " ++ show args -say :: [String] -> Repl ()+say :: String -> Repl () say args = do- _ <- liftIO $ callCommand $ "cowsay" ++ " " ++ (unwords args)+ _ <- liftIO $ callCommand $ "cowsay" ++ " " ++ args return () -opts :: [(String, [String] -> Repl ())]+opts :: [(String, String -> Repl ())] opts =- [ ("help", help), -- :help+ [ ("help", help . words), -- :help ("say", say) -- :say ] ini :: Repl () ini = liftIO $ putStrLn "Welcome!" +final :: Repl ExitDecision+final = do+ liftIO $ putStrLn "Goodbye!"+ return Exit++repl_alt :: IO ()+repl_alt = evalReplOpts $ ReplOpts+ { banner = const $ pure ">>> "+ , command = cmd+ , options = opts+ , prefix = Just ':'+ , multilineCommand = Just "paste"+ , tabComplete = (Word0 completer)+ , initialiser = ini+ , finaliser = final+ }++customBanner :: MultiLine -> Repl String+customBanner SingleLine = pure ">>> "+customBanner MultiLine = pure "| "+ repl :: IO ()-repl = evalRepl (pure ">>> ") cmd opts (Just ':') (Word0 completer) ini+repl = evalRepl (const $ pure ">>> ") cmd opts (Just ':') (Just "paste") (Word0 completer) ini final main :: IO () main = pure ()
examples/Stateful.hs view
@@ -6,6 +6,7 @@ import Control.Monad.State.Strict import Data.List (isPrefixOf)+import Data.Monoid import qualified Data.Set as Set import System.Console.Repline @@ -13,18 +14,18 @@ -- Stateful Completion ------------------------------------------------------------------------------- -type IState = Set.Set String+type IState = (Int, Set.Set String) type Repl a = HaskelineT (StateT IState IO) a -- Evaluation cmd :: String -> Repl ()-cmd input = modify $ \s -> Set.insert input s+cmd input = modify . fmap $ \s -> Set.insert input s -- Completion comp :: (Monad m, MonadState IState m) => WordCompleter m comp n = do- ns <- get+ (c, ns) <- get return $ filter (isPrefixOf n) (Set.toList ns) -- Commands@@ -32,22 +33,32 @@ help args = liftIO $ print $ "Help!" ++ show args puts :: [String] -> Repl ()-puts args = modify $ \s -> Set.union s (Set.fromList args)+puts args = modify . fmap $ \s -> Set.union s (Set.fromList args) -opts :: [(String, [String] -> Repl ())]+opts :: [(String, String -> Repl ())] opts =- [ ("help", help), -- :help- ("puts", puts) -- :puts+ [ ("help", help . words), -- :help+ ("puts", puts . words) -- :puts ] ini :: Repl () ini = return () +final :: Repl ExitDecision+final = do+ (count, s) <- get+ if count == 0+ then return Exit+ else do+ liftIO . putStrLn $ "Exit in " <> show count <> "..."+ put (count - 1, s)+ return Continue+ -- Tab completion inside of StateT repl :: IO () repl =- flip evalStateT Set.empty $- evalRepl (pure ">>> ") cmd opts Nothing (Word comp) ini+ flip evalStateT (3, Set.empty) $+ evalRepl (const $ pure ">>> ") cmd opts Nothing Nothing (Word comp) ini final main :: IO () main = pure ()
repline.cabal view
@@ -1,35 +1,35 @@-name: repline-version: 0.3.0.0-synopsis: Haskeline wrapper for GHCi-like REPL interfaces.-license: MIT-license-file: LICENSE-author: Stephen Diehl-maintainer: stephen.m.diehl@gmail.com-copyright: 2014-2019 Stephen Diehl-category: User Interfaces-build-type: Simple-extra-source-files: README.md-cabal-version: >=1.10+name: repline+version: 0.4.0.0+synopsis: Haskeline wrapper for GHCi-like REPL interfaces.+license: MIT+license-file: LICENSE+author: Stephen Diehl+maintainer: stephen.m.diehl@gmail.com+copyright: 2014-2020 Stephen Diehl+category: User Interfaces+build-type: Simple+extra-source-files: README.md+cabal-version: >=1.10 tested-with:- GHC == 7.6.1,- GHC == 7.6.2,- GHC == 7.6.3,- GHC == 7.8.1,- GHC == 7.8.2,- GHC == 7.8.3,- GHC == 7.8.4,- GHC == 7.10.1,- GHC == 7.10.2,- GHC == 7.10.3,- GHC == 8.0.1,- GHC == 8.2.1,- GHC == 8.4.1,- GHC == 8.6.1,- GHC == 8.8.1- GHC == 8.10.1-homepage: https://github.com/sdiehl/repline-bug-Reports: https://github.com/sdiehl/repline/issues+ GHC ==7.6.1+ || ==7.6.2+ || ==7.6.3+ || ==7.8.1+ || ==7.8.2+ || ==7.8.3+ || ==7.8.4+ || ==7.10.1+ || ==7.10.2+ || ==7.10.3+ || ==8.0.1+ || ==8.2.1+ || ==8.4.1+ || ==8.6.1+ || ==8.8.1+ || ==8.10.1 +homepage: https://github.com/sdiehl/repline+bug-reports: https://github.com/sdiehl/repline/issues description: Haskeline wrapper for GHCi-like REPL interfaces. Composable with normal mtl transformers. @@ -37,52 +37,65 @@ README.md ChangeLog.md -Source-Repository head- Type: git- Location: git@github.com:sdiehl/repline.git+source-repository head+ type: git+ location: git@github.com:sdiehl/repline.git library- hs-source-dirs: src- exposed-modules: System.Console.Repline- ghc-options: -Wall+ hs-source-dirs: src+ exposed-modules: System.Console.Repline+ ghc-options: -Wall build-depends:- base >= 4.6 && <5.0,- haskeline >= 0.8 && <0.9,- containers >= 0.5 && <0.7,- exceptions >= 0.10 && <0.11,- mtl >= 2.2 && <2.3,- process >= 1.2 && <2.0- if !impl(ghc >= 8.0)- Build-Depends: fail >= 4.9 && <4.10- default-language: Haskell2010+ base >=4.6 && <5.0+ , containers >=0.5 && <0.7+ , exceptions >=0.10 && <0.11+ , haskeline >=0.8 && <0.9+ , mtl >=2.2 && <2.3+ , process >=1.2 && <2.0 + if !impl(ghc >=8.0)+ build-depends: fail ==4.9.*++ default-language: Haskell2010+ test-suite prefix- type: exitcode-stdio-1.0- main-is: examples/Prefix.hs- default-language: Haskell2010- build-depends: - base,- mtl,- containers,- repline+ type: exitcode-stdio-1.0+ main-is: examples/Prefix.hs+ default-language: Haskell2010+ build-depends:+ base+ , containers+ , mtl+ , repline test-suite simple- type: exitcode-stdio-1.0- main-is: examples/Simple.hs- default-language: Haskell2010- build-depends: - base,- mtl,- containers,- process,- repline+ type: exitcode-stdio-1.0+ main-is: examples/Simple.hs+ default-language: Haskell2010+ build-depends:+ base+ , containers+ , mtl+ , process+ , repline test-suite stateful- type: exitcode-stdio-1.0- main-is: examples/Stateful.hs- default-language: Haskell2010- build-depends: - base,- mtl,- containers,- repline+ type: exitcode-stdio-1.0+ main-is: examples/Stateful.hs+ default-language: Haskell2010+ build-depends:+ base+ , containers+ , mtl+ , repline++test-suite multiline+ type: exitcode-stdio-1.0+ main-is: examples/Multiline.hs+ default-language: Haskell2010+ build-depends:+ base+ , containers+ , mtl+ , process+ , repline
src/System/Console/Repline.hs view
@@ -1,156 +1,182 @@-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE NoMonomorphismRestriction #-} -{- |--Repline exposes an additional monad transformer on top of Haskeline called 'HaskelineT'. It simplifies several-aspects of composing Haskeline with State and Exception monads in modern versions of mtl.--> type Repl a = HaskelineT IO a--The evaluator 'evalRepl' evaluates a 'HaskelineT' monad transformer by constructing a shell with several-custom functions and evaluating it inside of IO:-- * Commands: Handled on ordinary input.-- * Completions: Handled when tab key is pressed.-- * Options: Handled when a command prefixed by a prefix character is entered.-- * Command prefix character: Optional command prefix ( passing Nothing ignores the Options argument ).-- * Banner: Text Displayed at initialization.-- * Initializer: Run at initialization.--A simple evaluation function might simply echo the output back to the screen.--> -- Evaluation : handle each line user inputs-> cmd :: String -> Repl ()-> cmd input = liftIO $ print input--Several tab completion options are available, the most common is the 'WordCompleter' which completes on single-words separated by spaces from a list of matches. The internal logic can be whatever is required and can also-access a StateT instance to query application state.--> -- Tab Completion: return a completion for partial words entered-> completer :: Monad m => WordCompleter m-> completer n = do-> let names = ["kirk", "spock", "mccoy"]-> return $ filter (isPrefixOf n) names--Input which is prefixed by a colon (commands like \":type\" and \":help\") queries an association list of-functions which map to custom logic. The function takes a space-separated list of augments in it's first-argument. If the entire line is desired then the 'unwords' function can be used to concatenate.--> -- Commands-> help :: [String] -> Repl ()-> help args = liftIO $ print $ "Help: " ++ show args->-> say :: [String] -> Repl ()-> say args = do-> _ <- liftIO $ system $ "cowsay" ++ " " ++ (unwords args)-> return ()--Now we need only map these functions to their commands.--> options :: [(String, [String] -> Repl ())]-> options = [-> ("help", help) -- :help-> , ("say", say) -- :say-> ]--The banner function is simply an IO action that is called at the start of the shell.--> ini :: Repl ()-> ini = liftIO $ putStrLn "Welcome!"--Putting it all together we have a little shell.--> main :: IO ()-> main = evalRepl (pure ">>> ") cmd options (Just ':') (Word completer) ini--Putting this in a file we can test out our cow-trek shell.--> $ runhaskell Main.hs-> Welcome!-> >>> <TAB>-> kirk spock mccoy->-> >>> k<TAB>-> kirk->-> >>> spam-> "spam"->-> >>> :say Hello Haskell-> _______________-> < Hello Haskell >-> ----------------> \ ^__^-> \ (oo)\_______-> (__)\ )\/\-> ||----w |-> || ||--See <https://github.com/sdiehl/repline> for more examples.---}--module System.Console.Repline (- -- * Repline Monad- HaskelineT,- runHaskelineT,-- -- * Toplevel- evalRepl,- ReplOpts(..),- evalReplOpts,-- -- * Repline Types- Cmd,- Options,- WordCompleter,- LineCompleter,- CompleterStyle(..),- Command,+-- |+--+-- Repline exposes an additional monad transformer on top of Haskeline called 'HaskelineT'. It simplifies several+-- aspects of composing Haskeline with State and Exception monads in modern versions of mtl.+--+-- > type Repl a = HaskelineT IO a+--+-- The evaluator 'evalRepl' evaluates a 'HaskelineT' monad transformer by constructing a shell with several+-- custom functions and evaluating it inside of IO:+--+-- * Commands: Handled on ordinary input.+--+-- * Completions: Handled when tab key is pressed.+--+-- * Options: Handled when a command prefixed by a prefix character is entered.+--+-- * Command prefix character: Optional command prefix ( passing Nothing ignores the Options argument ).+--+-- * Multi-line command: Optional command name that switches to a multi-line input. (Press <Ctrl-D> to exit and commit the multi-line input). Passing Nothing disables multi-line input support.+--+-- * Banner: Text Displayed at initialisation. It takes an argument so it can take into account if the current line is part of a multi-line input.+--+-- * Initialiser: Run at initialisation.+--+-- * Finaliser: Run on <Ctrl-D>, it can be used to output a custom exit message or to choose whether to exit or not depending on the application state+--+-- A simple evaluation function might simply echo the output back to the screen.+--+-- > -- Evaluation : handle each line user inputs+-- > cmd :: String -> Repl ()+-- > cmd input = liftIO $ print input+--+-- Several tab completion options are available, the most common is the 'WordCompleter' which completes on single+-- words separated by spaces from a list of matches. The internal logic can be whatever is required and can also+-- access a StateT instance to query application state.+--+-- > -- Tab Completion: return a completion for partial words entered+-- > completer :: Monad m => WordCompleter m+-- > completer n = do+-- > let names = ["kirk", "spock", "mccoy"]+-- > return $ filter (isPrefixOf n) names+--+-- Input which is prefixed by a colon (commands like \":type\" and \":help\") queries an association list of+-- functions which map to custom logic. The function takes a space-separated list of augments in it's first+-- argument. If the entire line is desired then the 'unwords' function can be used to concatenate.+--+-- > -- Commands+-- > help :: [String] -> Repl ()+-- > help args = liftIO $ print $ "Help: " ++ show args+-- >+-- > say :: [String] -> Repl ()+-- > say args = do+-- > _ <- liftIO $ system $ "cowsay" ++ " " ++ (unwords args)+-- > return ()+--+-- Now we need only map these functions to their commands.+--+-- > options :: [(String, [String] -> Repl ())]+-- > options = [+-- > ("help", help) -- :help+-- > , ("say", say) -- :say+-- > ]+--+-- The initialiser function is simply an IO action that is called at the start of the shell.+--+-- > ini :: Repl ()+-- > ini = liftIO $ putStrLn "Welcome!"+--+-- The finaliser function is an IO action that is called at the end of the shell.+--+-- final :: Repl ExitDecision+-- final = do+-- liftIO $ putStrLn "Goodbye!"+-- return Exit+--+-- Putting it all together we have a little shell.+--+-- > main :: IO ()+-- > main = evalRepl (pure ">>> ") cmd options (Just ':') (Word completer) ini+--+-- Alternatively instead of initialising the repl from position arguments you+-- can pass the 'ReplOpts' record with explicitly named arguments.+--+-- > main_alt :: IO ()+-- > main_alt = evalReplOpts $ ReplOpts+-- > { banner = const (pure ">>> ")+-- > , command = cmd+-- > , options = opts+-- > , prefix = Just ':'+-- > , multilineCommand = Nothing+-- > , tabComplete = (Word0 completer)+-- > , initialiser = ini+-- > , finaliser = final+-- > }+--+-- Putting this in a file we can test out our cow-trek shell.+--+-- > $ runhaskell Main.hs+-- > Welcome!+-- > >>> <TAB>+-- > kirk spock mccoy+-- >+-- > >>> k<TAB>+-- > kirk+-- >+-- > >>> spam+-- > "spam"+-- >+-- > >>> :say Hello Haskell+-- > _______________+-- > < Hello Haskell >+-- > ---------------+-- > \ ^__^+-- > \ (oo)\_______+-- > (__)\ )\/\+-- > ||----w |+-- > || ||+--+-- See <https://github.com/sdiehl/repline> for more examples.+module System.Console.Repline+ ( -- * Repline Monad+ HaskelineT,+ runHaskelineT, - -- * Completers- CompletionFunc, -- re-export- fallbackCompletion,+ -- * Toplevel+ evalRepl,+ ReplOpts (..),+ evalReplOpts, - wordCompleter,- listCompleter,- fileCompleter,- listWordCompleter,- runMatcher,- trimComplete,+ -- * Repline Types+ Cmd,+ Options,+ WordCompleter,+ LineCompleter,+ CompleterStyle (..),+ Command,+ ExitDecision (..),+ MultiLine (..), - -- * Utilities- abort,- tryAction,- dontCrash,-) where+ -- * Completers+ CompletionFunc, -- re-export+ fallbackCompletion,+ wordCompleter,+ listCompleter,+ fileCompleter,+ listWordCompleter,+ runMatcher,+ trimComplete, -import System.Console.Haskeline.Completion-import qualified System.Console.Haskeline as H+ -- * Utilities+ abort,+ tryAction,+ dontCrash,+ )+where -import Data.List (isPrefixOf)+import Control.Monad.Catch import Control.Monad.Fail as Fail-import Control.Monad.State.Strict import Control.Monad.Reader-import Control.Monad.Catch+import Control.Monad.State.Strict+import Data.List (isPrefixOf)+import qualified System.Console.Haskeline as H+import System.Console.Haskeline.Completion ------------------------------------------------------------------------------- -- Haskeline Transformer ------------------------------------------------------------------------------- +-- | Monad transformer for readline input newtype HaskelineT (m :: * -> *) a = HaskelineT {unHaskeline :: H.InputT m a} deriving ( Monad,@@ -172,14 +198,14 @@ class MonadCatch m => MonadHaskeline m where getInputLine :: String -> m (Maybe String) getInputChar :: String -> m (Maybe Char)- outputStr :: String -> m ()- outputStrLn :: String -> m ()+ 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+ outputStr = H.outputStr+ outputStrLn = H.outputStrLn instance Fail.MonadFail m => Fail.MonadFail (HaskelineT m) where fail = lift . Fail.fail@@ -189,21 +215,32 @@ put = lift . put instance MonadReader r m => MonadReader r (HaskelineT m) where- ask = lift ask+ 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+ outputStr = lift . outputStr+ outputStrLn = lift . outputStrLn ------------------------------------------------------------------------------- -- Repl ------------------------------------------------------------------------------- -- | Command function synonym-type Cmd m = [String] -> m ()+--+-- The argument corresponds to the arguments of the command, it may contain+-- spaces or newlines (when input is multi-line).+--+-- For example, with prefix @':'@ and command @"command"@ the argument 'String' for:+--+-- @+-- :command some arguments+-- @+--+-- is @"some arguments"@+type Cmd m = String -> m () -- | Options function synonym type Options m = [(String, Cmd m)]@@ -225,46 +262,73 @@ -- | Catch all toplevel failures. dontCrash :: (MonadIO m, MonadCatch m) => m () -> m ()-dontCrash m = catch m ( \ e@SomeException{} -> liftIO ( print e ))+dontCrash m = catch m (\e@SomeException {} -> liftIO (print e)) -- | Abort the current REPL loop, and continue. abort :: MonadThrow m => HaskelineT m a abort = throwM H.Interrupt -- | Completion loop.-replLoop :: (Functor m, MonadMask m, MonadIO m)- => HaskelineT m String -- ^ banner function- -> Command (HaskelineT m) -- ^ command function- -> Options (HaskelineT m) -- ^ options function- -> Maybe Char -- ^ options prefix- -> HaskelineT m ()-replLoop banner cmdM opts optsPrefix = loop+replLoop ::+ (Functor m, MonadMask m, MonadIO m) =>+ -- | Banner function+ (MultiLine -> HaskelineT m String) ->+ -- | Command function+ Command (HaskelineT m) ->+ -- | options function+ Options (HaskelineT m) ->+ -- | options prefix+ Maybe Char ->+ -- | multi-line command+ Maybe String ->+ -- | Finaliser ( runs on <Ctrl-D> )+ HaskelineT m ExitDecision ->+ HaskelineT m ()+replLoop banner cmdM opts optsPrefix multiCommand finalz = loop where loop = do- prefix <- banner+ prefix <- banner SingleLine minput <- H.handleInterrupt (return (Just "")) $ getInputLine prefix+ handleCommands minput+ handleCommands minput = case minput of- Nothing -> outputStrLn "Goodbye."+ 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 : args) -> do- let optAction = optMatcher cmd opts args+ (cmd : _)+ | Just cmd == multiCommand -> do+ outputStrLn "-- Entering multi-line mode. Press <Ctrl-D> to finish."+ loopMultiLine []+ (cmd : _) -> do+ let -- If there are any arguments, cmd is followed by a+ -- whitespace character (space, newline, ...)+ 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 () -- | Match the options.-optMatcher :: MonadHaskeline m => String -> Options m -> [String] -> m ()+optMatcher :: MonadHaskeline m => String -> Options m -> String -> m () optMatcher s [] _ = outputStrLn $ "No such command :" ++ s-optMatcher s ((x, m):xs) args+optMatcher s ((x, m) : xs) args | s `isPrefixOf` x = m args | otherwise = optMatcher s xs args @@ -272,50 +336,85 @@ -- Toplevel ------------------------------------------------------------------------------- +-- | Decide whether to exit the REPL or not+data ExitDecision+ = -- | Keep the REPL open+ Continue+ | -- | Close the REPL and exit+ Exit++-- | Context for the current line if it is part of a multi-line input or not+data MultiLine = MultiLine | SingleLine deriving (Eq, Show)+ -- | REPL Options datatype-data ReplOpts m = ReplOpts {- banner :: HaskelineT m String -- ^ Banner- , command :: Command (HaskelineT m) -- ^ Command function- , options :: Options (HaskelineT m) -- ^ Options list and commands- , prefix :: Maybe Char -- ^ Optional command prefix ( passing Nothing ignores the Options argument )- , tabComplete :: CompleterStyle m -- ^ Tab completion function- , initialiser :: HaskelineT m () -- ^ Initialiser+data ReplOpts m = ReplOpts+ { -- | Banner+ banner :: MultiLine -> HaskelineT m String,+ -- | Command function+ command :: Command (HaskelineT m),+ -- | Options list and commands+ options :: Options (HaskelineT m),+ -- | Optional command prefix ( passing Nothing ignores the Options argument )+ prefix :: Maybe Char,+ -- | Optional multi-line command ( passing Nothing disables multi-line support )+ multilineCommand :: Maybe String,+ -- | Tab completion function+ tabComplete :: CompleterStyle m,+ -- | Initialiser+ initialiser :: HaskelineT m (),+ -- | Finaliser ( runs on <Ctrl-D> )+ finaliser :: HaskelineT m ExitDecision } -- | Evaluate the REPL logic into a MonadCatch context from the ReplOpts -- configuration. evalReplOpts :: (MonadMask m, MonadIO m) => ReplOpts m -> m ()-evalReplOpts ReplOpts {..} = evalRepl- banner- command- options- prefix- tabComplete- initialiser+evalReplOpts ReplOpts {..} =+ evalRepl+ banner+ command+ options+ prefix+ multilineCommand+ tabComplete+ initialiser+ finaliser -- | Evaluate the REPL logic into a MonadCatch context.-evalRepl- :: (MonadMask m, MonadIO m) -- Terminal monad ( often IO ).- => HaskelineT m String -- ^ Banner- -> Command (HaskelineT m) -- ^ Command function- -> Options (HaskelineT m) -- ^ Options list and commands- -> Maybe Char -- ^ Optional command prefix ( passing Nothing ignores the Options argument )- -> CompleterStyle m -- ^ Tab completion function- -> HaskelineT m a -- ^ Initialiser- -> m ()-evalRepl banner cmd opts optsPrefix comp initz = runHaskelineT _readline (initz >> monad)+evalRepl ::+ (MonadMask m, MonadIO m) =>+ -- | Banner+ (MultiLine -> HaskelineT m String) ->+ -- | Command function+ Command (HaskelineT m) ->+ -- | Options list and commands+ Options (HaskelineT m) ->+ -- | Optional command prefix ( passing Nothing ignores the Options argument )+ Maybe Char ->+ -- | Optional multi-line command ( passing Nothing disables multi-line support )+ Maybe String ->+ -- | Tab completion function+ CompleterStyle m ->+ -- | Initialiser+ HaskelineT m a ->+ -- | Finaliser ( runs on Ctrl-D )+ HaskelineT m ExitDecision ->+ m ()+evalRepl banner cmd opts optsPrefix multiCommand comp initz finalz = runHaskelineT _readline (initz >> monad) where- monad = replLoop banner cmd opts optsPrefix- _readline = H.Settings- { H.complete = mkCompleter comp- , H.historyFile = Just ".history"- , H.autoAddHistory = True- }+ monad = replLoop banner cmd opts optsPrefix multiCommand finalz+ _readline =+ H.Settings+ { H.complete = mkCompleter comp,+ H.historyFile = Just ".history",+ H.autoAddHistory = True+ } --------------------------------------------------------------------------------+------------------------------------------------------------------------------ -- Completions ------------------------------------------------------------------------------- +-- | Tab completer types data CompleterStyle m = -- | Completion function takes single word. Word (WordCompleter m)@@ -329,10 +428,10 @@ Prefix (CompletionFunc m) [(String, CompletionFunc m)]- -- | Combine two completions- | Combine (CompleterStyle m) (CompleterStyle m)- -- | Custom completion- | Custom (CompletionFunc m)+ | -- | Combine two completions+ Combine (CompleterStyle m) (CompleterStyle m)+ | -- | Custom completion+ Custom (CompletionFunc m) -- | Make a completer function from a completion type mkCompleter :: MonadIO m => CompleterStyle m -> CompletionFunc m