twitch-0.1.7.2: src/Twitch/Main.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
module Twitch.Main where
import Control.Applicative -- satisfy GHC < 7.10
import Control.Concurrent ( threadDelay )
import Control.Exception ( finally )
import Data.Monoid
import Options.Applicative
( Parser
, helper
, execParser
, value
, short
, progDesc
, option
, metavar
, long
, info
, help
, header
, fullDesc
, auto
, eitherReader
, switch
, flag
, ParserInfo
)
import Data.Default ( Default(..) )
import qualified System.FSNotify as FS
import Twitch.Path ( findAllDirs )
import qualified Twitch.InternalRule as IR
import System.IO
( IOMode(AppendMode)
, Handle
, hPrint
, openFile
, hClose
)
import Data.Foldable ( for_ )
import Twitch.Run ( runWithConfig )
import Twitch.Internal ( Dep )
import System.Directory ( getCurrentDirectory )
import Data.Maybe ( fromMaybe )
import System.FilePath
( FilePath
, (</>)
, isRelative
, isValid
)
import Control.Monad ( forever, liftM )
-- Moved here to suppress redundant import warnings for GHC > 7.10
import Prelude hiding (log, FilePath)
-- parse the command line
--
concatMapM :: Monad m => (a -> m [b]) -> [a] -> m [b]
concatMapM f = liftM concat . mapM f
data LoggerType
= LogToStdout
| LogToFile
| NoLogger
deriving (Eq, Show, Read, Ord)
toLogger :: FilePath
-> LoggerType
-> IO (IR.Issue -> IO (), Maybe Handle)
toLogger filePath lt = case lt of
LogToStdout -> return (print, Nothing)
LogToFile -> do
handle <- openFile filePath AppendMode
return (hPrint handle, Just handle)
NoLogger -> return (const $ return (), Nothing)
data Options = Options
{ log :: LoggerType
-- ^ The logger type.
-- This corresponds to the --log or -l argument. The valid options
-- are "LogToStdout", "LogToFile", and "NoLogger"
-- If "LogToFile" a file can provide with the 'logFile' field.
, logFile :: Maybe FilePath
-- ^ The file to log to.
-- This is only used if the 'log' field is set to "LogToFile".
-- This corresponds to the --log-file or -f argument.
, root :: Maybe FilePath
-- ^ The root directory to watch.
-- This corresponds to the --root and -r argument.
-- By default this is empty and the current directory is used.
, recurseThroughDirectories :: Bool
-- ^ If true, main will recurse through all subdirectories of the 'dirsToWatch'
-- field. Otherwise the 'dirsToWatch' will be used literally.
-- By default this is true, and disabled with the --no-recurse-flag .
, debounce :: DebounceType
-- ^ This corresponds to the debounce type used in the fsnotify library
-- The argument for default main is --debounce or -b .
-- Valid options are "DebounceDefault", "Debounce", "NoDebounce".
-- If "Debounce" is used, then a debounce amount must be specified with the
-- 'debounceAmount'.
, debounceAmount :: Double
-- ^ The amount to debounce. This is only meaningful when 'debounce' is set
-- to 'Debounce'.
-- It corresponds to the --debounce-amount or -a argument.
, pollInterval :: Int
-- ^ poll interval if polling is used.
-- This corresponds to the --poll-interval or -i argument.
, usePolling :: Bool
-- ^ Sets polling to true if used.
-- This corresponds to the --should-poll or -p flag.
}
data DebounceType
= DebounceDefault
| Debounce
| NoDebounce
deriving (Eq, Show, Read, Ord)
-- use the new vinyl for the options
-- the you get the monoid instance for free
instance Default Options where
def = Options
{ log = NoLogger
, logFile = Nothing
, root = Nothing
, recurseThroughDirectories = True
, debounce = DebounceDefault
, debounceAmount = 0
, pollInterval = 10^(6 :: Int) -- 1 second
, usePolling = False
}
dropDoubleQuotes :: String -> String
dropDoubleQuotes [] = []
dropDoubleQuotes (x : xs)
| x == '\"' = xs
| otherwise = x : xs
stripDoubleQuotes :: String -> String
stripDoubleQuotes = dropDoubleQuotes . reverse . dropDoubleQuotes . reverse
-- strip quotes if they are there
readFilePath :: String -> Either String FilePath
readFilePath xs =
let filePath = stripDoubleQuotes xs
in if isValid filePath then
Right filePath
else
Left $ "invalid filePath " ++ xs
pOptions :: Parser Options
pOptions
= Options
<$> option auto
( long "log"
<> short 'l'
<> metavar "LOG_TYPE"
<> help "Type of logger. Valid options are LogToStdout | LogToFile | NoLogger"
<> value (log def)
)
<*> option (Just <$> eitherReader readFilePath)
( long "log-file"
<> short 'f'
<> metavar "LOG_FILE"
<> help "Log file"
<> value (logFile def)
)
<*> option (Just <$> eitherReader readFilePath)
( long "root"
<> short 'r'
<> metavar "ROOT"
<> help "Root directory to watch"
<> value (root def)
)
<*> flag True False
( long "no-recurse"
<> short 'n'
<> help "flag to turn off recursing"
)
<*> option auto
( long "debounce"
<> short 'b'
<> metavar "DEBOUNCE"
<> help "Type of debouncing. Valid choices are DebounceDefault | Debounce | NoDebounce"
<> value (debounce def)
)
<*> option auto
( long "debounce-amount"
<> short 'a'
<> metavar "DEBOUNCE_AMOUNT"
<> help "Target for the greeting"
<> value (debounceAmount def)
)
<*> option auto
( long "poll-interval"
<> short 'i'
<> metavar "POLL_INTERVAL"
<> help "Poll interval if polling is used"
<> value (pollInterval def)
)
<*> switch
( long "should-poll"
<> short 'p'
<> help "Whether to use polling or not. Off by default"
)
-- This is like run, but the config params can be over written from the defaults
toDB :: Real a => a -> DebounceType -> FS.Debounce
toDB amount dbtype = case dbtype of
DebounceDefault -> FS.DebounceDefault
Debounce -> FS.Debounce $ fromRational $ toRational amount
NoDebounce -> FS.NoDebounce
makeAbsolute :: FilePath -> FilePath -> FilePath
makeAbsolute currentDir path =
if isRelative path then
currentDir </> path
else
path
optionsToConfig :: Options -> IO (FilePath, IR.Config, Maybe Handle)
optionsToConfig Options {..} = do
currentDir <- getCurrentDirectory
let root' = makeAbsolute currentDir $ fromMaybe currentDir root
(logger, mhandle) <- toLogger (fromMaybe "log.txt" logFile) log
dirsToWatch <- if recurseThroughDirectories then
(root' :) <$> findAllDirs root'
else
return [root']
let watchConfig = FS.WatchConfig
{ FS.confDebounce = toDB debounceAmount debounce
, FS.confPollInterval = pollInterval
, FS.confUsePolling = usePolling
}
let config = IR.Config
{ logger = logger
, dirs = dirsToWatch
, watchConfig = watchConfig
}
return (root', config, mhandle)
opts :: ParserInfo Options
opts = info (helper <*> pOptions)
( fullDesc
<> progDesc "twitch"
<> header "a file watcher"
)
-- | Simplest way to create a file watcher app. Set your main equal to defaultMain
-- and you are good to go. See the module documentation for examples.
--
-- The command line is parsed to make 'Options' value. For more information on
-- the arguments that can be passed see the doc for 'Options' and the run the
-- executable made with defaultMain with the --help argument.
defaultMain :: Dep -> IO ()
defaultMain dep = do
options <- execParser opts
defaultMainWithOptions options dep
-- | A main file that uses manually supplied options instead of parsing the passed in arguments.
defaultMainWithOptions :: Options -> Dep -> IO ()
defaultMainWithOptions options dep = do
(root, config, mhandle) <- optionsToConfig options
manager <- runWithConfig root config dep
putStrLn "Type Ctrl+C to quit"
forever (threadDelay maxBound)
`finally` do
for_ mhandle hClose
FS.stopManager manager