repline (empty) → 0.1.0.0
raw patch · 5 files changed
+416/−0 lines, 5 filesdep +basedep +containersdep +haskelinesetup-changed
Dependencies added: base, containers, haskeline, mtl
Files
- LICENSE +20/−0
- README.md +88/−0
- Setup.hs +2/−0
- repline.cabal +37/−0
- src/System/Console/Repline.hs +269/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2014 Stephen Diehl++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,88 @@+Repline+-------++Slightly higher level wrapper for creating GHCi-like REPL monads that are composable with normal MTL+transformers. Mostly exists because I got tired of implementing the same interface for simple shells over and+over, and because vanilla Haskeline has a kind of quirky API.++[](https://travis-ci.org/sdiehl/repline)++Usage+-----++```haskell+type Repl a = HaskelineT IO a++-- Evaluation : handle each line user inputs+cmd :: String -> Repl ()+cmd input = liftIO $ print input+++-- Tab Completion: return a completion for partial words entered+completer :: Monad m => WordCompleter m+completer n = do+ let names = ["kirk", "spock", "mccoy"]+ let matches = filter (isPrefixOf n) names+ return $ map simpleCompletion matches++-- Commands+help :: [String] -> Repl ()+help args = liftIO $ print $ "Help: " ++ show args++say :: [String] -> Repl ()+say args = do+ _ <- liftIO $ system $ "cowsay" ++ " " ++ (unwords args)+ return ()+++options :: [(String, [String] -> Repl ())]+options = [+ ("help", help) -- :help+ , ("say", say) -- :say+ ]+++ini :: Repl ()+ini = liftIO $ putStrLn "Welcome!"++main :: IO ()+main = evalRepl ">>> " cmd options (Word completer) ini+```++Trying it out:++```haskell+$ runhaskell Main.hs+Welcome!+>>> <TAB>+kirk spock mccoy++>>> k<TAB>+kirk++>>> spam+"spam"++>>> :say Hello Haskell+ _______________ +< Hello Haskell >+ --------------- + \ ^__^+ \ (oo)\_______+ (__)\ )\/\+ ||----w |+ || ||+```++Installation+------------++```bash+$ cabal install repline+```++License+-------++Copyright (c) 2014, Stephen Diehl+Released under the MIT License
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ repline.cabal view
@@ -0,0 +1,37 @@+name: repline+version: 0.1.0.0+license: MIT+license-file: LICENSE+author: Stephen Diehl+maintainer: stephen.m.diehl@gmail.com+copyright: 2014 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.3, GHC == 7.8.3+Bug-Reports: https://github.com/sdiehl/repline/issues++Description:+ Haskeline wrapper for GHCi-like REPL interfaces. Composable with normal mtl transformers.+Source-Repository head+ Type: git+ Location: git@github.com:sdiehl/repline.git++library+ hs-source-dirs: src+ exposed-modules: System.Console.Repline+ -- other-modules: + build-depends:+ base >= 4.6 && <4.7,+ containers >= 0.5 && <0.6,+ mtl >= 2.2 && <2.3,+ haskeline >= 0.7 && <0.8+ other-extensions:+ FlexibleInstances,+ FlexibleContexts,+ TypeSynonymInstances,+ UndecidableInstances,+ MultiParamTypeClasses,+ GeneralizedNewtypeDeriving+ default-language: Haskell2010
+ src/System/Console/Repline.hs view
@@ -0,0 +1,269 @@+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++{- |++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 colon is entered.++ * 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"]+> let matches = filter (isPrefixOf n) names+> return $ map simpleCompletion matches++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 ">>> " cmd options (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 (+ HaskelineT,+ runHaskelineT,++ Cmd,+ Options,+ WordCompleter,+ LineCompleter,+ CompleterStyle(..),+ Command,++ --ReplSettings(ReplSettings),+ --emptySettings,++ evalRepl,+ --evalReplWith,++ listFiles, -- re-export+ trimComplete,+) where++import System.Console.Haskeline.Completion+import System.Console.Haskeline.MonadException+import qualified System.Console.Haskeline as H++import Data.List (isPrefixOf)+import Control.Applicative+import Control.Monad.State.Strict++-------------------------------------------------------------------------------+-- Haskeline Transformer+-------------------------------------------------------------------------------++newtype HaskelineT (m :: * -> *) a = HaskelineT { unHaskeline :: H.InputT m a }+ deriving (Monad, Functor, Applicative, MonadIO, MonadException, MonadTrans, MonadHaskeline)++runHaskelineT :: MonadException m => H.Settings m -> HaskelineT m a -> m a+runHaskelineT s m = H.runInputT s (H.withInterrupt (unHaskeline m))++class MonadException m => MonadHaskeline m where+ getInputLine :: String -> m (Maybe String)+ getInputChar :: String -> m (Maybe Char)+ outputStr :: String -> m ()+ outputStrLn :: String -> m ()++instance MonadException m => MonadHaskeline (H.InputT m) where+ getInputLine = H.getInputLine+ getInputChar = H.getInputChar+ outputStr = H.outputStr+ outputStrLn = H.outputStrLn++instance MonadState s m => MonadState s (HaskelineT m) where+ get = lift get+ put = lift . put++instance (MonadHaskeline m) => MonadHaskeline (StateT s m) where+ getInputLine = lift . getInputLine+ getInputChar = lift . getInputChar+ outputStr = lift . outputStr+ outputStrLn = lift . outputStrLn++-------------------------------------------------------------------------------+-- Repl+-------------------------------------------------------------------------------++type Cmd m = [String] -> m ()+type Options m = [(String, Cmd m)]+type Command m = String -> m ()++type WordCompleter m = (String -> m [Completion])+type LineCompleter m = (String -> String -> m [Completion])++data ReplSettings m = ReplSettings+ { _cmd :: Command (HaskelineT m) -- ^ Command function+ , _opts :: Options (HaskelineT m) -- ^ Options map+ , _compstyle :: CompleterStyle m -- ^ Internal completer function+ , _init :: m () -- ^ Initializer function.+ , _banner :: String -- ^ Banner string+ }++emptySettings :: MonadHaskeline m => ReplSettings m+emptySettings = ReplSettings (const $ return ()) [] File (return ()) ""++-- | Wrap a HasklineT action so that if an interrupt is thrown the shell continues as normal.+tryAction :: MonadException m => HaskelineT m a -> HaskelineT m a+tryAction (HaskelineT f) = HaskelineT (H.withInterrupt loop)+ where loop = handle (\H.Interrupt -> loop) f++-- | Completion loop.+replLoop :: MonadException m+ => String+ -> Command (HaskelineT m)+ -> Options (HaskelineT m)+ -> HaskelineT m ()+replLoop banner cmdM opts = loop+ where+ loop = do+ minput <- H.handleInterrupt (return (Just "")) $ getInputLine banner+ case minput of+ Nothing -> outputStrLn "Goodbye."++ Just "" -> loop+ Just ":" -> loop++ Just (':' : cmds) -> do+ let (cmd:args) = words cmds+ optMatcher cmd opts args+ loop++ Just input -> do+ H.handleInterrupt (liftIO $ putStrLn "Ctrl-C") $ cmdM input+ loop++-- | Match the options.+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++-- | Evaluate the REPL logic into a MonadException context.+evalRepl :: MonadException m -- Terminal monad ( often IO ).+ => String -- ^ Banner+ -> Command (HaskelineT m) -- ^ Command function+ -> Options (HaskelineT m) -- ^ Options list and commands+ -> CompleterStyle m -- ^ Tab completion function+ -> HaskelineT m a -- ^ Initializer+ -> m ()+evalRepl banner cmd opts comp initz = runHaskelineT _readline (initz >> monad)+ where+ monad = replLoop banner cmd opts+ _readline = H.Settings+ { H.complete = mkCompleter comp+ , H.historyFile = Just ".history"+ , H.autoAddHistory = True+ }++-- | Evaluate the REPL logic with the settings record.+evalReplWith :: MonadException m => ReplSettings m -> m ()+evalReplWith settings = runHaskelineT _readline (tryAction monad)+ where+ monad = replLoop (_banner settings) (_cmd settings) (_opts settings)+ _readline = H.Settings+ { H.complete = mkCompleter (_compstyle settings)+ , H.historyFile = Just ".history"+ , H.autoAddHistory = True+ }++-------------------------------------------------------------------------------+-- Completions+-------------------------------------------------------------------------------++data CompleterStyle m+ = Word (WordCompleter m) -- ^ Completion function takes single word.+ | Cursor (LineCompleter m) -- ^ Completion function takes tuple of full line.+ | File -- ^ Completion function completes files in CWD.+ | Mixed (CompletionFunc m) -- ^ Function manually manipulates readline result.++mkCompleter :: MonadIO m => CompleterStyle m -> CompletionFunc m+mkCompleter (Word f) = completeWord (Just '\\') " \t()[]" f+mkCompleter (Cursor f) = completeWordWithPrev (Just '\\') " \t()[]" f+mkCompleter File = completeFilename+mkCompleter (Mixed f) = f++trimComplete :: String -> Completion -> Completion+trimComplete prefix (Completion a b c) = Completion (drop (length prefix) a) b c