-- | Proof-of-concept module: use digestive functors for a command line -- interface prompt -- {-# LANGUAGE GeneralizedNewtypeDeriving #-} module Text.Digestive.Cli ( Descriptions (..) , Prompt , prompt , promptRead , runPrompt ) where import Data.Map (Map) import qualified Data.Map as M import Data.Monoid (Monoid, mempty, mappend) import Control.Applicative ((<$>)) import Text.Digestive.Result import Text.Digestive.Types import Text.Digestive.Transform import qualified Text.Digestive.Common as Common -- | A list of descriptions for a certain input prompt -- newtype Descriptions = Descriptions { unDescriptions :: Map FormId [String] } deriving (Show) instance Monoid Descriptions where mempty = Descriptions mempty mappend (Descriptions m1) (Descriptions m2) = Descriptions $ M.unionWith (++) m1 m2 -- | Type for a prompt -- type Prompt a = Form IO String String Descriptions a -- | Remove the descriptions for the inputs already in the input map. -- neededDescriptions :: InputMap -> Descriptions -> Descriptions neededDescriptions (InputMap inputMap) = Descriptions . M.filterWithKey notInInput . unDescriptions where notInInput k _ = k `notElem` map fst inputMap -- | Add errors to the descriptions -- addErrors :: [(FormRange, String)] -> Descriptions -> Descriptions addErrors errors (Descriptions descr) = Descriptions $ foldl add' descr errors where add' map' ((FormRange x _, e)) = M.insertWith (++) x [e] map' newtype InputMap = InputMap { unInputMap :: [(FormId, String)] } deriving (Show, Monoid) -- | Create an environment from an input map -- inputMapEnvironment :: Monad m => InputMap -> Environment m String inputMapEnvironment map' = Environment $ return . flip lookup (unInputMap map') -- | Prompt once for a needed field. Return (key, value) -- promptOnce :: Descriptions -> IO (FormId, String) promptOnce (Descriptions descr) | M.null descr = error "No descriptions!" | otherwise = do putStrLn "" mapM_ putStrLn description putStr "> " (,) key <$> getLine where (key, description) = M.findMin descr -- | Remove all input for which errors are found -- removeInvalidInput :: InputMap -> [(FormRange, String)] -> InputMap removeInvalidInput = foldl removeInvalidInput' where removeInvalidInput' :: InputMap -> (FormRange, String) -> InputMap removeInvalidInput' (InputMap map') (range, _) = InputMap $ filter (not . flip isInRange range . fst) map' -- | Generate a prompt field from a description -- prompt :: String -> Prompt String prompt descr = Common.input (const $ const $ const []) toResult (\x _ -> Descriptions $ M.singleton x [descr]) "" where toResult Nothing = Error [] toResult (Just x) = Ok x -- | Generate a prompt field for a value which can be read -- promptRead :: Read a => String -- ^ Error when the value can't be read -> String -- ^ Description -> Prompt a -- ^ Resulting prompt promptRead error' descr = prompt descr `transform` transformRead error' -- | Run a prompt. This will repeatedly prompt for input until we have a valid -- result. -- runPrompt :: Prompt a -> IO a runPrompt form = prompt' mempty where prompt' inputMap = do (v, r) <- runForm form "form" $ inputMapEnvironment inputMap case r of Ok x -> return x Error e -> do let inputMap' = removeInvalidInput inputMap e descr = addErrors e $ neededDescriptions inputMap' (unView v []) input' <- promptOnce descr prompt' $ inputMap' `mappend` InputMap [input']