diff --git a/Display.hs b/Display.hs
new file mode 100644
--- /dev/null
+++ b/Display.hs
@@ -0,0 +1,99 @@
+module Display (
+    TermColor,
+    parseColor,
+    errMsg,
+    output,
+    reset
+  ) where
+
+import System.IO
+import Data.Char
+import Util
+
+type TermColor = [Int]
+
+displayColor :: TermColor -> String
+displayColor [] = ""
+displayColor c =
+  "\ESC[" ++ (join ';' (map show c)) ++ "m"
+
+displayResetColor = displayColor [colorReset]
+
+colorReset = colorValue "reset"
+colorValue "reset"	= 0
+colorValue "br" 	= 1
+--colorValue "dark"	= 2
+colorValue "so"		= 3
+colorValue "hl"		= 3
+colorValue "ul"		= 4
+colorValue "bl"		= 5
+colorValue "rev"	= 7
+colorValue "hidden"	= 8
+colorValue "nobr"	= 22--21
+--colorValue "nodark"	= 22
+colorValue "noso"	= 23
+colorValue "nohl"	= 23
+colorValue "noul"	= 24
+colorValue "nobl"	= 25
+colorValue "norev"	= 27
+colorValue "nohidden"	= 28
+colorValue "black"	= 30
+colorValue "red"	= 31
+colorValue "green"	= 32
+colorValue "yellow"	= 33
+colorValue "blue"	= 34
+colorValue "magenta"	= 35
+colorValue "cyan"	= 36
+colorValue "white"	= 37
+--colorValue ('/':c)	= 10 + colorValue c
+colorValue "/black"	= 40
+colorValue "/red"	= 41
+colorValue "/green"	= 42
+colorValue "/yellow"	= 43
+colorValue "/blue"	= 44
+colorValue "/magenta"	= 45
+colorValue "/cyan"	= 46
+colorValue "/white"	= 47
+
+colorValue "normal"	= colorValue "reset"
+colorValue "bold"	= colorValue "br"
+colorValue "nobold"	= colorValue "nobr"
+colorValue "bright"	= colorValue "br"
+colorValue "nobright"	= colorValue "nobr"
+colorValue "dim"	= colorValue "dark"
+colorValue "nodim"	= colorValue "nodark"
+colorValue "standout"	= colorValue "so"
+colorValue "nostandout"	= colorValue "noso"
+colorValue "hilite"	= colorValue "hl"
+colorValue "nohilite"	= colorValue "nohl"
+colorValue "underline"  = colorValue "ul"
+colorValue "nounderline"= colorValue "noul"
+colorValue "blink"	= colorValue "bl"
+colorValue "noblink"	= colorValue "nobl"
+colorValue "reverse"	= colorValue "rev"
+colorValue "noreverse"	= colorValue "norev"
+
+colorValue x = error ("unknown color name: " ++ x)
+
+colorSep = not . isAlphaNum
+
+parseColor :: String -> TermColor
+parseColor [] = []
+parseColor ('/':s) =
+  colorValue ('/':x) : parseColor r
+  where
+    (x, r) = break colorSep s
+parseColor s@(c:s')
+  | colorSep c = parseColor s'
+  | otherwise = colorValue x : parseColor r
+      where
+	(x, r) = break colorSep s
+
+errMsg :: String -> IO ()
+errMsg m = putStr displayResetColor >> hPutStrLn stderr m
+
+output :: TermColor -> String -> IO ()
+output c m = putStrLn (displayColor (colorReset : c) ++ m)
+
+reset :: IO ()
+reset = putStr displayResetColor
diff --git a/Main.hs b/Main.hs
new file mode 100644
--- /dev/null
+++ b/Main.hs
@@ -0,0 +1,193 @@
+{-# LANGUAGE CPP #-}
+import System.Exit
+import Control.Monad
+import qualified System.Environment
+import qualified System.Console.GetOpt as GetOpt
+import System.Posix.Signals (installHandler, sigINT, Handler(..))
+import qualified GHC.IOBase
+import qualified Control.Concurrent
+import qualified Control.Exception
+import qualified Data.IORef
+import qualified Text.Regex
+#ifdef INOTIFY
+import qualified System.INotify as INotify
+#endif
+
+import Util
+import Display
+import TailTypes
+import Tail
+import TailHandle
+
+data Options = Options{
+  optionTails :: [Tail],
+  optionTail :: Tail,
+  optionMatch :: TailMatch
+}
+
+defaultTail = Tail{
+  tailTarg = undefined, -- read "-",
+  tailPollInterval = read "5",
+  tailReopenInterval = read "0",
+#ifdef INOTIFY
+  tailPollINotify = True,
+  tailReopenINotify = False,
+#endif
+  tailBegin = False,
+  tailTimeFmt = "%c",
+  tailMatches = []
+}
+
+defaultOptions = Options{
+  optionTails = [],
+  optionTail = defaultTail,
+  optionMatch = MatchAll
+}
+
+set_opt :: (Tail -> Tail) -> Options -> Options
+set_opt p o = o{ optionTail = p $ optionTail o }
+
+set_match :: TailMatch -> Options -> Options
+set_match m o = o{ optionMatch = m }
+
+add_action :: TailAction -> Options -> Options
+add_action a o = set_opt add o where
+  add t = t{ tailMatches = (optionMatch o, a) : (tailMatches t) }
+
+prog_header = "Usage: ztail [OPTIONS] FILE ...\n\
+Follow the specified files (ala tail -f).  FILE may be a path, '-' for stdin,\n\
+or '&N' for file descriptor N.  OPTIONS apply only to the following FILE\n\
+(except for -irt), and match options (-amn) apply to all following actions\n\
+(-hcdse).  Actions involving TEXT can contain the following \\-escapes:\n\
+    \\0 current file        \\@ current time (from -t)\n\
+    \\_ current line        \\` \\' pre- and post-matching text\n\
+    \\& matching text       \\N (1-9) group in match\n\
+\&" --"
+prog_usage = GetOpt.usageInfo prog_header prog_options
+
+prog_options :: [GetOpt.OptDescr (Options -> Options)]
+prog_options = [
+    GetOpt.Option "b" ["begin"]
+      (GetOpt.NoArg (set_opt $ \p -> p{ tailBegin = True }))
+      ("start reading at the beginning of the file (rather than only new lines at the end)"),
+    GetOpt.Option "i" ["interval"]
+      (GetOpt.ReqArg (\i -> set_opt $ \p -> p{ tailPollInterval = read i }) "INT")
+      ("poll for data every INT seconds [" ++ show (tailPollInterval defaultTail) ++ "] on all following files"),
+    GetOpt.Option "r" ["reopen"]
+      (GetOpt.OptArg (\i -> set_opt $ \p -> p{ tailReopenInterval = maybe (tailPollInterval p) read i }) "INT")
+      ("check file name (like tail -F) every INT seconds or every poll"),
+    GetOpt.Option "t" ["timefmt"]
+      (GetOpt.ReqArg (\t -> set_opt $ \p -> p{ tailTimeFmt = t }) "FMT")
+      ("set time format for \\@ substitution (in strftime(3)) [" ++ tailTimeFmt defaultTail ++ "]"),
+    GetOpt.Option "T" ["timestamp"]
+      (GetOpt.OptArg (maybe id $ \t -> add_action (ActionSubst "\\@ \\_") . set_opt (\p -> p{ tailTimeFmt = t })) "FMT")
+      ("timestamp with FMT; equivalent to: [-t FMT] -h '\\@ '"),
+#ifdef INOTIFY
+    GetOpt.Option "I" ["inotify"]
+      (GetOpt.NoArg (set_opt $ \p -> p{ tailPollINotify = True, tailPollInterval = read "0" }))
+      ("use inotify to poll for new data (only for regular files)"),
+    GetOpt.Option "R" ["ireopen"]
+      (GetOpt.NoArg (set_opt $ \p -> p{ tailReopenINotify = True }))
+      ("use inotify to monitor file renames (only for preexisting, leaf files)"),
+#endif
+
+    GetOpt.Option "a" ["all"]
+      (GetOpt.NoArg (set_match MatchAll))
+      ("perform following action for every line from this FILE (default)"),
+    GetOpt.Option "m" ["match"]
+      (GetOpt.ReqArg (\m -> set_match $ MatchRegex $ Text.Regex.mkRegexWithOpts m False True) "REGEX")
+      ("perform following action for each line matching REGEX"),
+    GetOpt.Option "M" ["imatch"]
+      (GetOpt.ReqArg (\m -> set_match $ MatchRegex $ Text.Regex.mkRegexWithOpts m False False) "REGEX")
+      ("perform following action for each line matching REGEX (case-insensitive)"),
+    GetOpt.Option "n" ["no-match"]
+      (GetOpt.ReqArg (\m -> set_match $ MatchNotRegex $ Text.Regex.mkRegexWithOpts m False True) "REGEX")
+      ("perform following action for each line not matching REGEX"),
+    GetOpt.Option "N" ["no-imatch"]
+      (GetOpt.ReqArg (\m -> set_match $ MatchNotRegex $ Text.Regex.mkRegexWithOpts m False False) "REGEX")
+      ("perform following action for each line not matching REGEX (case-insensitive)"),
+
+    GetOpt.Option "h" ["header"]
+      (GetOpt.ReqArg (\h -> add_action $ ActionSubst (h ++ "\\_")) "TEXT")
+      ("display TEXT header before (matching) lines (same as -s 'TEXT\\_')"),
+    GetOpt.Option "c" ["color"]
+      (GetOpt.ReqArg (\c -> add_action $ ActionColor $ parseColor c) "COLOR")
+      ("display (matching) lines in COLOR (valid colors are: normal, br,ul,bl,rev,hidden, nobr,noul..., black,red,green,yellow,blue,magenta,cyan,white, /black,/red,...)"),
+    GetOpt.Option "d" ["hide"]
+      (GetOpt.NoArg (add_action ActionHide))
+      ("hide (matching) lines"),
+    GetOpt.Option "s" ["substitute"]
+      (GetOpt.ReqArg (\s -> add_action $ ActionSubst s) "TEXT")
+      ("substitute (matching) lines with TEXT"),
+    GetOpt.Option "e" ["execute"]
+      (GetOpt.ReqArg (\e -> add_action $ ActionExecute e) "PROG")
+      ("execute PROG for every (matching) line")
+  ]
+prog_arg a Options{ optionTails = l, optionTail = t } =
+  Options{
+    optionTails = t{
+      tailTarg = read a,
+      tailMatches = reverse (tailMatches t)
+    } : l,
+    optionTail = t{
+      tailBegin = False,
+      tailMatches = []
+    },
+    optionMatch = MatchAll
+  }
+
+run :: [Tail] -> IO (Control.Concurrent.MVar ExitCode)
+run tails = do
+  emv <- Control.Concurrent.newEmptyMVar
+  let exit = Control.Concurrent.putMVar emv
+  let exit0 = exit ExitSuccess
+  let exit1 = exit (ExitFailure 1)
+  installHandler sigINT (CatchOnce exit0) Nothing
+
+  lockv <- Control.Concurrent.newQSem 0
+  let lock = Control.Exception.bracket_ (Control.Concurrent.waitQSem lockv) (Control.Concurrent.signalQSem lockv)
+  let unlock = Control.Exception.bracket_ (Control.Concurrent.signalQSem lockv) (Control.Concurrent.waitQSem lockv)
+
+  count <- Data.IORef.newIORef (length tails)
+#ifdef INOTIFY
+  inotify <- 
+    catchOnlyIOET GHC.IOBase.UnsupportedOperation 
+      (Just =.< INotify.initINotify) 
+      (return Nothing)
+#endif
+
+  let error t e = if isint e then exit0 else tailErrMsg t (show e) >> exit1
+      runt = runTail TailRuntime
+	{ trText = tailText
+	, trUnlock = unlock
+#ifdef INOTIFY
+	, trINotify = inotify
+#endif
+	}
+  mapM_ (\t -> Control.Concurrent.forkIO $ lock $ Control.Exception.handle (error t) $ do
+      runt t
+      i <- Data.IORef.atomicModifyIORef count (\i -> (pred i, i))
+      when (i == 1) exit0)
+    tails
+  Control.Concurrent.signalQSem lockv
+  return emv
+  where
+    isint (Control.Exception.IOException e) = GHC.IOBase.ioe_type e == GHC.IOBase.Interrupted
+    isint _ = False
+
+main = do
+  args <- System.Environment.getArgs
+  tails <- case (GetOpt.getOpt (GetOpt.ReturnInOrder prog_arg) prog_options args) of
+    (s, [], []) -> case optionTails $ foldl (\s t -> t s) defaultOptions s of
+      [] -> do
+	putStrLn prog_usage
+	exitWith ExitSuccess
+      t -> return t
+    (_, _, err) -> do
+      mapM_ putStrLn err
+      putStrLn prog_usage
+      exitFailure
+  e <- run tails >>= Control.Concurrent.takeMVar
+  when (e == ExitSuccess) $
+    errMsg "ztail: done"
+  exitWith e
diff --git a/README b/README
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,15 @@
+An even more improved version of xtail/tail -f.
+
+Requirements:
+ - ghc
+ - hinotify (http://hackage.haskell.org/cgi-bin/hackage-scripts/package/hinotify)
+
+Example:
+  ztail -I -r300 \
+	-c bright,red /var/log/kernel \
+	-c yellow /var/log/daemon \
+	-a -c blue -t '%b %d %H:%M:%S' -h '\@ ' /var/log/Xorg.0.log
+
+https://dylex.net:9947/src
+
+Suggestions, improvements, patches, complaints welcome.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/Tail.hs b/Tail.hs
new file mode 100644
--- /dev/null
+++ b/Tail.hs
@@ -0,0 +1,82 @@
+module Tail (
+    tailText
+  ) where
+
+import Data.Array (Array, array, (//), (!))
+import Text.Regex
+import qualified Data.Char
+import qualified System.Exit
+import qualified System.Cmd
+import qualified Data.Time.Clock
+import qualified Data.Time.LocalTime
+import qualified Data.Time.Format
+import qualified System.Locale
+
+import Util
+import Display
+import TailTypes
+
+type Substitutions = Array Char String
+default_subst :: Substitutions
+default_subst = array ('\0','\127') [('\\',"\\")]
+
+timeFmt :: String -> IO String
+timeFmt f = do
+  t <- Data.Time.Clock.getCurrentTime
+  z <- Data.Time.LocalTime.getCurrentTimeZone
+  return $ Data.Time.Format.formatTime System.Locale.defaultTimeLocale f (Data.Time.LocalTime.utcToLocalTime z t)
+
+shellEscape :: String -> String
+shellEscape [] = []
+shellEscape (c:l)
+  | Data.Char.isAlphaNum c = c: shellEscape l
+  | otherwise = '\\':c: shellEscape l
+
+substText :: Substitutions -> (String -> String) -> String -> String
+substText sub f str = go str
+  where
+    go ('\\':x:l) = f (sub!x) ++ go l
+    go (x:l) = x : go l
+    go [] = []
+
+matchText :: Substitutions -> TailMatch -> String -> Maybe Substitutions
+matchText sub MatchAll t = Just (sub // [('_',t)])
+matchText sub (MatchRegex m) t =
+  case matchRegexAll m t of
+    Nothing -> Nothing
+    Just (pre, mat, post, exps) ->
+      Just (sub
+	// [('_',t), ('`',pre), ('&',mat), ('\'',post)]
+	// zip ['1'..'9'] exps)
+matchText sub (MatchNotRegex m) t =
+  case matchRegex m t of
+    Nothing -> Just (sub // [('_',t)])
+    Just _ -> Nothing
+
+execute :: Tail -> String -> IO System.Exit.ExitCode
+execute th e =
+  tailErrMsg th ("execute: " ++ e) >>
+  System.Cmd.system e
+
+processText :: Tail -> String -> IO ()
+processText t x = do
+  now <- timeFmt $ tailTimeFmt t
+  let init_sub = default_subst // [('0',tailName t),('@',now)] 
+  case foldl mact (Just x, init_sub, [], []) (tailMatches t) of
+    (Nothing, _, _, _) -> nop
+    (Just out, _, color, exec) ->
+      output color out >>
+      mapM_ (execute t) exec
+  where
+    mact r@(Nothing, _, _, _) _ = r
+    mact r@(Just s, sub, cl, el) (m, a) =
+      case matchText sub m s of
+	Nothing -> r
+	Just sub -> case a of
+	  ActionNone -> (Just s, sub, cl, el)
+	  ActionHide -> (Nothing, sub, cl, el)
+	  ActionColor c -> (Just s, sub, c ++ cl, el)
+	  ActionSubst s' -> (Just (substText sub id s'), sub, cl, el)
+	  ActionExecute e -> (Just s, sub, cl, (substText sub shellEscape e) : el)
+
+tailText = processText
diff --git a/TailHandle.hs b/TailHandle.hs
new file mode 100644
--- /dev/null
+++ b/TailHandle.hs
@@ -0,0 +1,261 @@
+{-# LANGUAGE CPP, DeriveDataTypeable #-}
+module TailHandle (
+  runTail
+  ) where
+
+import Control.Monad
+import System.Posix.Types
+import System.Posix.Files
+import System.Posix.IO
+import qualified Data.Maybe
+import qualified GHC.Handle (SeekMode(AbsoluteSeek))
+import qualified GHC.IOBase
+import Control.Concurrent
+import qualified Control.Exception
+import qualified Data.Typeable
+#ifdef INOTIFY
+import qualified System.INotify as INotify
+#endif
+import Data.IORef
+
+import Util
+import TailTypes
+
+data TailHandle = TailHandle{
+  thTail :: Tail,
+  thRuntime :: TailRuntime,
+  thPoll :: ThreadId,
+  thReopen :: Maybe ThreadId,
+  thFd :: Maybe Fd,
+  thPos :: FileOffset,
+  thIno :: Maybe FileID,
+  thBuf :: String,
+  thAgain :: Bool,
+#ifdef INOTIFY
+  thPollWatch :: Maybe INotify.WatchDescriptor,
+  thReopenWatch :: Maybe INotify.WatchDescriptor,
+#endif
+  thSigPending :: IORef (Maybe TailSignal),
+  thSigHandler :: IO () -> IO ()
+}
+
+data TailSignal = PollSignal | ReopenSignal deriving (Show, Data.Typeable.Typeable, Eq, Ord)
+
+thErrMsg = tailErrMsg . thTail
+
+catchDoesNotExist :: IO a -> IO (Maybe a)
+catchDoesNotExist f = catchOnlyIOET GHC.IOBase.NoSuchThing (liftM Just f) (return Nothing)
+
+bad = ioError . userError
+
+closeTail :: TailHandle -> IO TailHandle
+closeTail th@TailHandle{ thFd = Nothing } = return th
+closeTail th@TailHandle{ thFd = Just fd } = do
+#ifdef INOTIFY
+  whenJust rm_watch (thPollWatch th)
+  whenJust rm_watch (thReopenWatch th)
+#endif
+  closeFd fd
+  return th{
+    thFd = Nothing
+  , thPos = 0
+  , thIno = Nothing
+#ifdef INOTIFY
+  , thPollWatch = Nothing
+  , thReopenWatch = Nothing 
+#endif
+  }
+#ifdef INOTIFY
+  where
+    rm_watch = INotify.removeWatch (Data.Maybe.fromJust (trINotify (thRuntime th)))
+#endif
+
+seekTail :: FileOffset -> TailHandle -> IO TailHandle
+seekTail _ TailHandle{ thFd = Nothing } = bad "seek on closed fd"
+seekTail c th@TailHandle{ thFd = Just fd } =
+  fdSeek fd GHC.Handle.AbsoluteSeek c >. th{ thPos = c }
+
+inotifyTail :: TailHandle -> IO TailHandle
+#ifdef INOTIFY
+inotifyTail th@TailHandle{ 
+    thRuntime = TailRuntime{ trINotify = Just inotify },
+    thPoll = tid,
+    thTail = Tail{
+      tailTarg = TailPath path,
+      tailPollINotify = ipoll,
+      tailReopenINotify = ireopen
+    } } = do
+  poll <- justWhen ipoll $
+    INotify.addWatch inotify [INotify.Modify] path 
+      (\_ -> Control.Exception.throwDynTo tid PollSignal)
+  reopen <- justWhen ireopen $
+    INotify.addWatch inotify [INotify.MoveSelf] path 
+      (\_ -> Control.Exception.throwDynTo tid ReopenSignal)
+  return th{
+    thPollWatch = poll,
+    thReopenWatch = reopen
+  }
+#endif
+inotifyTail th = return th
+
+openTail :: TailHandle -> IO TailHandle
+openTail th@TailHandle{ thFd = Nothing } = get (tailTarg (thTail th)) where
+  get (TailFd fd) = got (Just fd)
+  get (TailPath path) = got =<<
+    catchDoesNotExist (
+      openFd path ReadOnly Nothing OpenFileFlags{
+	append = False, exclusive = False, noctty = False, nonBlock = True, trunc = False
+      })
+  got Nothing = thErrMsg th "No such file or directory" >. th{ thPos = 0 }
+  got (Just fd) = do
+    setFdOption fd NonBlockingRead True
+    go fd =<< getFdStatus fd
+  go fd stat
+    | isBlockDevice stat || isDirectory stat || isSymbolicLink stat =
+      closeFd fd >> bad "unsupported file type"
+    | isRegularFile stat =
+      inotifyTail th' >>= seekTail (if pos < 0 then max 0 $ sz + 1 + pos else min sz pos)
+    | otherwise =
+      return th'{ thPos = -1 }
+    where
+      th' = th{ thFd = Just fd, thIno = Just (fileID stat), thAgain = True }
+      sz = fileSize stat
+      pos = thPos th
+openTail _ = bad "open on opened tail"
+
+reopenTail :: TailHandle -> IO TailHandle
+reopenTail th@TailHandle{ thTail = Tail{ tailTarg = TailPath path }, thIno = ino } = do
+  stat <- catchDoesNotExist $ getFileStatus path
+  case stat of
+    Nothing -> return th
+    Just _ | ino == Nothing -> openTail th
+    Just stat | ino == Just (fileID stat) -> return th
+    Just stat | fileSize stat == 0 -> return th
+    _ -> do
+      thErrMsg th "Following new file"
+      closeTail th >>= openTail
+reopenTail th = return th
+
+noRead :: TailHandle -> (TailHandle, [String])
+noRead th = (th{ thAgain = False }, [])
+
+bufsiz :: ByteCount
+bufsiz = 8192
+
+readTail :: TailHandle -> IO (TailHandle, [String])
+readTail th@TailHandle{ thFd = Nothing } = return (noRead th)
+readTail th@TailHandle{ thFd = Just fd, thPos = pos } =
+  if pos == -1
+    then catchOnlyIOET GHC.IOBase.EOF readsock $ do
+      checkbuf th >.= noRead
+      -- thErrMsg th "closed?"
+      -- closeTail th >.= noRead th
+    else getFdStatus fd >.= fileSize >>= gotlen
+  where
+    checkbuf th@TailHandle{ thBuf = buf } = do
+      when (buf /= "") $
+	thErrMsg th ("Unterminated line: " ++ buf)
+      return th{ thBuf = "" }
+    gotlen len
+      | len < pos = do
+          thErrMsg th ("File truncated to " ++ show len)
+	  seekTail 0 th >>= readTail
+      | len == pos = do
+	  checkbuf th >.= noRead
+      | otherwise = do
+	  let count = min (fromIntegral (len - pos)) bufsiz
+	  (buf, buflen) <- readit (fromIntegral count)
+	  when (buflen /= count) $
+	    thErrMsg th ("Short read (" ++ show buflen ++ "/" ++ show count ++ ")")
+	  return $ gotbuf th{ thPos = pos + fromIntegral buflen, thAgain = buflen == bufsiz } buf
+    readsock =
+      readit bufsiz >.= fst >.= gotbuf th{ thAgain = True }
+    gotbuf th "" = noRead th
+    gotbuf th@TailHandle{ thBuf = oldbuf } buf =
+      case initlast $ split (== '\n') buf of
+	([], r) ->
+	  (th{ thBuf = oldbuf ++ r }, [])
+	(l1 : l, r) ->
+	  (th{ thBuf = r }, (oldbuf ++ l1) : l)
+    readit len = catchOnlyIOET GHC.IOBase.ResourceExhausted 
+      (fdRead fd len)
+      (return ("", 0))
+
+pause :: IO ()
+pause = threadDelay (30*60*1000000) -- fixme how? (-1) doesn't work
+
+waitTail :: TailHandle -> IO ()
+waitTail TailHandle{ thFd = Nothing } = pause
+waitTail TailHandle{ thFd = Just fd, thPos = -1 } = threadWaitRead fd
+waitTail TailHandle{ thTail = t, thAgain = again } = do
+  if again
+    then yield 
+    else if i == 0
+      then pause
+      else threadDelay i
+  where 
+    i = fromInterval (tailPollInterval t)
+
+reopenThread :: Int -> ThreadId -> IO ()
+reopenThread ri tid = forever $ do
+  threadDelay ri
+  Control.Exception.throwDynTo tid ReopenSignal
+
+newTail :: TailRuntime -> Tail -> IO TailHandle
+newTail tr tail = do
+  tid <- myThreadId
+  rid <- justWhen (ri /= 0) $ forkIO (reopenThread ri tid)
+  sigpend <- newIORef Nothing
+  return TailHandle{
+    thTail = tail,
+    thRuntime = tr,
+    thPoll = tid,
+    thReopen = rid,
+    thFd = Nothing,
+    thPos = if tailBegin tail then 0 else -1,
+    thIno = Nothing,
+    thBuf = [],
+    thAgain = True,
+#ifdef INOTIFY
+    thPollWatch = Nothing,
+    thReopenWatch = Nothing,
+#endif
+    thSigPending = sigpend,
+    thSigHandler = (flip Control.Exception.catchDyn) (\s ->
+      modifyIORef sigpend (max (Just s)))
+  }
+  where 
+    ri = fromInterval (tailReopenInterval tail)
+
+runTail :: TailRuntime -> Tail -> IO ()
+runTail tr tail = Control.Exception.block $
+  newTail tr tail >>=
+  openTail >>= go >>= 
+  \TailHandle{ thReopen = wid } -> whenJust killThread wid
+  where
+    catch th ReopenSignal = reopenTail th -- >>= wait
+    catch th PollSignal = return th
+    wait th = do
+      sig <- readIORef (thSigPending th)
+      writeIORef (thSigPending th) Nothing
+      maybe 
+	(Control.Exception.catchDyn 
+	  (trUnlock tr $ waitTail th >. th) 
+	  (catch th))
+	(catch th)
+	sig
+    poll th
+      | thReopen th == Nothing
+	&& (thFd th == Nothing
+	  || (thAgain th == False && thPos th /= -1
+	    && fromInterval (tailPollInterval (thTail th)) == 0
+#ifdef INOTIFY
+	    && thPollWatch th == Nothing 
+	    && thReopenWatch th == Nothing
+#endif
+	    ))
+	= return th
+      | otherwise = wait th >>= go
+    go th = readTail th >>= proc >>= poll
+    proc (th, s) = mapM_ (thSigHandler th . fun) s >. th
+    fun = (trText tr) tail
diff --git a/TailTypes.hs b/TailTypes.hs
new file mode 100644
--- /dev/null
+++ b/TailTypes.hs
@@ -0,0 +1,76 @@
+{-# LANGUAGE CPP, Rank2Types #-}
+module TailTypes (
+    Interval,
+    fromInterval,
+    TailTarget(..),
+    TailRuntime(..),
+    TailMatch(..), TailAction(..),
+    TailMatches,
+    Tail(..), 
+    tailName, tailErrMsg
+  ) where
+
+import qualified System.Posix.Types
+import qualified System.Posix.IO
+import qualified Text.Regex
+#ifdef INOTIFY
+import qualified System.INotify as INotify
+#endif
+
+import Display
+
+newtype Interval = Interval Int
+instance Show Interval where
+  show (Interval x) = show (fromIntegral x / 1000000.0)
+instance Read Interval where
+  readsPrec n s = map (\(x,s) -> (Interval $ floor (1000000.0 * x), s)) $ readsPrec n s
+fromInterval (Interval x) = x
+
+data TailTarget = TailPath FilePath | TailFd System.Posix.Types.Fd
+instance Show TailTarget where
+  show (TailFd 0) = "-"
+  show (TailFd x) = '&':(show x)
+  show (TailPath path) = path
+instance Read TailTarget where
+  readsPrec _ "-" = [(TailFd System.Posix.IO.stdInput, "")]
+  readsPrec n ('&':s) = map (\(f,s) -> (TailFd f, s)) $ readsPrec n s
+  readsPrec _ s = [(TailPath s, "")]
+
+data TailMatch =
+    MatchAll
+  | MatchRegex Text.Regex.Regex
+  | MatchNotRegex Text.Regex.Regex
+data TailAction =
+    ActionNone
+  | ActionHide
+  | ActionColor TermColor
+  | ActionSubst String
+  | ActionExecute String
+type TailMatches = [(TailMatch, TailAction)]
+
+data Tail = Tail{
+  tailTarg :: TailTarget,
+  tailPollInterval :: Interval,
+  tailReopenInterval :: Interval,
+#ifdef INOTIFY
+  tailPollINotify :: Bool,
+  tailReopenINotify :: Bool,
+#endif
+  tailBegin :: Bool,
+  tailTimeFmt :: String,
+  tailMatches :: TailMatches
+}
+
+tailName = show . tailTarg
+
+tailErrMsg t msg =
+  errMsg ("ztail " ++ tailName t ++ ": " ++ msg)
+
+data TailRuntime = TailRuntime
+  { trUnlock :: forall a. IO a -> IO a
+  , trText :: Tail -> String -> IO ()
+#ifdef INOTIFY
+  , trINotify :: Maybe INotify.INotify
+#endif
+}
+
diff --git a/Util.hs b/Util.hs
new file mode 100644
--- /dev/null
+++ b/Util.hs
@@ -0,0 +1,65 @@
+module Util (
+    split,
+    join,
+    initlast,
+    (>.=), (>.), (=.<),
+    nop,
+    justIf,
+    whenJust, justWhen,
+    catchOnlyIOET
+  ) where
+
+import qualified GHC.IOBase
+import Control.Monad hiding (join)
+
+split :: (a -> Bool) -> [a] -> [[a]]
+{-# SPECIALIZE split :: (Char -> Bool) -> String -> [String] #-}
+split p = splitp where
+  splitp [] = [[]]
+  splitp (x:l)
+    | p x       = []     : splitp  l
+    | otherwise = (x:l1) : l'
+	where (l1:l') = splitp l
+
+join :: a -> [[a]] -> [a]
+{-# SPECIALIZE join :: Char -> [String] -> String #-}
+join p = joinp where
+  joinp [] = []
+  joinp [x] = x
+  joinp (x:l) = x ++ p : joinp l
+
+initlast :: [a] -> ([a], a)
+initlast [] = error "initlast: empty list"
+initlast [x] = ([], x)
+initlast (x:l) = (x:l1, r) where
+  (l1, r) = initlast l
+
+infixl 1 >., >.=
+infixr 1 =.<
+(>.) :: Monad m => m a -> b -> m b
+(>.=) :: Monad m => m a -> (a -> b) -> m b
+(=.<) :: Monad m => (a -> b) -> m a -> m b
+
+(>.) e r = e >> return r
+(>.=) e r = e >>= return . r
+(=.<) r e = return . r =<< e -- fmap, <$>, liftM
+
+nop :: Monad m => m ()
+nop = return ()
+
+whenJust :: Monad m => (a -> m ()) -> Maybe a -> m ()
+whenJust = maybe nop
+
+justIf :: Bool -> a -> Maybe a
+justIf False = const Nothing
+justIf True = Just
+
+justWhen :: Monad m => Bool -> m a -> m (Maybe a)
+justWhen False _ = return Nothing
+justWhen True r = Just =.< r
+
+catchOnlyIOET :: GHC.IOBase.IOErrorType -> IO a -> IO a -> IO a
+catchOnlyIOET t f h = catch f c where
+    c e 
+      | GHC.IOBase.ioe_type e == t = h
+      | otherwise = ioError e
diff --git a/ztail.cabal b/ztail.cabal
new file mode 100644
--- /dev/null
+++ b/ztail.cabal
@@ -0,0 +1,23 @@
+Name:		ztail
+Version:	1.0
+Author:		Dylan Simon
+Maintainer:     dylan@dylex.net
+License:        BSD3
+Synopsis:	Multi-file, colored, filtered log tailer.
+Description:	An even more improved version of xtail/tail -f, including inotify support, full regex-based filtering, substitution, and colorization.
+Category:	System,Console
+Build-Type:	Simple
+Cabal-Version:	>= 1.2
+tested-with:    GHC == 6.8.2, GHC == 6.10.1
+extra-source-files: README
+
+Flag INotify
+    Description:	Enable inotify support
+
+Executable ztail
+    Main-is:		Main.hs
+    Other-Modules:	Util, Display, TailTypes, TailHandle, Tail
+    Build-Depends:	base < 4, regex-compat, unix, time, old-locale, process, array
+    if flag(inotify)
+        Build-Depends:	hinotify
+        CPP-Options:	-DINOTIFY
