vimus-0.1.0: src/Command/Core.hs
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, MultiParamTypeClasses, FlexibleContexts, ScopedTypeVariables #-}
module Command.Core (
Command
, commandName
, commandAction
, commandSynopsis
, Argument (..)
, ArgumentSpec (..)
, noCompletion
, argumentParser
, Action (..)
, VimusAction
, runAction
, command
-- * Helpers for defining @Argument@ instances
, missingArgument
, invalidArgument
, specificArgumentError
-- * exported for testing
, readParser
, IsAction (..)
) where
import Control.Applicative
import Control.Monad (unless)
import Data.Char
import Vimus (Vimus)
import Text.Read (readMaybe)
import WindowLayout (WindowColor(..), defaultColor)
import UI.Curses (Color, black, red, green, yellow, blue, magenta, cyan, white)
import Command.Type
import Command.Help () -- for the (IsString Help) instance
import Command.Completion
import Command.Parser
import Song.Format (SongFormat)
import qualified Song.Format as SongFormat
import qualified Song
runAction :: Action a -> String -> Either String a
runAction action s = either (Left . show) (Right . fst) $ runParser (unAction action <* endOfInput) s
class IsAction a b where
toAction :: a -> Action b
actionArguments :: a -> b -> [ArgumentInfo]
instance IsAction a a where
toAction a = Action $ do
r <- takeInput
unless (null r) $ do
parserFail (SuperfluousInput r)
return a
actionArguments _ _ = []
instance (Argument a, IsAction b c) => IsAction (a -> b) c where
toAction action = Action $ (argumentParser <* skipWhile isSpace) >>= unAction . toAction . action
actionArguments _ _ = mkArgumentInfo (argumentSpec :: (ArgumentSpec a)) : actionArguments (undefined :: b) (undefined :: c)
-- | Get help text for given command.
commandSynopsis :: Command -> String
commandSynopsis c = unwords $ commandName c : map (\x -> "{" ++ argumentInfoName x ++ "}") (commandArguments c)
-- | Define a command.
command :: forall a . IsAction a (Vimus ()) => String -> Help -> a -> Command
command name description action = Command name description (actionArguments action (undefined :: Vimus ())) (toAction action)
-- | Create an ArgumentInfo from given ArgumentSpec.
mkArgumentInfo :: ArgumentSpec a -> ArgumentInfo
mkArgumentInfo arg = ArgumentInfo {
argumentInfoName = argumentSpecName arg
, argumentInfoComplete = argumentSpecComplete arg
}
-- | Like ArgumentInfo, but includes a parser for the argument.
data ArgumentSpec a = ArgumentSpec {
argumentSpecName :: String
, argumentSpecComplete :: CompletionFunction
, argumentSpecParser :: Parser a
}
-- | An argument.
class Argument a where
-- | A parser for this argument, together with a description.
--
-- The description provides information about the argument, that can be used
-- for command-line completion and online help.
--
-- The parser can assume that the input is either empty or starts with a
-- non-whitespace character.
argumentSpec :: ArgumentSpec a
argumentParser :: Argument a => Parser a
argumentParser = argumentSpecParser argumentSpec
argumentName :: forall a . Argument a => a -> String
argumentName _ = argumentSpecName (argumentSpec :: ArgumentSpec a)
-- | A parser for arguments in the Read class.
readParser :: forall a . (Read a, Argument a) => Parser a
readParser = mkParser readMaybe
-- | A helper function for constructing argument parsers.
mkParser :: forall a . (Argument a) => (String -> Maybe a) -> Parser a
mkParser f = do
r <- takeWhile1 (not . isSpace) <|> missingArgument (undefined :: a)
maybe (invalidArgument (undefined ::a) r) return (f r)
-- | A failing parser that indicates a missing argument.
missingArgument :: Argument a => a -> Parser b
missingArgument = parserFail . MissingArgument . argumentName
-- | A failing parser that indicates an invalid argument.
invalidArgument :: Argument a => a -> Value -> Parser b
invalidArgument t = parserFail . InvalidArgument (argumentName t)
-- | A failing parser that indicates a specific error. It takes precedence
-- over any other kind of error.
specificArgumentError :: String -> Parser b
specificArgumentError = parserFail . SpecificArgumentError
instance Argument Int where
argumentSpec = ArgumentSpec "int" noCompletion readParser
instance Argument Integer where
argumentSpec = ArgumentSpec "integer" noCompletion readParser
instance Argument Float where
argumentSpec = ArgumentSpec "float" noCompletion readParser
instance Argument Double where
argumentSpec = ArgumentSpec "double" noCompletion readParser
instance Argument String where
argumentSpec = ArgumentSpec "string" noCompletion (mkParser Just)
instance Argument SongFormat where
argumentSpec = ArgumentSpec "songformat" noCompletion (SongFormat.parser Song.metaQueries)
-- | Create an ArgumentSpec from an association list.
mkArgumentSpec :: Argument a => String -> [(String, a)] -> ArgumentSpec a
mkArgumentSpec name values = ArgumentSpec name complete parser
where
parser = mkParser ((`lookup` values) . map toLower)
complete = completeOptions (map fst values)
instance Argument WindowColor where
argumentSpec = mkArgumentSpec "item" [
("main", MainColor)
, ("ruler", RulerColor)
, ("tab", TabColor)
, ("input", InputColor)
, ("playstatus", PlayStatusColor)
, ("songstatus", SongStatusColor)
, ("error", ErrorColor)
, ("suggestions", SuggestionsColor)
]
instance Argument Color where
argumentSpec = mkArgumentSpec "color" [
("default", defaultColor)
, ("black", black)
, ("red", red)
, ("green", green)
, ("yellow", yellow)
, ("blue", blue)
, ("magenta", magenta)
, ("cyan", cyan)
, ("white", white)
]