{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} -- | This libraries provides a polysemy effect that provides interactive command -- line usage. module Polysemy.Readline ( -- * Effect and Actions Readline (..), getInputLine, getInputLineWithInitial, getInputChar, getPassword, waitForAnyKey, outputStr, outputStrLn, -- * Interpreters runReadline, runReadlineFinal, interpretReadlineAsInputT, -- * Re-exports from @haskeline@ H.Settings (..), H.defaultSettings, H.runInputT, ) where import Control.Monad.Catch import Control.Monad.IO.Class import Polysemy import Polysemy.Embed import qualified System.Console.Haskeline as H -- | For documentation on actions see haskeline's functions with the same name -- and similar type signatures. data Readline (m :: * -> *) a where GetInputLine :: String -> Readline m (Maybe String) GetInputLineWithInitial :: String -> (String, String) -> Readline m (Maybe String) GetInputChar :: String -> Readline m (Maybe Char) GetPassword :: Maybe Char -> String -> Readline m (Maybe String) WaitForAnyKey :: String -> Readline m Bool OutputStr :: String -> Readline m () -- TODO(Devin): add these two values as well -- WithInterrupt :: m a -> Readline m a -- HandleInterrupt :: m a -> m a -> Readline m a makeSem ''Readline outputStrLn :: Member Readline r => String -> Sem r () outputStrLn str = outputStr (str <> "\n") -- | The simplest way to run a Readline effect. Immediately eliminates the -- resulting 'H.InputT'. There is one problem with this approach however. -- Internal details of polysemy cause 'H.runInputT' to be run once per effect -- call (e.g. @getInputLine "> " >> getInputLine "> "@ will result in two calls -- to 'H.runInputT'), and the History state of consecutive runs is not -- preserved unless there is a history file. If you want history for your repl -- there are therefore two recommended approaches: -- -- * Provide a history file in the settings you specify. e.g. -- @runReadline ('H.defaultSettings' {historyFile = Just ".repl_history"})@. -- This is the easiest approach but technically suboptimal because the history -- file will be read between every different primitive effect call. -- * Use 'interpretReadlineAsInputT' or 'runReadlineFinal' and keep the -- `H.InputT` around until after using 'runFinal' to escape polysemy land. This -- way state can be preserved between effect calls. For an example using this -- see @examples/Echo.hs@. runReadline :: forall m r a. (MonadIO m, MonadMask m, Member (Embed m) r) => H.Settings m -> Sem (Readline : r) a -> Sem r a runReadline settings = runEmbedded (H.runInputT settings) . interpretReadlineAsInputT . raiseUnder @(Embed (H.InputT m)) -- | Interpreter for the somewhat common case of wanting to keep InputT around -- until after 'runFinal' to ensure that state is preserved between subsequent -- effects. runReadlineFinal :: forall m r a. (MonadIO m, MonadMask m, Member (Final (H.InputT m)) r) => Sem (Readline : r) a -> Sem r a runReadlineFinal = embedToFinal . interpretReadlineAsInputT . raiseUnder @(Embed (H.InputT m)) -- | Interpret in terms of an embedded 'H.InputT' stack. interpretReadlineAsInputT :: forall m r a. (MonadIO m, MonadMask m, Member (Embed (H.InputT m)) r) => Sem (Readline : r) a -> Sem r a interpretReadlineAsInputT = interpret $ \case GetInputLine prompt -> embed $ H.getInputLine prompt GetInputLineWithInitial prompt initial -> embed $ H.getInputLineWithInitial prompt initial GetInputChar prompt -> embed $ H.getInputChar prompt GetPassword maskChar prompt -> embed $ H.getPassword maskChar prompt WaitForAnyKey prompt -> embed $ H.waitForAnyKey prompt OutputStr str -> embed $ H.outputStr str