{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK prune #-}

{- |
Helpers for watching files for changes and taking action in the event of a
change.
-}
module Core.Program.Notify
    ( waitForChange
    ) where

import Control.Concurrent.MVar (newEmptyMVar, putMVar, readMVar)
import Control.Monad.IO.Class (liftIO)
import Core.Data.Structures
import Core.Program.Execute
import Core.Program.Logging
import Core.Program.Unlift
import Data.Foldable (foldrM)
import System.Directory (canonicalizePath)
import System.FSNotify (Event (..), eventPath, watchDir, withManager)
import System.FilePath (dropFileName)

{- |
Watch for changes to a given list of files.

Before continuing we insert a 100ms pause to allow whatever the editor was to
finish its write and switcheroo sequence.
-}

--
-- Ideally we'd just set up inotifies on these individual files, but that
-- doesn't work when programs like vim move the original file, save a new one,
-- then delete the renamed original.
--
-- From previous work we know that @CLOSE_WRITE@ is emitted reliably by inotify
-- on Linux through these sequences. We need to continue testing to assure
-- ourselves that the __fsnotify__ package's @Modify@ represents this
-- accurately.
--
waitForChange :: [FilePath] -> Program τ ()
waitForChange :: forall τ. [FilePath] -> Program τ ()
waitForChange [FilePath]
files =
    let f :: FilePath -> Set FilePath -> Set FilePath
        f :: FilePath -> Set FilePath -> Set FilePath
f FilePath
path Set FilePath
acc = FilePath -> Set FilePath -> Set FilePath
forall ε. Key ε => ε -> Set ε -> Set ε
insertElement FilePath
path Set FilePath
acc

        g :: FilePath -> Set FilePath -> Set FilePath
        g :: FilePath -> Set FilePath -> Set FilePath
g FilePath
path Set FilePath
acc = FilePath -> Set FilePath -> Set FilePath
forall ε. Key ε => ε -> Set ε -> Set ε
insertElement (FilePath -> FilePath
dropFileName FilePath
path) Set FilePath
acc
    in  do
            Rope -> Program τ ()
forall τ. Rope -> Program τ ()
info Rope
"Watching for changes"

            canonical <- (FilePath -> Program τ FilePath)
-> [FilePath] -> Program τ [FilePath]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
forall (m :: * -> *) a b. Monad m => (a -> m b) -> [a] -> m [b]
mapM (IO FilePath -> Program τ FilePath
forall a. IO a -> Program τ a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO FilePath -> Program τ FilePath)
-> (FilePath -> IO FilePath) -> FilePath -> Program τ FilePath
forall b c a. (b -> c) -> (a -> b) -> a -> c
. FilePath -> IO FilePath
canonicalizePath) [FilePath]
files
            let paths = (FilePath -> Set FilePath -> Set FilePath)
-> Set FilePath -> [FilePath] -> Set FilePath
forall a b. (a -> b -> b) -> b -> [a] -> b
forall (t :: * -> *) a b.
Foldable t =>
(a -> b -> b) -> b -> t a -> b
foldr FilePath -> Set FilePath -> Set FilePath
f Set FilePath
forall ε. Key ε => Set ε
emptySet [FilePath]
canonical
            let dirs = (FilePath -> Set FilePath -> Set FilePath)
-> Set FilePath -> [FilePath] -> Set FilePath
forall a b. (a -> b -> b) -> b -> [a] -> b
forall (t :: * -> *) a b.
Foldable t =>
(a -> b -> b) -> b -> t a -> b
foldr FilePath -> Set FilePath -> Set FilePath
g Set FilePath
forall ε. Key ε => Set ε
emptySet [FilePath]
files

            withContext $ \forall β. Program τ β -> IO β
runProgram -> do
                block <- IO (MVar Bool)
forall a. IO (MVar a)
newEmptyMVar
                withManager $ \WatchManager
manager -> do
                    -- setup watches
                    stoppers <-
                        (FilePath -> [IO ()] -> IO [IO ()])
-> [IO ()] -> Set FilePath -> IO [IO ()]
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> b -> m b) -> b -> t a -> m b
foldrM
                            ( \FilePath
dir [IO ()]
acc -> do
                                Program τ () -> IO ()
forall β. Program τ β -> IO β
runProgram (Rope -> FilePath -> Program τ ()
forall α τ. Show α => Rope -> α -> Program τ ()
debugS Rope
"watching" FilePath
dir)
                                stopper <-
                                    WatchManager -> FilePath -> ActionPredicate -> Action -> IO (IO ())
watchDir
                                        WatchManager
manager
                                        FilePath
dir
                                        ( \Event
trigger -> case Event
trigger of
                                            Modified FilePath
file UTCTime
_ EventIsDirectory
_ -> do
                                                if FilePath -> Set FilePath -> Bool
forall ε. Key ε => ε -> Set ε -> Bool
containsElement FilePath
file Set FilePath
paths
                                                    then Bool
True
                                                    else Bool
False
                                            Event
_ -> Bool
False
                                        )
                                        ( \Event
trigger -> do
                                            Program τ () -> IO ()
forall β. Program τ β -> IO β
runProgram (Rope -> FilePath -> Program τ ()
forall α τ. Show α => Rope -> α -> Program τ ()
debugS Rope
"trigger" (Event -> FilePath
eventPath Event
trigger))
                                            MVar Bool -> Bool -> IO ()
forall a. MVar a -> a -> IO ()
putMVar MVar Bool
block Bool
False
                                        )
                                return (stopper : acc)
                            )
                            []
                            Set FilePath
dirs

                    -- wait
                    _ <- readMVar block

                    sequence_ stoppers
                    return ()

            sleepThread 0.1