diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,9 @@
+Copyright (c) 2009, Daniel Lyons
+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 Daniel Lyons nor the names of its 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 HOLDER 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.txt b/README.txt
new file mode 100644
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,88 @@
+tablify - a simple utility for formatting CSV files.
+by Daniel Lyons <fusion@storytotell.org>
+
+What it does:
+
+ Tablify is a simple program which does nothing but parse CSV files and render
+ them into various display-friendly formats. I spend a fair amount of my time
+ dealing with data migration and sometimes it's handy to look at the data
+ directly or publish it in such a fashion as other people may find it
+ convenient.
+
+ Tablify does not understand every possible CSV format; I took the CSV code
+ from the book Real World Haskell. I am not particularly interested in adding
+ support for other varieties of CSV because this parser seems to handle the
+ common cases and generally if you are getting something else, it's because 
+ you asked for something weird. Please let me know if this is not the case for 
+ you.
+
+ A major rationale for this application was that I noticed that there are some
+ nice characters in the Unicode standard for drawing boxes. I used to draw
+ boxes for text files like this:
+
+ +--------------+-----+
+ | Name         | Age |
+ +--------------+-----+
+ | Daniel Lyons | 28  |
+ | Reid Givens  | 29  |
+ +--------------+-----+
+
+ This works OK (it's what MySQL and PostgreSQL's command line clients do,
+ after all.) But how much cooler does this look:
+
+ ┌──────────────┬─────┐
+ │ name         │ age │
+ ├──────────────┼─────┤
+ │ Daniel Lyons │ 28  │
+ │ Reid Givens  │ 29  │
+ └──────────────┴─────┘
+
+ If you have a nice Unicode font like Menlo on your machine, that should look
+ freaking awesome. This is what you get using the -U flag. If you want the old
+ fugly ASCII table, use -A.
+
+ Anyway, it occurred to me while writing it that I could quite easily support
+ a few other formats, so I wrote an HTML output routine and a TBL output
+ routine, TBL being part of troff or groff depending on your system.
+
+ The HTML output routine escapes HTML entities (<, > and &). I expect if you
+ actually use TBL you will want to tinker with what it gives you. I haven't
+ embellished the system with any justification alternatives besides left
+ justification; this is really only an issue for the Unicode output option.
+
+ The program assumes that your input and your output are both UTF-8. There are
+ utilities you can use such as iconv to fix files coming in or going out if
+ that's not your situation. You may specify many files on the command line, or
+ - if you want it to read from standard input.
+
+ Enjoy!
+
+Dependencies:
+
+ • GHC (Glasgow Haskell Compiler) — http://www.haskell.org/ghc/
+ • utf8-string library — http://hackage.haskell.org/package/utf8-string
+
+These are generally included with GHC:
+
+ • parsec
+ • regex-compat
+ • haskell98
+
+Building:
+
+ Run these three commands ($ represents your shell's prompt):
+
+ $ runhaskell Setup.hs configure
+ $ runhaskell Setup.hs build
+ $ sudo runhaskell Setup.hs install
+
+ Alternatively, if you have the cabal utility installed, you can run:
+
+ $ cabal configure
+ $ cabal build
+ $ cabal install
+
+Contacting me:
+
+ If you run into any bugs or think of a feature you'd like me to add, please
+ email me at fusion@storytotell.org and tell me about it.
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/Tablify.cabal b/Tablify.cabal
new file mode 100644
--- /dev/null
+++ b/Tablify.cabal
@@ -0,0 +1,20 @@
+Cabal-Version:       >= 1.2
+Name:                Tablify
+Version:             0.8
+License:             BSD3
+License-File:        LICENSE.txt
+Author:              Daniel Lyons
+Homepage:            http://www.storytotell.org/code/tablify
+Category:            Text
+Copyright:           © 2009-2011 Daniel Lyons
+Synopsis:            Tool to render CSV into tables of various formats
+Build-Type:          Simple
+Maintainer:          Daniel Lyons <fusion@storytotell.org>
+Description:         Tool to render CSV into tables of various formats, including HTML, tbl, and character art (both ASCII and Unicode)
+Extra-Source-Files:  README.txt
+
+Executable tablify   
+  Main-Is:           Tablify.hs
+  Other-Modules:     ASCII, CSV, Converter, FixedWidth, HTML, TBL, Tablify, Unicode, Utilities
+  Build-Depends:     base < 5, utf8-string, parsec, regex-compat, haskell98, xhtml, safer-file-handles
+  HS-Source-Dirs:    src
diff --git a/src/ASCII.hs b/src/ASCII.hs
new file mode 100644
--- /dev/null
+++ b/src/ASCII.hs
@@ -0,0 +1,23 @@
+module ASCII (converter) where
+import Prelude hiding (length)
+
+import Utilities
+import Converter
+import FixedWidth
+
+-- our basic agenda here is to convert the input into an array then we're
+-- going to process the array to discover the maximum widths for each column.
+-- once we have these maximums, we'll begin constructing the output by
+-- prefixing and suffixing padded strings in the tabular format.
+-- we take the first row to be the header. empty cells are permitted.
+
+asciify :: Table -> String
+asciify = fixedWidthTable ("+","+","+") ("+","+","+") ("+","+","+") '-' '|'
+
+converter :: Converter
+converter = Converter
+    { cName        = "ASCII"
+    , cConvert     = asciify
+    , cShortOpt    = "A"
+    , cLongOpt     = "ascii"
+    }
diff --git a/src/CSV.hs b/src/CSV.hs
new file mode 100644
--- /dev/null
+++ b/src/CSV.hs
@@ -0,0 +1,38 @@
+{-# LANGUAGE RankNTypes #-}
+
+module CSV where
+import Text.ParserCombinators.Parsec
+
+-- these CSV file parsing routines courtesy of Real World Haskell:
+-- http://book.realworldhaskell.org/read/using-parsec.html
+
+csvFile :: forall st. GenParser Char st [[String]]
+csvFile = endBy line eol
+
+line :: forall st. GenParser Char st [String]
+line = sepBy cell (char ',')
+
+cell :: forall st. GenParser Char st String
+cell = quotedCell <|> many (noneOf ",\n\r")
+
+quotedCell :: forall st. GenParser Char st String
+quotedCell = 
+    do _ <- char '"'
+       content <- many quotedChar
+       _ <- char '"' <?> "quote at end of cell"
+       return content
+
+quotedChar :: forall st. GenParser Char st Char
+quotedChar =
+        noneOf "\""
+    <|> try (string "\"\"" >> return '"')
+
+eol :: forall st. GenParser Char st String
+eol =   try (string "\n\r")
+    <|> try (string "\r\n")
+    <|> string "\n"
+    <|> string "\r"
+    <?> "end of line"
+
+parseCSV :: String -> Either ParseError [[String]]
+parseCSV = parse csvFile "(unknown)"
diff --git a/src/Converter.hs b/src/Converter.hs
new file mode 100644
--- /dev/null
+++ b/src/Converter.hs
@@ -0,0 +1,10 @@
+module Converter where
+
+import Utilities
+
+data Converter = Converter
+    { cName         :: String
+    , cConvert      :: Table -> String
+    , cShortOpt     :: String
+    , cLongOpt      :: String
+    }
diff --git a/src/FixedWidth.hs b/src/FixedWidth.hs
new file mode 100644
--- /dev/null
+++ b/src/FixedWidth.hs
@@ -0,0 +1,47 @@
+module FixedWidth
+    (Joints, fixedWidthTable) where
+
+import Prelude hiding (length, replicate)
+import Data.List hiding (length, replicate)
+
+import Utilities
+
+pad :: Integer -> String -> String
+pad n str = str ++ replicate (n - length str) ' '
+
+-- given a table, return a list of the maximum widths of each column
+columnWidths :: Table -> [Integer]
+columnWidths table = map (maximum . map length) $ transpose table
+
+boxRow :: [Integer] -> Char -> String -> String -> String -> String
+boxRow widths sp left mid right = 
+       left 
+    ++ intercalate mid (map (\x -> replicate (x+2) sp) widths)
+    ++ right
+
+type Joints = (String,String,String)
+
+surround :: Char -> String -> String
+surround c s = [c] ++ s ++ [c]
+
+-- We used to call this ASCII art, but these days it's Unicode.
+fixedWidthTable :: Joints -> Joints -> Joints -> Char -> Char -> Table -> String
+fixedWidthTable (tl,tm,tr) (ml, mm, mr) (bl, bm, br) h v table = 
+        intercalate "\n" [btop, bhead, bmid, body, bbot]
+    where
+        -- I'm pattern matching the (0,0) as a way of 
+        -- checking that the table is valid
+        widths = columnWidths table
+                
+        btop = boxRow widths h tl tm tr
+        bmid = boxRow widths h ml mm mr
+        bbot = boxRow widths h bl bm br
+        
+        bhead, body :: String
+        bhead = formatRow (head table)
+        body  = intercalate "\n" $ map formatRow (tail table)
+
+        formatRow row = surround v $ intercalate [v] $ 
+                        zipWith formatCell row widths
+        formatCell :: String -> Integer -> String
+        formatCell val width = surround ' ' $ pad width val
diff --git a/src/HTML.hs b/src/HTML.hs
new file mode 100644
--- /dev/null
+++ b/src/HTML.hs
@@ -0,0 +1,17 @@
+module HTML (converter) where
+
+import Text.XHtml.Strict hiding (header, body)
+import Utilities
+import Converter
+
+htmlify :: Table -> String
+htmlify tbl = prettyHtml $ table << concatHtml [thead << header, tbody << body]
+  where
+    header = tr << rowToTr th (head tbl)
+    row r  = tr << rowToTr td r
+    body   = map row $ tail tbl
+
+    rowToTr cellType = map (cellType <<)
+
+converter :: Converter
+converter = Converter "HTML" htmlify "H" "html"
diff --git a/src/TBL.hs b/src/TBL.hs
new file mode 100644
--- /dev/null
+++ b/src/TBL.hs
@@ -0,0 +1,19 @@
+module TBL (converter) where
+
+import Prelude hiding (length, replicate)
+import Utilities
+import Converter
+
+-- TBL... yeah...
+tblify :: Table -> String
+tblify table = intercalate "\n" [tblstart, header, body, tblend]
+    where
+        columns = length $ head table
+        tblstart = ".TS"
+        tblend = ".TE"
+        header = heading 'c' ++ "\n" ++ heading 'l' ++ "."
+        heading c = intersperse ' ' $ replicate (columns+1) c
+        body = intercalate "\n" $ map (intercalate "\t") table
+
+converter :: Converter
+converter = Converter "TBL" tblify "T" "tbl"
diff --git a/src/Tablify.hs b/src/Tablify.hs
new file mode 100644
--- /dev/null
+++ b/src/Tablify.hs
@@ -0,0 +1,109 @@
+-- Generates tables from CSV files.
+--
+-- I wrote it specifically to make pretty Unicode tables like this:
+-- 
+-- ┌─────┬─────┬─────┐
+-- │ foo │ bar │ baz │
+-- ├─────┼─────┼─────┤
+-- │ boo │ baa │ bee │
+-- │ haa │ hee │ hoo │
+-- └─────┴─────┴─────┘
+--
+-- It always treats the first record as the column headers.
+
+module Main where
+
+-- this declaration lets me forget all about qualifying the UTF8 version of IO
+--import Prelude hiding (putStr, putStrLn, getContents) 
+
+import System
+import System.IO
+--import System.IO.SaferFileHandles
+--import System.IO.UTF8
+import System.Console.GetOpt
+
+import Converter
+import Utilities
+import Unicode
+import HTML
+import TBL
+import ASCII
+import CSV
+import LaTeX
+
+version :: Double
+version = 0.8
+
+data Options = Options 
+    { optHelp        :: Bool
+    , optVersion     :: Bool
+    , optConverter   :: Converter
+    }
+
+defaultOptions :: Options
+defaultOptions = Options
+    { optHelp      = False
+    , optVersion   = False
+    , optConverter = HTML.converter
+    }
+
+converters :: [Converter]
+converters = 
+    [ ASCII.converter
+    , HTML.converter
+    , TBL.converter
+    , Unicode.converter
+    , LaTeX.converter]
+
+usage :: String
+usage = "Usage: tablify [OPTION...] file"
+
+options :: [OptDescr (Options -> Options)]
+options = 
+    [ Option "v"  ["version"] 
+        (NoArg (\o -> o { optVersion = True }))
+        "show version"
+    , Option "h"  ["help"] 
+        (NoArg (\o -> o { optHelp = True }))
+        "show this help" ] ++ moreOptions
+    where
+        moreOptions = map converterToOption converters
+        converterToOption c@(Converter name _ short long) = 
+            Option short [long] 
+                (NoArg (\o -> o {optConverter = c}))
+                ("output " ++ name ++ " table")
+
+getOptions :: [String] -> IO (Options, [String])
+getOptions argv =
+    case getOpt Permute options argv of
+        (o, n, []) -> return (foldl (flip id) defaultOptions o, n)
+        (_, _, errors) -> ioError (userError 
+                            (concat errors ++ usageInfo usage options))
+
+processOpts :: Table -> Options -> String
+processOpts table (Options _ _ (Converter { cConvert = f })) = f table
+        
+parseData :: String -> IO Table
+parseData dat = case parseCSV dat of 
+    Left _ -> ioError $ userError "unable to parse file"
+    Right result -> return result
+
+processFile :: Options -> String -> IO ()
+processFile opts file = do
+    fileData <- if file == "-" then getContents    else readFile file
+    table <- parseData fileData
+    putStrLn $ processOpts table opts
+
+showInfo :: Options -> IO ()
+showInfo Options { optVersion = True } = putStrLn $ "tablify version " ++ show version
+showInfo Options { optHelp = True }    = putStr $ usageInfo usage options
+showInfo _                             = fail "unable to show info for weird options"
+
+main :: IO ()
+main = do
+    args <- getArgs
+    (opts, arguments) <- getOptions args
+    hSetEncoding stdout utf8
+    if optHelp opts || optVersion opts 
+        then showInfo opts
+        else mapM_ (processFile opts) arguments
diff --git a/src/Unicode.hs b/src/Unicode.hs
new file mode 100644
--- /dev/null
+++ b/src/Unicode.hs
@@ -0,0 +1,17 @@
+module Unicode (converter) where
+
+import Utilities
+import Converter
+import FixedWidth
+
+-- our basic agenda here is to convert the input into an array then we're
+-- going to process the array to discover the maximum widths for each column.
+-- once we have these maximums, we'll begin constructing the output by
+-- prefixing and suffixing padded strings in the tabular format.
+-- we take the first row to be the header. empty cells are permitted.
+
+unicate :: Table -> String
+unicate = fixedWidthTable ("┌","┬","┐") ("├","┼","┤") ("└","┴","┘") '─' '│'
+
+converter :: Converter
+converter = Converter "Unicode" unicate "U" "unicode"
diff --git a/src/Utilities.hs b/src/Utilities.hs
new file mode 100644
--- /dev/null
+++ b/src/Utilities.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE RankNTypes #-}
+
+module Utilities
+     (Table,
+     module Data.List,
+     repeats,
+     length,
+     replicate
+    ) where
+
+import Data.List hiding (length, replicate)
+import Prelude hiding (length, replicate)
+
+type Table = [[String]]
+
+-- let's simplify the reading of the code
+length :: forall b. [b] -> Integer
+length = genericLength
+
+replicate :: forall a. Integer -> a -> [a]
+replicate = genericReplicate
+
+repeats :: [a] -> Integer -> [a]
+repeats s c = concat $ genericTake c $ repeat s
