{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts, FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Graphics.Vty.Config
( InputMap
, Config(..)
, VtyConfigurationError(..)
, userConfig
, overrideEnvConfig
, standardIOConfig
, runParseConfig
, parseConfigFile
, defaultConfig
)
where
import Prelude
import Control.Applicative hiding (many)
import Control.Exception (catch, IOException, Exception(..), throwIO)
import Control.Monad (liftM, guard, void)
import qualified Data.ByteString as BS
#if !(MIN_VERSION_base(4,8,0))
import Data.Monoid (Monoid(..))
#endif
#if !(MIN_VERSION_base(4,11,0))
import Data.Semigroup (Semigroup(..))
#endif
import Data.Typeable (Typeable)
import Graphics.Vty.Input.Events
import GHC.Generics
import System.Directory (getAppUserDataDirectory)
import System.Environment (lookupEnv)
import System.Posix.IO (stdInput, stdOutput)
import System.Posix.Types (Fd(..))
import Text.Parsec hiding ((<|>))
import Text.Parsec.Token ( GenLanguageDef(..) )
import qualified Text.Parsec.Token as P
data VtyConfigurationError
= VtyMissingTermEnvVar
deriving (Show, Eq, Typeable)
instance Exception VtyConfigurationError where
displayException VtyMissingTermEnvVar = "TERM environment variable not set"
type InputMap = [(Maybe String, String, Event)]
data Config = Config
{
vmin :: Maybe Int
, vtime :: Maybe Int
, mouseMode :: Maybe Bool
, bracketedPasteMode :: Maybe Bool
, debugLog :: Maybe FilePath
, inputMap :: InputMap
, inputFd :: Maybe Fd
, outputFd :: Maybe Fd
, termName :: Maybe String
} deriving (Show, Eq)
defaultConfig :: Config
defaultConfig = mempty
instance Semigroup Config where
c0 <> c1 = Config
{ vmin = vmin c1 <|> vmin c0
, vtime = vtime c1 <|> vtime c0
, mouseMode = mouseMode c1
, bracketedPasteMode = bracketedPasteMode c1
, debugLog = debugLog c1 <|> debugLog c0
, inputMap = inputMap c0 <> inputMap c1
, inputFd = inputFd c1 <|> inputFd c0
, outputFd = outputFd c1 <|> outputFd c0
, termName = termName c1 <|> termName c0
}
instance Monoid Config where
mempty = Config
{ vmin = Nothing
, vtime = Nothing
, mouseMode = Nothing
, bracketedPasteMode = Nothing
, debugLog = mempty
, inputMap = mempty
, inputFd = Nothing
, outputFd = Nothing
, termName = Nothing
}
#if !(MIN_VERSION_base(4,11,0))
mappend = (<>)
#endif
userConfig :: IO Config
userConfig = do
configFile <- (mappend <$> getAppUserDataDirectory "vty" <*> pure "/config") >>= parseConfigFile
overrideConfig <- maybe (return defaultConfig) parseConfigFile =<< lookupEnv "VTY_CONFIG_FILE"
let base = configFile <> overrideConfig
mappend base <$> overrideEnvConfig
overrideEnvConfig :: IO Config
overrideEnvConfig = do
d <- lookupEnv "VTY_DEBUG_LOG"
return $ defaultConfig { debugLog = d }
standardIOConfig :: IO Config
standardIOConfig = do
mb <- lookupEnv "TERM"
case mb of
Nothing -> throwIO VtyMissingTermEnvVar
Just t ->
return defaultConfig
{ vmin = Just 1
, mouseMode = Just False
, bracketedPasteMode = Just False
, vtime = Just 100
, inputFd = Just stdInput
, outputFd = Just stdOutput
, termName = Just t
}
parseConfigFile :: FilePath -> IO Config
parseConfigFile path = do
catch (runParseConfig path <$> BS.readFile path)
(\(_ :: IOException) -> return defaultConfig)
runParseConfig :: String -> BS.ByteString -> Config
runParseConfig name cfgTxt =
case runParser parseConfig () name cfgTxt of
Right cfg -> cfg
Left{} -> defaultConfig
type Parser = Parsec BS.ByteString ()
configLanguage :: Monad m => P.GenLanguageDef BS.ByteString () m
configLanguage = LanguageDef
{ commentStart = "{-"
, commentEnd = "-}"
, commentLine = "--"
, nestedComments = True
, identStart = letter <|> char '_'
, identLetter = alphaNum <|> oneOf "_'"
, opStart = opLetter configLanguage
, opLetter = oneOf ":!#$%&*+./<=>?@\\^|-~"
, reservedOpNames = []
, reservedNames = []
, caseSensitive = True
}
configLexer :: Monad m => P.GenTokenParser BS.ByteString () m
configLexer = P.makeTokenParser configLanguage
mapDecl :: Parser Config
mapDecl = do
"map" <- P.identifier configLexer
termIdent <- (char '_' >> P.whiteSpace configLexer >> return Nothing)
<|> (Just <$> P.stringLiteral configLexer)
bytes <- P.stringLiteral configLexer
key <- parseValue
modifiers <- parseValue
return defaultConfig { inputMap = [(termIdent, bytes, EvKey key modifiers)] }
debugLogDecl :: Parser Config
debugLogDecl = do
"debugLog" <- P.identifier configLexer
path <- P.stringLiteral configLexer
return defaultConfig { debugLog = Just path }
ignoreLine :: Parser ()
ignoreLine = void $ manyTill anyChar newline
parseConfig :: Parser Config
parseConfig = liftM mconcat $ many $ do
P.whiteSpace configLexer
let directives = [try mapDecl, try debugLogDecl]
choice directives <|> (ignoreLine >> return defaultConfig)
class Parse a where parseValue :: Parser a
instance Parse Char where parseValue = P.charLiteral configLexer
instance Parse Int where parseValue = fromInteger <$> P.natural configLexer
instance Parse Key where parseValue = genericParse
instance Parse Modifier where parseValue = genericParse
instance Parse a => Parse [a] where
parseValue = P.brackets configLexer
(parseValue `sepBy` P.symbol configLexer ",")
genericParse :: (Generic a, GParse (Rep a)) => Parser a
genericParse = to <$> gparse
class GParse f where gparse :: Parser (f a)
instance GParse f => GParse (M1 S i f) where gparse = M1 <$> gparse
instance GParse U1 where gparse = return U1
instance Parse a => GParse (K1 i a) where gparse = K1 <$> parseValue
instance (GParse f, GParse g) => GParse (f :*: g) where
gparse = (:*:) <$> gparse <*> gparse
instance GParseAlts f => GParse (M1 D i f) where
gparse =
do con <- P.identifier configLexer
M1 <$> gparseAlts con
class GParseAlts f where
gparseAlts :: String -> Parser (f a)
instance (Constructor i, GParse f) => GParseAlts (M1 C i f) where
gparseAlts con =
do guard (con == conName (M1 Nothing :: C1 i Maybe a))
M1 <$> gparse
instance (GParseAlts f, GParseAlts g) => GParseAlts (f :+: g) where
gparseAlts con = L1 <$> gparseAlts con <|> R1 <$> gparseAlts con
instance GParseAlts V1 where gparseAlts _ = fail "GParse: V1"