diff --git a/extras/completion/bash b/extras/completion/bash index c2e498a..9cbc07d 100644 --- a/extras/completion/bash +++ b/extras/completion/bash @@ -1,9 +1,9 @@ _maid() { case "$2","$3" in -*,*) - COMPREPLY=( $(compgen -W "-h -l -n -q -f --help --list --dry-run --quiet --taskfile" -- "$2") ) + COMPREPLY=( $(compgen -W "-h -l -n -q -f -w -r --help --list --dry-run --quiet --taskfile --watch --restart" -- "$2") ) ;; - *,-f | *,--maidfile) + *,-f | *,--maidfile | *,-w | *,--watch) COMPREPLY=( $(compgen -f -- "$2") ) ;; *) diff --git a/extras/completion/fish b/extras/completion/fish index 8c53734..6c6e121 100644 --- a/extras/completion/fish +++ b/extras/completion/fish @@ -1,9 +1,12 @@ -complete -c maid -n "__fish_is_first_arg" -f +complete -c maid -e +complete -c maid -n "__fish_is_first_token" -f complete -c maid -a "(maid -l | sed 's/ \(.\)/\t\L\1/')" complete -c maid -s h -l help -d "display usage" complete -c maid -s l -l list -d "list tasks concisely" complete -c maid -s n -l dry-run -d "don't run anything, only display commands" complete -c maid -s q -l quiet -d "don't display anything" -complete -c maid -s f -l taskfile -d "use tasks in file" +complete -c maid -s f -l taskfile -r -d "use tasks in file" +complete -c maid -s w -l watch -r -d "watch files in path" +complete -c maid -s r -l restart -d "restart tasks instead of waiting" # vim: ft=fish diff --git a/extras/completion/zsh b/extras/completion/zsh index ce74187..0a7ccb1 100644 --- a/extras/completion/zsh +++ b/extras/completion/zsh @@ -18,6 +18,8 @@ _maid() { "(dry-run)"{-n,--dry-run}"[don't run anything, only display commands]" \ "(quiet)"{-q,--quiet}"[don't display anything]" \ "(taskfile)"{-f,--taskfile=}"[use tasks in file]:maidfile:_files" \ + "(watch)"{-w,--watch=}"[watch tasks in path]:watch:_files" \ + "(restart)"{-r,--restart}"[restart tasks instead of waiting]" \ ":task:_maid_tasks" \ "*:args:_default" } diff --git a/maid-build.cabal b/maid-build.cabal index a81bd9b..2173595 100644 --- a/maid-build.cabal +++ b/maid-build.cabal @@ -19,7 +19,7 @@ source-repository head location: https://github.com/rniii/maid.git common base - ghc-options: -Wall -Wno-name-shadowing + ghc-options: -Wall -Wno-name-shadowing -threaded default-language: Haskell2010 build-depends: base >= 4.7 && <5 @@ -31,13 +31,14 @@ library Maid.Parser, build-depends: ansi-terminal ^>= 1.1, + bytestring ^>= 0.11, directory ^>= 1.3, filepath ^>= 1.5, - process ^>= 1.6, - bytestring ^>= 0.11, - text ^>= 2.0, + fsnotify ^>= 0.4, mtl ^>= 2.2, + process ^>= 1.6, temporary ^>= 1.3, + text ^>= 2.0, executable maid import: base diff --git a/src/Maid.hs b/src/Maid.hs index 71a12ad..6208ca2 100644 --- a/src/Maid.hs +++ b/src/Maid.hs @@ -7,8 +7,9 @@ module Maid (run, MaidError) where import Maid.Parser (Task (..), parseTasks) import Control.Applicative (liftA2) +import Control.Concurrent (forkIO, killThread, newEmptyMVar, newMVar, putMVar, swapMVar, takeMVar, threadDelay, tryTakeMVar) import Control.Exception (Exception, handle, throw) -import Control.Monad (forM_, unless, when) +import Control.Monad (forM_, forever, unless, void, when) import Control.Monad.Reader (MonadIO (liftIO), MonadReader, ReaderT (runReaderT), asks) import Control.Monad.State (StateT, execStateT, gets, modify) import Data.Bool (bool) @@ -23,6 +24,7 @@ import System.Console.GetOpt (ArgDescr (NoArg, ReqArg), ArgOrder (RequireOrder), import System.Directory (getCurrentDirectory) import System.Environment (getArgs, getProgName, lookupEnv) import System.Exit (ExitCode (ExitFailure), exitFailure, exitSuccess, exitWith) +import System.FSNotify (watchTree, withManager) import System.FilePath (combine, isDrive, takeDirectory, takeFileName) import System.IO (hClose, hPutStrLn, stderr, stdout) import System.IO.Error (catchIOError) @@ -47,6 +49,8 @@ run = do , Option ['n'] ["dry-run"] (NoArg DryRun) "Don't run anything, only display commands" , Option ['q'] ["quiet"] (NoArg Quiet) "Don't display anything" , Option ['f'] ["taskfile"] (ReqArg Maidfile "FILE") "Use tasks in FILE" + , Option ['w'] ["watch"] (ReqArg Watch "PATH") "Watch files in PATH. Can be specified multiple times" + , Option ['r'] ["restart"] (NoArg Restart) "When files change, restart the task instead of waiting" ] parseOpt :: Flag -> StateT Context IO () @@ -70,8 +74,19 @@ run = do when (null tasks) $ throw $ NoTasks file modify $ \c -> c{ctxTaskfile = (file, tasks)} - - context = Context ([], []) False False <$> defaultStyle + parseOpt (Watch dir) = modify $ \c -> c{ctxWatch = dir : ctxWatch c} + parseOpt Restart = modify $ \c -> c{ctxWatchRestart = True} + + context = + (`fmap` defaultStyle) $ \style -> + Context + { ctxTaskfile = ([], []) + , ctxDryRun = False + , ctxQuiet = False + , ctxWatch = [] + , ctxWatchRestart = False + , ctxStyle = style + } setTaskfile ctx = case ctxTaskfile ctx of ([], []) -> do @@ -81,10 +96,17 @@ run = do bail e = putErr e >> exitFailure - runArgs (task : args) = runTask (T.pack task) args + runArgs (task : args) = execTask (T.pack task) args runArgs [] = listTasks -data Flag = Help | List | DryRun | Quiet | Maidfile String +data Flag + = Help + | List + | DryRun + | Quiet + | Maidfile String + | Watch String + | Restart putErr :: String -> IO () putErr msg = do @@ -95,10 +117,43 @@ getTask :: Text -> [Task] -> Task getTask name = fromMaybe (throw $ UnknownTask $ T.unpack name) . find ((. tName) (== name)) -runTask :: Text -> [String] -> Maid () -runTask name args = do +execTask :: Text -> [String] -> Maid () +execTask name args = do task <- asks (getTask name . snd . ctxTaskfile) - displayCommand ("maid " <> name) + watch <- asks ctxWatch + watchTask watch task args + +watchTask :: [FilePath] -> Task -> [String] -> Maid () +watchTask [] task args = do + runTask task args +watchTask paths task args = do + restart <- asks ctxWatchRestart + exec <- asks $ runReaderT $ unMaid $ runTask task args + liftIO $ withManager $ \mgr -> do + action <- bool watchWait watchRestart restart exec + forM_ paths $ \path -> + watchTree mgr path (const True) (const action) + forever (threadDelay maxBound) + where + watchWait exec = do + void exec + lock <- newMVar () + return $ void $ forkIO $ do + blocked <- isNothing <$> tryTakeMVar lock + unless blocked (exec >> putMVar lock ()) + watchRestart exec = do + active <- forkIO exec >>= newMVar + -- debounced child thread, makes infinite loops less overwhelming + lock <- newEmptyMVar + void $ forkIO $ forever $ do + takeMVar lock + killThread =<< swapMVar active =<< forkIO exec + threadDelay (300 * 1000) + return $ putMVar lock () + +runTask :: Task -> [String] -> Maid () +runTask task args = do + displayCommand ("maid " <> tName task) runLang (tLang task) args (tCode task) runLang :: Text -> [String] -> Text -> Maid () @@ -181,6 +236,8 @@ data Context = Context { ctxTaskfile :: (FilePath, [Task]) , ctxDryRun :: Bool , ctxQuiet :: Bool + , ctxWatch :: [FilePath] + , ctxWatchRestart :: Bool , ctxStyle :: Style }