{-# LANGUAGE OverloadedStrings #-} -- | Building blocks for a GHCI-like REPL with colon-commands. module Climb ( Command , CommandExc (..) , Completion , OptionCommands , ReplDef (..) , ReplDirective (..) , bareCommand , noOptionCommands , noCompletion , runReplDef ) where import Control.Exception (Exception) import Control.Monad (unless) import Control.Monad.Catch (MonadThrow (..)) import Control.Monad.Fix (fix) import Control.Monad.IO.Class (MonadIO (..)) import Control.Monad.IO.Unlift (MonadUnliftIO) import Data.Foldable (for_) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.IO as TIO import Data.Typeable (Typeable) import Linenoise.Repl (ReplDirective (..), replM) -- | A 'Command' takes some input, performs some effect, and returns a directive (continue or quit). type Command m = Text -> m ReplDirective -- | List of 'Command's by name with help text. type OptionCommands m = Map Text (Text, Command m) -- | A 'Completion' takes some input and returns potential matches. type Completion m = Text -> m [Text] -- | Sometimes things go wrong... data CommandExc = ExpectedNoInputError -- ^ An option 'Command' got input when it expected None | MissingCommandError !Text -- ^ An option 'Command' was not found by name. deriving (Eq, Show, Typeable) instance Exception CommandExc -- | Defines a REPL with commands, options, and completion. data ReplDef m = ReplDef { _rdOnInterrupt :: !ReplDirective , _rdGreeting :: !Text , _rdPrompt :: !Text , _rdOptionCommands :: !(OptionCommands m) , _rdExecCommand :: !(Command m) , _rdCompletion :: !(Completion m) } noOptionCommands :: OptionCommands m noOptionCommands = Map.empty noCompletion :: Applicative m => Completion m noCompletion = const (pure []) assertEmpty :: MonadThrow m => Text -> m () assertEmpty input = unless (Text.null input) (throwM ExpectedNoInputError) -- | Helps you define commands that expect no input. bareCommand :: MonadThrow m => m ReplDirective -> Command m bareCommand act input = assertEmpty input >> act quitCommand :: MonadThrow m => Command m quitCommand = bareCommand (pure ReplQuit) helpCommand :: (MonadThrow m, MonadIO m) => OptionCommands m -> Command m helpCommand opts = bareCommand $ do liftIO (TIO.putStrLn "Available commands:") for_ (Map.toList opts) $ \(name, (desc, _)) -> liftIO (TIO.putStrLn (":" <> name <> "\t" <> desc)) pure ReplContinue defaultOptions :: (MonadThrow m, MonadIO m) => OptionCommands m -> OptionCommands m defaultOptions opts = Map.fromList [ ("quit", ("quit", quitCommand)) , ("help", ("describe all commands", helpCommand opts)) ] outerCommand :: MonadThrow m => OptionCommands m -> Command m -> Command m outerCommand opts exec input = case Text.uncons input of Just (':', rest) -> do let (name, subInput) = Text.break (==' ') rest case Map.lookup name opts of Nothing -> throwM (MissingCommandError name) Just (_, command) -> command (Text.drop 1 subInput) _ -> exec input -- | Runs a REPL as defined. runReplDef :: (MonadThrow m, MonadUnliftIO m) => ReplDef m -> m () runReplDef (ReplDef onInterrupt greeting prompt opts exec comp) = do let allOpts = fix (\c -> defaultOptions c <> opts) action = outerCommand allOpts exec liftIO (TIO.putStrLn greeting) liftIO (TIO.putStrLn "Enter `:quit` to exit or `:help` to see all commands.") replM onInterrupt prompt action comp