diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2014, Chris Chalmers
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Chris Chalmers nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+texrunner
+=========
+
+texrunner provides a (hopefully) convenient interface to Tex including
+functions for running Tex, parsing log files.
+
+The online module allows retrieving results of commands, such as
+dimensions of an `hbox`, using Tex's interactive features.
+
+![hbox dimensions recovered from texrunner](https://rawgit.com/cchalmers/texrunner/master/diagrams/hbox.svg)
+
+This package is a work in progress and likely contains lots of bugs.
+Eventually I hope it will be good enough for general use but for now
+it's only suitable for simple things.
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/src/System/Texrunner.hs b/src/System/Texrunner.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Texrunner.hs
@@ -0,0 +1,109 @@
+----------------------------------------------------------------------------
+-- |
+-- Module      :  System.Texrunner
+-- Copyright   :  (c) 2014 Christopher Chalmers
+-- License     :  BSD-style (see LICENSE)
+-- Maintainer  :  c.chalmers@me.com
+--
+-- Functions for running Tex.
+--
+-----------------------------------------------------------------------------
+
+module System.Texrunner
+  ( runTex
+  , runTex'
+  , prettyPrintLog
+  ) where
+
+import           Control.Applicative
+import qualified Data.ByteString.Char8      as C8 hiding (concatMap)
+import           Data.ByteString.Lazy.Char8 as LC8 hiding (concatMap)
+import           Data.Maybe
+
+import           System.Directory
+import           System.Environment
+import           System.Exit
+import           System.FilePath
+import           System.IO
+import           System.IO.Temp
+import           System.Process
+
+import           System.Texrunner.Parse
+
+-- | Same as 'runTex'' but runs Tex in a temporary system directory.
+runTex :: String     -- ^ Tex command
+       -> [String]   -- ^ Additional arguments
+       -> [FilePath] -- ^ Additional Tex input paths
+       -> ByteString -- ^ Source Tex file
+       -> IO (ExitCode, TexLog, Maybe ByteString)
+runTex command args extras source =
+  withSystemTempDirectory "texrunner." $ \path ->
+    runTex' path command args extras source
+
+-- | Run Tex program in the given directory. Additional Tex inputs are
+--   for filepaths to things like images that Tex can refer to.
+runTex' :: FilePath   -- ^ Directory to run Tex in
+        -> String     -- ^ Tex command
+        -> [String]   -- ^ Additional arguments
+        -> [FilePath] -- ^ Additional Tex inputs
+        -> ByteString -- ^ Source Tex file
+        -> IO (ExitCode, TexLog, Maybe ByteString)
+runTex' path command args extras source = do
+
+  LC8.writeFile (path </> "texrunner.tex") source
+
+  environment <- extraTexInputs (path:extras) <$> getEnvironment
+
+  let p = (proc command ("texrunner.tex" : args))
+            { cwd     = Just path
+            , std_in  = CreatePipe
+            , std_out = CreatePipe
+            , env     = Just environment
+            }
+
+  (Just inH, Just outH, _, pHandle) <- createProcess p
+
+  hClose inH
+  a <- C8.hGetContents outH -- backup log
+
+  hClose outH
+  exitC <- waitForProcess pHandle
+
+  pdfExists <- doesFileExist (path </> "texrunner.pdf")
+  pdfFile   <- if pdfExists
+                  then Just <$> LC8.readFile (path </> "texrunner.pdf")
+                  else return Nothing
+
+  logExists <- doesFileExist (path </> "texrunner.log")
+  logFile   <- if logExists
+                  then Just <$> C8.readFile (path </> "texrunner.log")
+                  else return Nothing
+
+  -- pdfFile <- optional $ LC8.readFile (path </> "texrunner.pdf")
+  -- logFile <- optional $ C8.readFile (path </> "texrunner.log")
+
+  return (exitC, parseLog $ fromMaybe a logFile, pdfFile)
+
+-- | Add a list of paths to the tex
+extraTexInputs :: [FilePath] -> [(String,String)] -> [(String,String)]
+extraTexInputs []      = id
+extraTexInputs inputss = alter f "TEXINPUTS"
+  where
+    f Nothing  = Just inputs
+    f (Just x) = Just (inputs ++ [searchPathSeparator] ++ x)
+    --
+    inputs = concatMap (++ [searchPathSeparator]) inputss
+    -- inputs = intercalate [searchPathSeparator] inputss
+
+-- Alter can be used to insert, delete or update an element. Similar to alter
+-- in Data.Map.
+alter :: Eq k => (Maybe a -> Maybe a) -> k -> [(k,a)] -> [(k,a)]
+alter f k = go
+  where
+    go []         = maybeToList ((,) k <$> f Nothing)
+    go ((k',x):xs)
+      | k' == k   = case f (Just x) of
+                      Just x' -> (k',x') : xs
+                      Nothing -> xs
+      | otherwise = (k',x) : go xs
+
diff --git a/src/System/Texrunner/Online.hs b/src/System/Texrunner/Online.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Texrunner/Online.hs
@@ -0,0 +1,207 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+
+----------------------------------------------------------------------------
+-- |
+-- Module      :  System.Texrunner.Online
+-- Copyright   :  (c) 2015 Christopher Chalmers
+-- License     :  BSD-style (see LICENSE)
+-- Maintainer  :  c.chalmers@me.com
+--
+-- Functions for running and parsing using Tex's online interface. This is
+-- mostly used for getting measurements like hbox dimensions and textwidth.
+--
+-- Tex's online interface is basically running the command line. You can
+-- see it by running @pdflatex@ without any arguments. The contents can
+-- be writen line by and tex can give feedback though stdout, which gets
+-- parsed in by this module. This is the only way I know to get info
+-- like hbox sizes. Please let me know if you know a better way.
+--
+-----------------------------------------------------------------------------
+
+module System.Texrunner.Online
+  ( OnlineTex
+  -- * Running Tex online
+  , runOnlineTex
+
+  , runOnlineTex'
+  -- * Interaction
+  , hbox
+  , hsize
+  , showthe
+  , onlineTexParser
+  , texPutStrLn
+
+  -- * Low level
+  -- | These functions allow give you direct access to the iostreams
+  --   with tex. The implementation is likely to change in the future
+  --   and using them directly is not recommended.
+  , TexStreams
+  , getInStream
+  , getOutStream
+  , clearUnblocking
+  ) where
+
+import           Control.Applicative
+import           Control.Monad.Reader
+import qualified Data.Attoparsec.ByteString   as A
+import           Data.ByteString.Char8        (ByteString)
+import qualified Data.ByteString.Char8        as C8
+import qualified Data.ByteString.Lazy.Char8   as LC8
+import           Data.List                    (find)
+import           Data.Maybe
+import           Data.Monoid
+import qualified Data.Traversable             as T
+
+import           System.Directory
+import           System.FilePath
+import           System.IO
+import           System.IO.Streams            as Streams
+import           System.IO.Streams.Attoparsec
+import           System.IO.Temp
+import           System.Process               as P (runInteractiveProcess)
+
+import           System.Texrunner.Parse
+
+-- | Type for dealing with Tex's pipping interface, the current streams
+--   are availble though the `MonadReader` instance.
+newtype OnlineTex a = OnlineTex {runOnlineTexT :: ReaderT TexStreams IO a}
+  deriving (Functor, Applicative, Monad, MonadIO, MonadReader TexStreams)
+
+-- Run a tex process, disguarding the resulting PDF.
+runOnlineTex :: String      -- ^ tex command
+             -> [String]    -- ^ tex command arguments
+             -> ByteString  -- ^ preamble
+             -> OnlineTex a -- ^ Online Tex to be Run
+             -> IO a
+runOnlineTex command args preamble process =
+  (\(a,_,_) -> a) <$> runOnlineTex' command args preamble process
+
+-- Run a tex process, keeping the resulting PDF. The OnlineTex must receive
+-- the terminating control sequence (\bye, \end{document}, \stoptext).
+runOnlineTex' :: String
+              -> [String]
+              -> ByteString
+              -> OnlineTex a
+              -> IO (a, TexLog, Maybe LC8.ByteString)
+runOnlineTex' command args preamble process =
+  withSystemTempDirectory "onlinetex." $ \path -> do
+    (outS, inS, h) <- mkTexHandles path Nothing command args preamble
+    a              <- flip runReaderT (outS, inS) . runOnlineTexT $ process
+
+    write Nothing outS
+    _ <- waitForProcess h
+
+    -- it's normally texput.pdf but some (Context) choose random names
+    pdfPath  <- find ((==".pdf") . takeExtension) <$> getDirectoryContents path
+    pdfFile  <- T.mapM (LC8.readFile . (path </>)) pdfPath
+
+    logPath  <- find ((==".log") . takeExtension) <$> getDirectoryContents path
+    logFile  <- T.mapM (C8.readFile . (path </>)) logPath
+
+    return (a, parseLog $ fromMaybe "" logFile, pdfFile)
+
+-- | Get the dimensions of a hbox.
+hbox :: Fractional n => ByteString -> OnlineTex (Box n)
+hbox str = do
+  clearUnblocking
+  texPutStrLn $ "\\setbox0=\\hbox{" <> str <> "}\n\\showbox0\n"
+  onlineTexParser parseBox
+
+-- | Parse result from @\showthe@.
+showthe :: Fractional n => ByteString -> OnlineTex n
+showthe str = do
+  clearUnblocking
+  texPutStrLn $ "\\showthe" <> str
+  onlineTexParser parseUnit
+
+-- | Dimensions from filling the current line.
+hsize :: Fractional n => OnlineTex n
+hsize = boxWidth <$> hbox "\\line{\\hfill}"
+
+-- | Run an Attoparsec parser on Tex's output.
+onlineTexParser :: A.Parser a -> OnlineTex a
+onlineTexParser p = getInStream >>= liftIO . parseFromStream p
+  -- TODO: have a timeout
+
+texPutStrLn :: ByteString -> OnlineTex ()
+texPutStrLn a = getOutStream >>= liftIO . write (Just $ C8.append a "\n")
+
+-- * Internal
+-- These functions should be used with caution.
+
+type TexStreams = (OutputStream ByteString, InputStream ByteString)
+
+-- | Get the output stream to read tex's output.
+getOutStream :: OnlineTex (OutputStream ByteString)
+getOutStream = reader fst
+
+-- | Get the input stream to give text to tex.
+getInStream :: OnlineTex (InputStream ByteString)
+getInStream = reader snd
+
+-- | Clear any output tex has already given.
+clearUnblocking :: OnlineTex ()
+clearUnblocking = getInStream >>= void . liftIO . Streams.read
+
+-- | Uses a surface to open an interface with Tex,
+mkTexHandles :: FilePath
+             -> Maybe [(String, String)]
+             -> String
+             -> [String]
+             -> ByteString
+             -> IO (OutputStream ByteString,
+                    InputStream ByteString,
+                    ProcessHandle)
+mkTexHandles dir env command args preamble = do
+
+  -- Tex doesn't send anything to stderr
+  (outStream, inStream, _, h) <- runInteractiveProcess'
+                                   command
+                                   args
+                                   (Just dir)
+                                   env
+
+  -- inStream <- debugStream inStream'
+
+  -- commands to get Tex to play nice
+  write (Just $ "\\tracingonline=1"  -- \showbox is echoed to stdout
+             <> "\\showboxdepth=1"   -- show boxes one deep
+             <> "\\showboxbreadth=1"
+             <> "\\scrollmode\n"     -- don't pause after showing something
+        ) outStream
+  write (Just preamble) outStream
+
+  return (outStream, inStream, h)
+
+-- Adapted from io-streams. Sets input handle to line buffering.
+runInteractiveProcess'
+    :: FilePath                 -- ^ Filename of the executable (see 'proc' for details)
+    -> [String]                 -- ^ Arguments to pass to the executable
+    -> Maybe FilePath           -- ^ Optional path to the working directory
+    -> Maybe [(String,String)]  -- ^ Optional environment (otherwise inherit)
+    -> IO (OutputStream ByteString,
+           InputStream ByteString,
+           InputStream ByteString,
+           ProcessHandle)
+runInteractiveProcess' cmd args wd env = do
+    (hin, hout, herr, ph) <- P.runInteractiveProcess cmd args wd env
+
+    -- it is possible to flush using write (Just "") but this seems nicer
+    -- is there a better way?
+    hSetBuffering hin LineBuffering
+
+    sIn  <- Streams.handleToOutputStream hin >>=
+            Streams.atEndOfOutput (hClose hin) >>=
+            Streams.lockingOutputStream
+    sOut <- Streams.handleToInputStream hout >>=
+            Streams.atEndOfInput (hClose hout) >>=
+            Streams.lockingInputStream
+    sErr <- Streams.handleToInputStream herr >>=
+            Streams.atEndOfInput (hClose herr) >>=
+            Streams.lockingInputStream
+
+    return (sIn, sOut, sErr, ph)
+
+-- debugStream :: InputStream ByteString -> IO (InputStream ByteString)
+-- debugStream = debugInput id "tex" Streams.stdout
diff --git a/src/System/Texrunner/Parse.hs b/src/System/Texrunner/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Texrunner/Parse.hs
@@ -0,0 +1,324 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+
+----------------------------------------------------------------------------
+-- |
+-- Module      :  System.Texrunner.Parse
+-- Copyright   :  (c) 2015 Christopher Chalmers
+-- License     :  BSD-style (see LICENSE)
+-- Maintainer  :  c.chalmers@me.com
+--
+-- Functions for parsing Tex output and logs. This log is parser is
+-- experimental and largely untested. Please make an issue for any logs
+-- that aren't parsed properly.
+--
+-----------------------------------------------------------------------------
+
+module System.Texrunner.Parse
+  ( -- * Box
+    Box (..)
+  , parseBox
+    -- * Errors
+  , TexLog (..)
+  , TexInfo (..)
+  , TexError (..)
+  , TexError' (..)
+  , someError
+  , badBox
+  , parseUnit
+  , parseLog
+  , prettyPrintLog
+  ) where
+
+import           Control.Applicative
+import           Data.Attoparsec.ByteString.Char8 as A
+import           Data.ByteString.Char8            (ByteString, cons, pack)
+import qualified Data.ByteString.Char8            as B
+import           Data.Maybe
+import           Data.Monoid
+
+------------------------------------------------------------------------
+-- Boxes
+------------------------------------------------------------------------
+
+-- | Data type for holding dimensions of a hbox. It is likely the
+--   internal representation will change to allow nested boxes in the
+--   future.
+data Box n = Box
+  { boxHeight :: n
+  , boxDepth  :: n
+  , boxWidth  :: n
+  } deriving Show
+
+int :: Parser Int
+int = decimal
+
+parseBox :: Fractional n => Parser (Box n)
+parseBox = do
+  A.skipWhile (/='\\') <* char '\\'
+  parseSingle <|> parseBox
+  where
+    parseSingle = do
+      _ <- "box" *> int <* "=\n\\hbox("
+      h <- rational <* char '+'
+      d <- rational <* ")x"
+      w <- rational
+      --
+      return $ Box (pt2bp h) (pt2bp d) (pt2bp w)
+
+parseUnit :: Fractional n => Parser n
+parseUnit = do
+  A.skipWhile (/='>') <* char '>'
+  skipSpace
+  fmap pt2bp rational <|> parseUnit
+
+pt2bp :: Fractional n => n -> n
+pt2bp = (/1.00374)
+
+------------------------------------------------------------------------
+-- Logs
+------------------------------------------------------------------------
+
+-- Everything's done using ByteString because io-streams' attoparsec module
+-- only has a ByteString function. It's very likely this will all change to
+-- Text in the future.
+
+data TexLog = TexLog
+  { texInfo   :: TexInfo
+  , numPages  :: Maybe Int
+  , texErrors :: [TexError]
+  -- , rawLog    :: ByteString
+  } deriving Show
+
+data TexInfo = TexInfo
+  { texCommand      :: Maybe ByteString
+  , texVersion      :: Maybe ByteString
+  , texDistribution :: Maybe ByteString
+  -- , texDate    :: Maybe Date
+  }
+  deriving Show
+
+-- Make shift way to parse a log by combining it in this way.
+instance Monoid TexLog where
+  mempty = TexLog (TexInfo Nothing Nothing Nothing) Nothing []
+  TexLog prog pages1 errors1 `mappend` TexLog _ pages2 errors2 =
+    case (pages1,pages2) of
+      (Just a,_) -> TexLog prog (Just a) (errors1 ++ errors2)
+      (_,b)      -> TexLog prog b (errors1 ++ errors2)
+
+infoParser :: Parser TexInfo
+infoParser
+  = TexInfo
+  <$> optional ("This is"   *> takeTill (== ',') <* anyChar)
+  <*> optional (" Version " *> takeTill (== ' ') <* anyChar)
+  <*> optional (char '('    *> takeTill (== ')') <* anyChar)
+  -- <*> Nothing
+
+logFile :: Parser TexLog
+logFile = mconcat <$> many logLine
+  where
+    logLine = do
+      info   <- infoParser
+      pages  <- optional nPages
+      errors <- maybeToList <$> optional someError
+      _      <- restOfLine
+      return $ TexLog info pages errors
+
+-- thisIs :: Parser TexVersion
+
+parseLog :: ByteString -> TexLog
+parseLog = (\(Right a) -> a) . parseOnly logFile
+-- the parse should never fail (I think)
+
+prettyPrintLog :: TexLog -> ByteString
+prettyPrintLog (TexLog {..}) =
+  fromMaybe "unknown program" (texCommand texInfo)
+  <> maybe "" (" version " <>) (texVersion texInfo)
+  <> maybe "" (" " <>) (texDistribution texInfo)
+  <> "\n"
+  <> maybe "" ((<> "pages\n") . pack . show) numPages
+  <> B.unlines (map (pack . show) texErrors)
+
+------------------------------------------------------------------------
+-- Errors
+------------------------------------------------------------------------
+
+-- | An error from tex with possible line number.
+data TexError = TexError
+  { errorLine :: Maybe Int
+  , error'    :: TexError'
+  }
+  deriving Show
+
+instance Eq TexError where
+  TexError _ a == TexError _ b = a == b
+
+-- | A subset of possible error Tex can throw.
+data TexError'
+  = UndefinedControlSequence ByteString
+  | MissingNumber
+  | Missing Char
+  | IllegalUnit -- (Maybe Char) (Maybe Char)
+  | PackageError String String
+  | LatexError ByteString
+  | BadBox ByteString
+  | EmergencyStop
+  | ParagraphEnded
+  | TooMany ByteString
+  | DimensionTooLarge
+  | TooManyErrors
+  | NumberTooBig
+  | ExtraBrace
+  | FatalError ByteString
+  | UnknownError ByteString
+  deriving (Show, Read, Eq)
+
+-- Parse any line begining with "! ". Any unknown errors are returned as 'UnknownError'.
+someError :: Parser TexError
+someError =  mark *> errors
+  where
+    -- in context exclamation mark isn't always at the begining
+    mark = "! " <|> (notChar '\n' *> mark)
+    errors =  undefinedControlSequence
+          <|> illegalUnit
+          <|> missingNumber
+          <|> missing
+          <|> latexError
+          <|> emergencyStop
+          <|> extraBrace
+          <|> paragraphEnded
+          <|> numberTooBig
+          <|> tooMany
+          <|> dimentionTooLarge
+          <|> tooManyErrors
+          <|> fatalError
+          <|> TexError Nothing <$> UnknownError <$> restOfLine
+
+noteStar :: Parser ()
+noteStar = skipSpace *> "<*>" *> skipSpace
+
+toBeReadAgain :: Parser Char
+toBeReadAgain = do
+  skipSpace
+  _ <- "<to be read again>"
+  skipSpace
+  anyChar
+
+-- insertedText :: Parser ByteString
+-- insertedText = do
+--   skipSpace
+--   _ <- "<inserted text>"
+--   skipSpace
+--   restOfLine
+
+------------------------------------------------------------------------
+-- Error parsers
+------------------------------------------------------------------------
+
+undefinedControlSequence :: Parser TexError
+undefinedControlSequence = do
+  _ <- "Undefined control sequence"
+
+  _ <- optional $ do -- for context log
+    skipSpace
+    _ <- "system"
+    let skipLines = line <|> restOfLine *> skipLines
+    skipLines
+
+  _ <- optional noteStar
+  skipSpace
+  l <- optional line
+  skipSpace
+  cs <- finalControlSequence
+  return $ TexError l (UndefinedControlSequence cs)
+
+finalControlSequence :: Parser ByteString
+finalControlSequence = last <$> many1 controlSequence
+  where
+    controlSequence = cons '\\' <$>
+      (char '\\' *> takeTill (\x -> isSpace x || x=='\\'))
+
+illegalUnit :: Parser TexError
+illegalUnit = do
+  _ <- "Illegal unit of measure (pt inserted)"
+  _ <- optional toBeReadAgain
+  _ <- optional toBeReadAgain
+
+  return $ TexError Nothing IllegalUnit
+
+missingNumber :: Parser TexError
+missingNumber = do
+  _ <- "Missing number, treated as zero"
+  _ <- optional toBeReadAgain
+  _ <- optional noteStar
+  return $ TexError Nothing MissingNumber
+
+badBox :: Parser TexError
+badBox = do
+  s <- choice ["Underfull", "Overfull", "Tight", "Loose"]
+  _ <- " \\hbox " *> char '(' *> takeTill (==')') <* char ')'
+  l <- optional line
+  return $ TexError l (BadBox s)
+
+missing :: Parser TexError
+missing = do
+  c <- "Missing " *> anyChar <* " inserted"
+  l <- optional line
+  return $ TexError l (Missing c)
+
+line :: Parser Int
+line =  " detected at line " *> decimal
+    <|> "l."                 *> decimal
+
+emergencyStop :: Parser TexError
+emergencyStop = "Emergency stop"
+             *> return (TexError Nothing EmergencyStop)
+
+fatalError :: Parser TexError
+fatalError = TexError Nothing <$> FatalError <$> (" ==> Fatal error occurred, " *> restOfLine)
+
+-- line 8058 tex.web
+extraBrace :: Parser TexError
+extraBrace = "Argument of" *> return (TexError Nothing ExtraBrace)
+
+tooMany :: Parser TexError
+tooMany = TexError Nothing <$> TooMany <$> ("Too Many " *> takeTill (=='\''))
+
+tooManyErrors :: Parser TexError
+tooManyErrors = "That makes 100 errors; please try again"
+             *> return (TexError Nothing TooManyErrors)
+
+dimentionTooLarge :: Parser TexError
+dimentionTooLarge = "Dimension too large"
+                 *> return (TexError Nothing DimensionTooLarge)
+
+-- line 8075 tex.web
+paragraphEnded :: Parser TexError
+paragraphEnded = do
+  _ <- "Paragraph ended before "
+  _ <- takeTill isSpace
+  _ <- toBeReadAgain
+  l <- optional line
+  return $ TexError l ParagraphEnded
+
+numberTooBig :: Parser TexError
+numberTooBig = "Number too big"
+            *> return (TexError Nothing NumberTooBig)
+
+-- Latex errors
+
+latexError :: Parser TexError
+latexError = TexError Nothing <$> LatexError <$> ("Latex Error: " *> restOfLine)
+
+-- Pages
+
+nPages :: Parser Int
+nPages = "Output written on "
+      *> skipWhile (/= '(') *> char '('
+      *> decimal
+
+-- Utilities
+
+restOfLine :: Parser ByteString
+restOfLine = takeTill (=='\n') <* char '\n'
+
diff --git a/tests/Tests.hs b/tests/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Tests.hs
@@ -0,0 +1,9 @@
+module Main (main) where
+
+import Test.Framework (defaultMain)
+import qualified Tex.PDF
+import qualified Tex.LogParse
+
+main :: IO ()
+main = defaultMain (Tex.PDF.tests ++ Tex.LogParse.tests)
+
diff --git a/tests/Tex/LogParse.hs b/tests/Tex/LogParse.hs
new file mode 100644
--- /dev/null
+++ b/tests/Tex/LogParse.hs
@@ -0,0 +1,160 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+module Tex.LogParse where
+
+import Data.ByteString.Lazy.Char8 as B (unlines, ByteString, writeFile)
+import Data.Monoid
+
+import Test.HUnit
+import Test.Framework.Providers.HUnit
+import Test.Framework as F
+
+import System.Texrunner
+import System.Texrunner.Parse
+import Control.Lens
+import Data.Foldable
+
+tests = texTests ++ latexTests ++ contextTests
+
+texTests = [checkErrors "tex error parse" tex]
+latexTests = [checkErrors "latex error parse" latex]
+contextTests = [checkErrors "context error parse" context]
+
+withHead :: Monad m => [a] -> (a -> m ()) -> m ()
+withHead (a:_) f = f a
+withHead _     _ = return ()
+
+tex e code = testCase ("tex" ++ show e) $ do
+  (exitCode, texLog, mPDF) <- runTex "pdftex" [] [] code
+  take 1 (map error' (texErrors texLog)) @?= [e]
+
+latexHeader, latexBye :: ByteString
+latexHeader = B.unlines
+  [ "\\documentclass{article}"
+  , "\\begin{document}"
+  ]
+latexBye = "\\end{document}"
+
+latex e code = testCase ("latex" ++ show e) $ do
+  (exitCode, texLog, mPDF) <- runTex "pdflatex" [] [] (latexHeader <> code)
+  head (map error' $ texErrors texLog) @?= e
+
+contextHeader, contextBye :: ByteString
+contextHeader = "\\starttext"
+contextBye = "\\stoptext"
+
+context e code = testCase ("context" ++ show e) $ do
+  (exitCode, texLog, mPDF) <- runTex "context" [] [] (contextHeader <> code)
+  take 1 (map error' (texErrors texLog)) @?= [e]
+  -- head (map error' $ texErrors texLog) @?= e
+  -- assertBool ("context" ++ show e) $ texLog `containsError` e
+
+-- Generating tex sample tex files -------------------------------------
+
+-- plain tex
+
+genTexFiles :: IO ()
+genTexFiles = for_ labeledErrors mkFile
+  where
+    mkFile (nm, (_err, xs)) = ifor_ xs $ \i x -> do
+      let doc = latexHeader <> x <> latexBye
+          name | length xs == 1 = nm
+               | otherwise      = nm <> "-" <> show (i+1)
+      B.writeFile ("tests/samples/tex/" <> name <> ".tex") doc
+
+-- latex
+
+-- pdflatex -draftmode --interaction=nonstopmode $i
+
+genLatexFiles :: IO ()
+genLatexFiles = for_ labeledErrors mkFile
+  where
+    mkFile (nm, (_err, xs)) = ifor_ xs $ \i x -> do
+      let doc = latexHeader <> x <> latexBye
+          name | length xs == 1 = nm
+               | otherwise      = nm <> "-" <> show (i+1)
+      B.writeFile ("tests/samples/latex/" <> name <> ".tex") doc
+
+-- context tex
+
+genContextFiles :: IO ()
+genContextFiles = for_ labeledErrors mkFile
+  where
+    mkFile (nm, (_err, xs)) = ifor_ xs $ \i x -> do
+      let doc = contextHeader <> x <> contextBye
+          name | length xs == 1 = nm
+               | otherwise      = nm <> "-" <> show (i+1)
+      B.writeFile ("tests/samples/context/" <> name <> ".tex") doc
+
+labeledErrors =
+  [ ("missing-dollar", missingDollar)
+  , ("dimention-too-large", dimensionTooLarge)
+  , ("illegal-unit", illegalUnit)
+  , ("missing-number", missingNumber)
+  , ("undefined-control-sequence", undefinedControlSequence)
+  ]
+
+-- Checking error parsing ----------------------------------------------
+
+containsError :: TexLog -> TexError -> Bool
+containsError log (TexError _ err) = err `elem` map error' (texErrors log)
+
+checkError :: (TexError' -> ByteString -> F.Test) -> (TexError', [ByteString]) -> F.Test
+checkError f (e, codes) = testGroup (show e) $ map (f e) codes
+
+checkErrors :: TestName -> (TexError' -> ByteString -> F.Test) ->  F.Test
+checkErrors name f = testGroup name $ map (checkError f) texErrs
+
+-- Sample errors -------------------------------------------------------
+
+texErrs =
+  [ missingDollar
+  , dimensionTooLarge
+  , illegalUnit
+  , missingNumber
+  , undefinedControlSequence
+  ]
+
+missingDollar = (,) (Missing '$')
+  [ "$x+1=2\n\n"
+  , "$$x+1=2\n\n"
+  ]
+
+dimensionTooLarge = (,) DimensionTooLarge
+  [ "\\hskip100000em"
+  ]
+
+illegalUnit = (,) IllegalUnit
+  [ "\\hskip1cn"
+  ]
+
+missingNumber = (,) MissingNumber
+  [ "\\hskip hi"
+  ]
+
+undefinedControlSequence = (,) (UndefinedControlSequence "\\hobx")
+  [ "\\hobx"
+  ]
+
+
+
+--
+-- missingDollarExample2= "x_1"
+--
+-- missingDollarExample3= "
+--
+-- numberTooBig = "10000000000"
+--
+-- overfull = "\\hbox to 1em{overfill box}"
+--
+-- underfill = "\\hbox to 20em{underfill box}"
+--
+-- illegalUnit = "\\hskip{1cn}"
+--
+-- undefinedControlSequence = "\\hobx"
+--
+-- missingNumber = "\\hskip"
+--
+--
+-- missingDollarTest = (texPutStrLn missingDollarExample, MissingDollar)
+--
diff --git a/tests/Tex/PDF.hs b/tests/Tex/PDF.hs
new file mode 100644
--- /dev/null
+++ b/tests/Tex/PDF.hs
@@ -0,0 +1,64 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+module Tex.PDF where
+
+import Data.ByteString.Lazy.Char8 as B
+import Data.Maybe
+import System.Exit
+
+import Test.HUnit
+import Test.Framework.Providers.HUnit
+
+import System.Texrunner
+import System.Texrunner.Online
+
+tests = [tex, latex, context, texOnline, latexOnline, contextOnline]
+texTests = [tex, texOnline]
+latexTests = [latex, latexOnline]
+contextTests = [context, contextOnline]
+
+texDocument :: ByteString
+texDocument = "hi\\bye"
+
+latexDocument :: ByteString
+latexDocument = B.unlines
+  [ "\\documentclass{article}"
+  , "\\begin{document}"
+  , "hi"
+  , "\\end{document}"
+  ]
+
+contextDocument :: ByteString
+contextDocument = B.unlines
+  [ "\\starttext"
+  , "hi"
+  , "\\stoptext"
+  ]
+
+tex     = testRunTeX "pdftex" [] texDocument
+latex   = testRunTeX "pdflatex" [] latexDocument
+context = testRunTeX "context" ["--once"] contextDocument
+
+testRunTeX command args document = testCase command $ do
+  (exitCode, _, mPDF) <- runTex command args [] document
+  exitCode @?= ExitSuccess
+  assertBool "pdf found" $ isJust mPDF
+
+
+-- online
+
+testOnlineTeX command args document = testCase (command ++ "Online") $ do
+  ((), _, mPDF) <- runOnlineTex' command args "" (texPutStrLn $ toStrict document)
+  assertBool "pdf found" $ isJust mPDF
+
+texOnline     = testOnlineTeX "pdftex" [] texDocument
+latexOnline   = testOnlineTeX "pdflatex" [] latexDocument
+contextOnline = testOnlineTeX "context" ["--pipe"] contextDocument
+
+
+
+
+-- tests to make:
+-- * texinputs for files in cwd
+-- * pdf made online
+
diff --git a/texrunner.cabal b/texrunner.cabal
new file mode 100644
--- /dev/null
+++ b/texrunner.cabal
@@ -0,0 +1,62 @@
+name:                texrunner
+version:             0.0.1.0
+synopsis:            Functions for running Tex from Haskell.
+description:
+  texrunner is an interface to tex that attempts to parse errors and
+  can parse tex in online mode to retrive hbox sizes.
+  .
+  This package should be considered very experimental. Eventually I hope
+  it will be good enough for general use but for now it's only suitable
+  for simple things.
+license:             BSD3
+license-file:        LICENSE
+bug-reports:         http://github.com/cchalmers/texrunner/issues
+author:              Christopher Chalmers
+maintainer:          c.chalmers@me.com
+copyright:           2015 Christopher Chalmers
+category:            System
+build-type:          Simple
+extra-source-files:  README.md
+cabal-version:       >=1.10
+source-repository head
+  type:     git
+  location: http://github.com/cchalmers/texrunner
+
+library
+  exposed-modules:
+    System.Texrunner
+    System.Texrunner.Online
+    System.Texrunner.Parse
+  build-depends:
+    base       >=4.6  && <4.9,
+    bytestring >=0.10 && <1.0,
+    filepath,
+    directory  >=1.2  && <2.0,
+    temporary  >=1.2  && <2.0,
+    process    >=1.2  && <2.0,
+    mtl        >=2.1  && <3.0,
+    attoparsec >=0.10 && <1.0,
+    io-streams >=1.1  && <2.0
+  hs-source-dirs: src
+  ghc-options:        -Wall
+  default-language:    Haskell2010
+
+test-suite tests
+  type:           exitcode-stdio-1.0
+  hs-source-dirs: tests
+  main-is:        Tests.hs
+  ghc-options:    -Wall
+  default-language: Haskell2010
+  other-modules:
+    Tex.PDF
+    Tex.LogParse
+
+  build-depends:
+    base,
+    lens,
+    test-framework,
+    test-framework-hunit,
+    HUnit,
+    bytestring,
+    texrunner
+
