@@ -1,25 +1,58 @@
1+-- Some code snippets are adapted from the fsnotify package.
2+-- http://hackage.haskell.org/package/fsnotify-0.3.0.1/
2-- Module : Streamly.Internal.FileSystem.Event.Windows
5-- Module : Streamly.Internal.FileSystem.Event.Windows
3-- Copyright : (c) 2020 Composewell Technologies
6-- Copyright : (c) 2020 Composewell Technologies
4−--
7+-- (c) 2012, Mark Dittmer
5−-- License : BSD3
8+-- License : BSD-3-Clause
6-- Maintainer : streamly@composewell.com
9-- Maintainer : streamly@composewell.com
7-- Stability : experimental
10-- Stability : experimental
8-- Portability : GHC
11-- Portability : GHC
15+-- Use 'watchTrees'or 'watchPaths' with a list of file system paths you want to
16+-- watch as argument. It returns a stream of 'Event' representing the file
10−-- Just report all events under the paths provided as arguments
17+-- system events occurring under the watched paths.
20+-- {-\# LANGUAGE MagicHash #-}
21+-- Stream.mapM_ (putStrLn . 'showEvent') $ 'watchTrees' [Array.fromCString\# "path"#]
24+-- 'Event' is an opaque type. Accessor functions (e.g. 'showEvent' above)
25+-- provided in this module are used to determine the attributes of the event.
29+-- For Windows reference documentation see:
31+-- * <https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-file_notify_information file notify information>
32+-- * <https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw read directory changes>
34+-- We try to keep the macOS\/Linux/Windows event handling APIs and defaults
35+-- semantically and syntactically as close as possible.
39+-- As per the Windows reference docs, the fs event notification API is
42+-- * Minimum supported client: Windows XP [desktop apps | UWP apps]
43+-- * Minimum supported server: Windows Server 2003 [desktop apps | UWP apps
12module Streamly.Internal.FileSystem.Event.Windows
45module Streamly.Internal.FileSystem.Event.Windows
14 -- * Subscribing to events
47 -- * Subscribing to events
16− -- ** Default configuration
49+ -- ** Default configuration
19 , Toggle (..)
52 , Toggle (..)
20− , setFlag
53+ , setFlag
21 , defaultConfig
54 , defaultConfig
22− , getConfigFlag
55+ , getConfigFlag
23 , setAllEvents
56 , setAllEvents
25 -- ** Watch Behavior
58 -- ** Watch Behavior
@@ -32,44 +65,44 @@ module Streamly.Internal.FileSystem.Event.Windows
32 , setModifiedAttribute
65 , setModifiedAttribute
33 , setModifiedSize
66 , setModifiedSize
34 , setModifiedLastWrite
67 , setModifiedLastWrite
35− , setModifiedSecurity
68+ , setModifiedSecurity
37 -- ** Watch APIs
70 -- ** Watch APIs
38 , watchPaths
71 , watchPaths
39 , watchPathsWith
72 , watchPathsWith
40 , watchTrees
73 , watchTrees
41− , watchTreesWith
74+ , watchTreesWith
43 -- * Handling Events
76 -- * Handling Events
44− , getRelPath
77+ , getRelPath
47 -- ** Item CRUD events
80 -- ** Item CRUD events
48 , isCreated
81 , isCreated
49− , isDeleted
82+ , isDeleted
50 , isMovedFrom
83 , isMovedFrom
51 , isMovedTo
84 , isMovedTo
52− , isModified
85+ , isModified
54 -- ** Exception Conditions
87 -- ** Exception Conditions
55 , isOverflow
88 , isOverflow
57 -- * Debugging
90 -- * Debugging
58− , showEvent
91+ , showEvent
62import Control.Monad.IO.Class (MonadIO(liftIO))
95import Control.Monad.IO.Class (MonadIO(liftIO))
63−import Data.Bits ((.|.), (.&.), complement)
96+import Data.Bits ((.|.), (.&.), complement)
64−import Data.Functor.Identity (runIdentity)
97+import Data.Functor.Identity (runIdentity)
65import Data.List.NonEmpty (NonEmpty)
98import Data.List.NonEmpty (NonEmpty)
66import Data.Word (Word8)
99import Data.Word (Word8)
67import Foreign.C.String (peekCWStringLen)
100import Foreign.C.String (peekCWStringLen)
68import Foreign.Marshal.Alloc (alloca, allocaBytes)
101import Foreign.Marshal.Alloc (alloca, allocaBytes)
69−import Foreign.Storable (peekByteOff)
102+import Foreign.Storable (peekByteOff)
70−import Foreign.Ptr (Ptr, FunPtr, castPtr, nullPtr, nullFunPtr, plusPtr)
103+import Foreign.Ptr (Ptr, FunPtr, castPtr, nullPtr, nullFunPtr, plusPtr)
71import Streamly.Prelude (SerialT, parallel)
104import Streamly.Prelude (SerialT, parallel)
72−import System.Win32.File
105+import System.Win32.File
73 ( FileNotificationFlag
106 ( FileNotificationFlag
74 , LPOVERLAPPED
107 , LPOVERLAPPED
75 , closeHandle
108 , closeHandle
@@ -81,10 +114,10 @@ import System.Win32.File
81 , fILE_NOTIFY_CHANGE_ATTRIBUTES
114 , fILE_NOTIFY_CHANGE_ATTRIBUTES
82 , fILE_NOTIFY_CHANGE_SIZE
115 , fILE_NOTIFY_CHANGE_SIZE
83 , fILE_NOTIFY_CHANGE_LAST_WRITE
116 , fILE_NOTIFY_CHANGE_LAST_WRITE
84− , fILE_NOTIFY_CHANGE_SECURITY
117+ , fILE_NOTIFY_CHANGE_SECURITY
85 , fILE_SHARE_READ
118 , fILE_SHARE_READ
86 , fILE_SHARE_WRITE
119 , fILE_SHARE_WRITE
87− , oPEN_EXISTING
120+ , oPEN_EXISTING
89import System.Win32.Types (BOOL, DWORD, HANDLE, LPVOID, LPDWORD, failIfFalse_)
122import System.Win32.Types (BOOL, DWORD, HANDLE, LPVOID, LPDWORD, failIfFalse_)
90import qualified Data.List.NonEmpty as NonEmpty
123import qualified Data.List.NonEmpty as NonEmpty
@@ -100,7 +133,7 @@ import Streamly.Internal.Data.Array.Storable.Foreign (Array)
101data Config = Config
134data Config = Config
102 { watchRec :: BOOL
135 { watchRec :: BOOL
103− , createFlags :: DWORD
136+ , createFlags :: DWORD
106-------------------------------------------------------------------------------
139-------------------------------------------------------------------------------
@@ -164,7 +197,7 @@ setModifiedAttribute = setFlag fILE_NOTIFY_CHANGE_ATTRIBUTES
164-- /Internal/
197-- /Internal/
166setModifiedSize :: Toggle -> Config -> Config
199setModifiedSize :: Toggle -> Config -> Config
167−setModifiedSize = setFlag fILE_NOTIFY_CHANGE_SIZE
200+setModifiedSize = setFlag fILE_NOTIFY_CHANGE_SIZE
169-- | Report when a file last write time is changed.
202-- | Report when a file last write time is changed.
@@ -173,7 +206,7 @@ setModifiedSize = setFlag fILE_NOTIFY_CHANGE_SIZE
173-- /Internal/
206-- /Internal/
175setModifiedLastWrite :: Toggle -> Config -> Config
208setModifiedLastWrite :: Toggle -> Config -> Config
176−setModifiedLastWrite = setFlag fILE_NOTIFY_CHANGE_LAST_WRITE
209+setModifiedLastWrite = setFlag fILE_NOTIFY_CHANGE_LAST_WRITE
178-- | Report when a file Security attributes is changed.
211-- | Report when a file Security attributes is changed.
@@ -182,7 +215,7 @@ setModifiedLastWrite = setFlag fILE_NOTIFY_CHANGE_LAST_WRITE
182-- /Internal/
215-- /Internal/
184setModifiedSecurity :: Toggle -> Config -> Config
217setModifiedSecurity :: Toggle -> Config -> Config
185−setModifiedSecurity = setFlag fILE_NOTIFY_CHANGE_SECURITY
218+setModifiedSecurity = setFlag fILE_NOTIFY_CHANGE_SECURITY
187-- | Set all events 'On' or 'Off'.
220-- | Set all events 'On' or 'Off'.
@@ -197,7 +230,7 @@ setAllEvents s =
197 . setModifiedAttribute s
230 . setModifiedAttribute s
198 . setModifiedSize s
231 . setModifiedSize s
199 . setModifiedLastWrite s
232 . setModifiedLastWrite s
200− . setModifiedSecurity s
233+ . setModifiedSecurity s
202defaultConfig :: Config
235defaultConfig :: Config
203defaultConfig = setAllEvents On $ Config {watchRec = True, createFlags = 0}
236defaultConfig = setAllEvents On $ Config {watchRec = True, createFlags = 0}
@@ -209,22 +242,22 @@ getConfigRecMode :: Config -> BOOL
209getConfigRecMode Config{..} = watchRec
242getConfigRecMode Config{..} = watchRec
211data Event = Event
244data Event = Event
212− { eventFlags :: DWORD
245+ { eventFlags :: DWORD
213− , eventRelPath :: String
246+ , eventRelPath :: String
214− , eventRootPath :: String
247+ , eventRootPath :: String
215 , totalBytes :: DWORD
248 , totalBytes :: DWORD
216− } deriving (Show, Ord, Eq)
249+ } deriving (Show, Ord, Eq)
218-- For reference documentation see:
251-- For reference documentation see:
220-- See https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-file_notify_information
253-- See https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-file_notify_information
221data FILE_NOTIFY_INFORMATION = FILE_NOTIFY_INFORMATION
254data FILE_NOTIFY_INFORMATION = FILE_NOTIFY_INFORMATION
222 { fniNextEntryOffset :: DWORD
255 { fniNextEntryOffset :: DWORD
223 , fniAction :: DWORD
256 , fniAction :: DWORD
224 , fniFileName :: String
257 , fniFileName :: String
225− } deriving Show
258+ } deriving Show
227−type LPOVERLAPPED_COMPLETION_ROUTINE =
260+type LPOVERLAPPED_COMPLETION_ROUTINE =
228 FunPtr ((DWORD, DWORD, LPOVERLAPPED) -> IO ())
261 FunPtr ((DWORD, DWORD, LPOVERLAPPED) -> IO ())
230-- | A handle for a watch.
263-- | A handle for a watch.
@@ -232,64 +265,80 @@ getWatchHandle :: FilePath -> IO (HANDLE, FilePath)
232getWatchHandle dir = do
265getWatchHandle dir = do
233 h <- createFile dir
266 h <- createFile dir
234 -- Access mode
267 -- Access mode
235− fILE_LIST_DIRECTORY
268+ fILE_LIST_DIRECTORY
236 -- Share mode
269 -- Share mode
237− (fILE_SHARE_READ .|. fILE_SHARE_WRITE)
270+ (fILE_SHARE_READ .|. fILE_SHARE_WRITE)
238 -- Security attributes
271 -- Security attributes
239− Nothing
272+ Nothing
240 -- Create mode, we want to look at an existing directory
273 -- Create mode, we want to look at an existing directory
241− oPEN_EXISTING
274+ oPEN_EXISTING
242 -- File attribute, NOT using OVERLAPPED since we work synchronously
275 -- File attribute, NOT using OVERLAPPED since we work synchronously
243− fILE_FLAG_BACKUP_SEMANTICS
276+ fILE_FLAG_BACKUP_SEMANTICS
244 -- No template file
277 -- No template file
245− Nothing
278+ Nothing
246 return (h, dir)
279 return (h, dir)
248-- For reference documentation see:
281-- For reference documentation see:
250-- See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
283-- See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
251−-- Note that this API uses UTF-16 for file system paths:
284+-- Note that this API uses UTF-16 for file system paths:
252-- 1. https://docs.microsoft.com/en-us/windows/win32/intl/unicode-in-the-windows-api
285-- 1. https://docs.microsoft.com/en-us/windows/win32/intl/unicode-in-the-windows-api
253-- 2. https://docs.microsoft.com/en-us/windows/win32/intl/unicode
286-- 2. https://docs.microsoft.com/en-us/windows/win32/intl/unicode
254−foreign import ccall safe
287+foreign import ccall safe
255 "windows.h ReadDirectoryChangesW" c_ReadDirectoryChangesW ::
288 "windows.h ReadDirectoryChangesW" c_ReadDirectoryChangesW ::
256− HANDLE -> LPVOID -> DWORD -> BOOL -> DWORD -> LPDWORD
289+ HANDLE
257− -> LPOVERLAPPED -> LPOVERLAPPED_COMPLETION_ROUTINE -> IO BOOL
296+ -> LPOVERLAPPED_COMPLETION_ROUTINE
259readDirectoryChangesW ::
299readDirectoryChangesW ::
260− HANDLE -> Ptr FILE_NOTIFY_INFORMATION -> DWORD
301+ -> Ptr FILE_NOTIFY_INFORMATION
261− -> BOOL -> FileNotificationFlag -> LPDWORD -> IO ()
304+ -> FileNotificationFlag
263readDirectoryChangesW h buf bufSize wst f br =
307readDirectoryChangesW h buf bufSize wst f br =
264− failIfFalse_ "ReadDirectoryChangesW" $ c_ReadDirectoryChangesW h (castPtr buf)
308+ let res = c_ReadDirectoryChangesW
265− bufSize wst f br nullPtr nullFunPtr
309+ h (castPtr buf) bufSize wst f br nullPtr nullFunPtr
310+ in failIfFalse_ "ReadDirectoryChangesW" res
267peekFNI :: Ptr FILE_NOTIFY_INFORMATION -> IO FILE_NOTIFY_INFORMATION
312peekFNI :: Ptr FILE_NOTIFY_INFORMATION -> IO FILE_NOTIFY_INFORMATION
268peekFNI buf = do
313peekFNI buf = do
269 neof <- peekByteOff buf 0
314 neof <- peekByteOff buf 0
270 acti <- peekByteOff buf 4
315 acti <- peekByteOff buf 4
271 fnle <- peekByteOff buf 8
316 fnle <- peekByteOff buf 8
317+ -- Note: The path is UTF-16 encoded C WChars, peekCWStringLen converts
318+ -- UTF-16 to UTF-32 Char String
272 fnam <- peekCWStringLen
319 fnam <- peekCWStringLen
273 -- start of array
320 -- start of array
274 (buf `plusPtr` 12,
321 (buf `plusPtr` 12,
275 -- fnle is the length in *bytes*, and a WCHAR is 2 bytes
322 -- fnle is the length in *bytes*, and a WCHAR is 2 bytes
276− fromEnum (fnle :: DWORD) `div` 2)
323+ fromEnum (fnle :: DWORD) `div` 2)
277 return $ FILE_NOTIFY_INFORMATION neof acti fnam
324 return $ FILE_NOTIFY_INFORMATION neof acti fnam
279−readChangeEvents :: Ptr FILE_NOTIFY_INFORMATION -> String -> DWORD -> IO [Event]
327+ Ptr FILE_NOTIFY_INFORMATION -> String -> DWORD -> IO [Event]
280−readChangeEvents pfni root bytesRet = do
328+readChangeEvents pfni root bytesRet = do
281 fni <- peekFNI pfni
329 fni <- peekFNI pfni
282 let entry = Event
330 let entry = Event
283− { eventFlags = fniAction fni
331+ { eventFlags = fniAction fni
284− , eventRelPath = fniFileName fni
332+ , eventRelPath = fniFileName fni
285 , eventRootPath = root
333 , eventRootPath = root
286 , totalBytes = bytesRet
334 , totalBytes = bytesRet
288− nioff = fromEnum $ fniNextEntryOffset fni
336+ nioff = fromEnum $ fniNextEntryOffset fni
289− entries <- if nioff == 0
338+ if nioff == 0
290− then return []
339+ then return []
291− else readChangeEvents (pfni `plusPtr` nioff) root bytesRet
340+ else readChangeEvents (pfni `plusPtr` nioff) root bytesRet
292− return $ entry :entries
341+ return $ entry : entries
294readDirectoryChanges ::
343readDirectoryChanges ::
295 String -> HANDLE -> Bool -> FileNotificationFlag -> IO [Event]
344 String -> HANDLE -> Bool -> FileNotificationFlag -> IO [Event]
@@ -300,7 +349,7 @@ readDirectoryChanges root h wst mask = do
300 readDirectoryChangesW h buffer (toEnum maxBuf) wst mask bret
349 readDirectoryChangesW h buffer (toEnum maxBuf) wst mask bret
301 bytesRet <- peekByteOff bret 0
350 bytesRet <- peekByteOff bret 0
302 readChangeEvents buffer root bytesRet
351 readChangeEvents buffer root bytesRet
304type FileAction = DWORD
353type FileAction = DWORD
306fILE_ACTION_ADDED :: FileAction
355fILE_ACTION_ADDED :: FileAction
@@ -322,19 +371,20 @@ eventStreamAggr :: (HANDLE, FilePath, Config) -> SerialT IO Event
322eventStreamAggr (handle, rootPath, cfg) = do
371eventStreamAggr (handle, rootPath, cfg) = do
323 let recMode = getConfigRecMode cfg
372 let recMode = getConfigRecMode cfg
324 flagMasks = getConfigFlag cfg
373 flagMasks = getConfigFlag cfg
325− S.concatMap S.fromList $ S.repeatM
374+ S.concatMap S.fromList $ S.repeatM
326 $ readDirectoryChanges rootPath handle recMode flagMasks
375 $ readDirectoryChanges rootPath handle recMode flagMasks
328−pathsToHandles ::
377+pathsToHandles ::
329 NonEmpty FilePath -> Config -> SerialT IO (HANDLE, FilePath, Config)
378 NonEmpty FilePath -> Config -> SerialT IO (HANDLE, FilePath, Config)
330pathsToHandles paths cfg = do
379pathsToHandles paths cfg = do
331 let pathStream = S.fromList (NonEmpty.toList paths)
380 let pathStream = S.fromList (NonEmpty.toList paths)
332 st2 = S.mapM getWatchHandle pathStream
381 st2 = S.mapM getWatchHandle pathStream
333− S.map (\(h, f) -> (h, f, cfg)) st2
382+ S.map (\(h, f) -> (h, f, cfg)) st2
335-------------------------------------------------------------------------------
384-------------------------------------------------------------------------------
336-- Utilities
385-- Utilities
337-------------------------------------------------------------------------------
386-------------------------------------------------------------------------------
338utf8ToString :: Array Word8 -> FilePath
388utf8ToString :: Array Word8 -> FilePath
339utf8ToString = runIdentity . S.toList . U.decodeUtf8 . A.toStream
389utf8ToString = runIdentity . S.toList . U.decodeUtf8 . A.toStream
@@ -357,17 +407,18 @@ closePathHandleStream = S.mapM_ (\(h, _, _) -> closeHandle h)
358-- /Internal/
408-- /Internal/
360−watchPathsWith ::
410+watchPathsWith ::
361− (Config -> Config)
411+ (Config -> Config)
362− -> NonEmpty (Array Word8)
412+ -> NonEmpty (Array Word8)
363 -> SerialT IO Event
413 -> SerialT IO Event
364watchPathsWith f paths =
414watchPathsWith f paths =
365 S.bracket before after (S.concatMapWith parallel eventStreamAggr)
415 S.bracket before after (S.concatMapWith parallel eventStreamAggr)
369− before = return $ pathsToHandles (utf8ToStringList paths)
419+ before =
370− (f $ setRecursiveMode False defaultConfig)
420+ let cfg = f $ setRecursiveMode False defaultConfig
421+ in return $ pathsToHandles (utf8ToStringList paths) cfg
371 after = liftIO . closePathHandleStream
422 after = liftIO . closePathHandleStream
373-- | Like 'watchPathsWith' but uses the 'defaultConfig' options.
424-- | Like 'watchPathsWith' but uses the 'defaultConfig' options.
@@ -389,17 +440,18 @@ watchPaths = watchPathsWith id
390-- /Internal/
441-- /Internal/
392−watchTreesWith ::
443+watchTreesWith ::
393− (Config -> Config)
444+ (Config -> Config)
394− -> NonEmpty (Array Word8)
445+ -> NonEmpty (Array Word8)
395− -> SerialT IO Event
446+ -> SerialT IO Event
396−watchTreesWith f paths =
447+watchTreesWith f paths =
397 S.bracket before after (S.concatMapWith parallel eventStreamAggr)
448 S.bracket before after (S.concatMapWith parallel eventStreamAggr)
401− before = return $ pathsToHandles (utf8ToStringList paths)
452+ before =
402− (f $ setRecursiveMode True defaultConfig)
453+ let cfg = f $ setRecursiveMode True defaultConfig
454+ in return $ pathsToHandles (utf8ToStringList paths) cfg
403 after = liftIO . closePathHandleStream
455 after = liftIO . closePathHandleStream
405-- | Like 'watchTreesWith' but uses the 'defaultConfig' options.
457-- | Like 'watchTreesWith' but uses the 'defaultConfig' options.
@@ -409,35 +461,78 @@ watchTreesWith f paths =
411watchTrees :: NonEmpty (Array Word8) -> SerialT IO Event
463watchTrees :: NonEmpty (Array Word8) -> SerialT IO Event
412−watchTrees = watchTreesWith id
464+watchTrees = watchTreesWith id
415getFlag :: DWORD -> Event -> Bool
466getFlag :: DWORD -> Event -> Bool
416getFlag mask Event{..} = eventFlags == mask
467getFlag mask Event{..} = eventFlags == mask
469+-- XXX Change the type to Array Word8 to make it compatible with other APIs.
471+-- | Get the file system object path for which the event is generated, relative
472+-- to the watched root. The path is a UTF-8 encoded array of bytes.
418getRelPath :: Event -> String
476getRelPath :: Event -> String
419getRelPath Event{..} = eventRelPath
477getRelPath Event{..} = eventRelPath
479+-- XXX Change the type to Array Word8 to make it compatible with other APIs.
481+-- | Get the watch root directory to which this event belongs.
421getRoot :: Event -> String
485getRoot :: Event -> String
422getRoot Event{..} = eventRootPath
486getRoot Event{..} = eventRootPath
488+-- XXX need to document the exact semantics of these.
490+-- | File/directory created in watched directory.
424isCreated :: Event -> Bool
494isCreated :: Event -> Bool
425isCreated = getFlag fILE_ACTION_ADDED
495isCreated = getFlag fILE_ACTION_ADDED
497+-- | File/directory deleted from watched directory.
427isDeleted :: Event -> Bool
501isDeleted :: Event -> Bool
428isDeleted = getFlag fILE_ACTION_REMOVED
502isDeleted = getFlag fILE_ACTION_REMOVED
504+-- | Generated for the original path when an object is moved from under a
505+-- monitored directory.
430isMovedFrom :: Event -> Bool
509isMovedFrom :: Event -> Bool
431isMovedFrom = getFlag fILE_ACTION_RENAMED_OLD_NAME
510isMovedFrom = getFlag fILE_ACTION_RENAMED_OLD_NAME
512+-- | Generated for the new path when an object is moved under a monitored
433isMovedTo :: Event -> Bool
517isMovedTo :: Event -> Bool
434isMovedTo = getFlag fILE_ACTION_RENAMED_NEW_NAME
518isMovedTo = getFlag fILE_ACTION_RENAMED_NEW_NAME
520+-- XXX This event is generated only for files and not directories?
522+-- | Determine whether the event indicates modification of an object within the
436isModified :: Event -> Bool
527isModified :: Event -> Bool
437isModified = getFlag fILE_ACTION_MODIFIED
528isModified = getFlag fILE_ACTION_MODIFIED
439−-- | If the buffer overflows, ReadDirectoryChangesW will still return true,
440−-- but the entire contents of the buffer are discarded.
530+-- | If the buffer overflows, entire contents of the buffer are discarded,
531+-- therefore, events are lost. The user application must scan everything under
532+-- the watched paths to know the current state.
441isOverflow :: Event -> Bool
536isOverflow :: Event -> Bool
442isOverflow Event{..} = totalBytes == 0
537isOverflow Event{..} = totalBytes == 0
@@ -447,11 +542,11 @@ isOverflow Event{..} = totalBytes == 0
448-- | Convert an 'Event' record to a String representation.
543-- | Convert an 'Event' record to a String representation.
449showEvent :: Event -> String
544showEvent :: Event -> String
450−showEvent ev@Event{..} =
545+showEvent ev@Event{..} =
451 "--------------------------"
546 "--------------------------"
452 ++ "\nRoot = " ++ show (getRoot ev)
547 ++ "\nRoot = " ++ show (getRoot ev)
453 ++ "\nPath = " ++ show (getRelPath ev)
548 ++ "\nPath = " ++ show (getRelPath ev)
454− ++ "\nFlags " ++ show eventFlags
549+ ++ "\nFlags " ++ show eventFlags
455 ++ showev isOverflow "Overflow"
550 ++ showev isOverflow "Overflow"
456 ++ showev isCreated "Created"
551 ++ showev isCreated "Created"
457 ++ showev isDeleted "Deleted"
552 ++ showev isDeleted "Deleted"
@@ -460,4 +555,4 @@ showEvent ev@Event{..} =
460 ++ showev isMovedTo "MovedTo"
555 ++ showev isMovedTo "MovedTo"
463− where showev f str = if f ev then "\n" ++ str else ""
558+ where showev f str = if f ev then "\n" ++ str else ""