diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2013, Bryan Richter
+
+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 Bryan Richter 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/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/System/Terminfo.hs b/System/Terminfo.hs
new file mode 100644
--- /dev/null
+++ b/System/Terminfo.hs
@@ -0,0 +1,162 @@
+-- |
+-- Module      :  System.Terminfo
+-- Copyright   :  (c) Bryan Richter (2013)
+-- License     :  BSD-style
+-- Maintainer  :  bryan.richter@gmail.com
+--
+-- This is a pure-Haskell (no FFI) module for accessing terminfo databases,
+-- which contain characteristics, or capabilities, for the various
+-- terminals such as screen, vt100, or xterm. Among other things, the
+-- capabilities include the idiosyncratic character sequences needed to
+-- send commands to the terminal. These commands include things like cursor
+-- movement.
+--
+-- For a deeper understanding of terminfo, consult the man pages for
+-- term(5) and terminfo(5).
+--
+-- There are three parts to this module: acquiring a terminfo database,
+-- querying the database, and defining the capabilities.
+--
+-- This module is dead simple, so a single example will hopefully suffice
+-- to demonstrate its usage.
+--
+-- @
+-- import System.Terminfo
+-- import System.Terminfo.Caps as C
+-- uglyExample :: IO (Maybe Int)
+-- uglyExample = do
+--     term \<- fromJust \<$> lookupEnv \"TERM\"
+--     db \<- 'acquireDatabase' term
+--     let maxColors (Right d) = 'queryNumTermCap' d C.'MaxColors'
+--     return $ maxColors db
+-- @
+--
+-- >>> uglyExample
+-- Just 256
+--
+
+module System.Terminfo (
+    -- * Acquiring a Database
+      acquireDatabase
+    -- * Querying Capabilities
+    -- $queryFuncs
+    , queryBoolTermCap
+    , queryNumTermCap
+    , queryStrTermCap
+    -- * The Capabilities
+    -- $capabilities
+
+    -- * The Database Type
+    , TIDatabase
+
+    ) where
+
+import Control.Applicative ((<$>), (<|>))
+import Control.Error
+import Control.Monad ((<=<), filterM)
+import qualified Data.ByteString as B
+import qualified Data.Map.Lazy as M
+import System.Directory
+import System.FilePath
+import System.IO
+
+import System.Terminfo.Types
+import System.Terminfo.DBParse
+import System.Terminfo.Internal (terminfoDBLocs)
+import System.Terminfo.Caps
+
+data DBType = BerkeleyDB | DirTreeDB
+    deriving(Show)
+
+-- Old MacDonald had a farm...
+type EIO = EitherT String IO
+
+acquireDatabase
+    :: String -- ^ System name
+    -> IO (Either String TIDatabase)
+       -- ^ A database object for the terminal, if it exists.
+acquireDatabase = runEitherT . (parseDBFile <=< findDBFile)
+
+findDBFile :: String -> EIO (DBType, FilePath)
+findDBFile term = case term of
+    (c:_) -> dbFileM c term `orLeft` "No terminfo db found"
+    _     -> hoistEither $ Left "User specified null terminal name"
+  where
+    orLeft = flip noteT
+    dbFileM c t = dirTreeDB c t <|> berkeleyDB
+
+-- | Not implemented
+berkeleyDB :: MaybeT IO (DBType, FilePath)
+berkeleyDB = nothing
+
+dirTreeDB :: Char -> String -> MaybeT IO (DBType, FilePath)
+dirTreeDB c term = MaybeT $ do
+    path <- findFirst =<< map (</> [c] </> term) <$> terminfoDBLocs
+    return $ (,) DirTreeDB <$> path
+
+findFirst :: [FilePath] -> IO (Maybe FilePath)
+findFirst = fmap headMay . filterM doesFileExist
+
+parseDBFile :: (DBType, FilePath) -> EIO TIDatabase
+parseDBFile (db, f) = case db of
+    DirTreeDB -> extractDirTreeDB f
+    BerkeleyDB -> hoistEither
+        $ Left "BerkeleyDB support not yet implemented"
+
+-- | Extract a 'TIDatabase' from the specified file. IO exceptions are
+-- left to their own devices.
+extractDirTreeDB :: FilePath
+                 -> EIO TIDatabase
+extractDirTreeDB =
+    hoistEither . parseDB
+    <=< rightT . B.hGetContents
+    <=< rightT . flip openBinaryFile ReadMode
+  where
+    rightT = EitherT . fmap Right
+
+queryBoolTermCap :: TIDatabase
+                 -> BoolTermCap
+                 -> Bool
+queryBoolTermCap (TIDatabase (TCBMap vals) _ _) cap =
+    fromMaybe False $ M.lookup cap vals
+
+queryNumTermCap :: TIDatabase
+                -> NumTermCap
+                -> Maybe Int
+queryNumTermCap (TIDatabase _ (TCNMap vals) _) cap = M.lookup cap vals
+
+-- | As this is a dead simple module, no \'smart\' handling of the
+-- returned string is implemented. In particular, placeholders for
+-- buffer characters and command arguments are left as-is. This will be
+-- rectified eventually, probably in a separate module.
+queryStrTermCap :: TIDatabase
+                -> StrTermCap
+                -> Maybe String
+queryStrTermCap (TIDatabase _ _ (TCSMap vals)) cap = M.lookup cap vals
+
+--
+-- DOCUMENTATION
+--
+
+
+
+-- $queryFuncs
+--
+-- For each of these three actions, the first argument is the database to
+-- query, and the second argument is the capability to look up.
+--
+-- I'm not super proud of this interface, but it's the best I can manage at
+-- present without requiring lots of mostly-empty case expressions. Perhaps
+-- someone will suggest a more interesting solution.
+
+-- $capabilities
+--
+-- /see/ "System.Terminfo.Caps"
+--
+-- There are no less than 497 capabilities specified in term.h on my
+-- Intel-based Ubuntu 12.04 notebook (slightly fewer in the terminfo(5) man
+-- page). The naive way of making these available to the user is as data
+-- constructors, and that is what I have done here.
+--
+-- The number of constructors absolutely crushes the namespace. I have
+-- sequestered them into their own module to try to alleviate the pain.
diff --git a/System/Terminfo/DBParse.hs b/System/Terminfo/DBParse.hs
new file mode 100644
--- /dev/null
+++ b/System/Terminfo/DBParse.hs
@@ -0,0 +1,126 @@
+{-# LANGUAGE RecordWildCards #-}
+
+-- |
+-- Module      :  System.Terminfo.DBParse
+-- Copyright   :  (c) Bryan Richter (2013)
+-- License     :  BSD-style
+-- 
+-- Maintainer  :  bryan.richter@gmail.com
+--
+-- An internal module encapsulating methods for parsing a terminfo file as
+-- generated by tic(1). The primary reference is the term(5) manpage.
+--
+
+module System.Terminfo.DBParse
+    ( parseDB
+    ) where
+
+import Control.Applicative ((<$>), (<*>))
+import Control.Arrow ((***))
+import qualified Control.Arrow as Arr
+import Control.Monad ((<=<), when, void)
+import Data.Attoparsec as A
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as B
+import Data.Char (chr)
+import qualified Data.Map.Lazy as M
+import Data.Word (Word16)
+
+import System.Terminfo.Types
+
+-- | term(5) defines a short integer as two 8-bit bytes, so:
+type ShortInt = Word16
+
+-- | short ints are stored little-endian.
+shortInt :: Integral a => ShortInt -> Parser a
+shortInt i = word8 first >> word8 second >> return (fromIntegral i)
+  where
+    (second, first) = (fromIntegral *** fromIntegral) $ i `divMod` 256
+
+-- | short ints are stored little-endian.
+--
+-- (-1) is represented by the two bytes 0o377 0o377.
+--
+-- Return type is Int so I can include (-1) in the possible outputs. I
+-- wonder if I will regret this.
+anyShortInt :: Parser Int
+anyShortInt = do
+    first <- fromIntegral <$> anyWord8
+    second <- fromIntegral <$> anyWord8
+    return $ if first == 0o377 && second == 0o377
+       then (-1)
+       else 256*second + first
+
+parseDB :: ByteString -> Either String TIDatabase
+parseDB = parseOnly tiDatabase
+
+tiDatabase :: Parser TIDatabase
+tiDatabase = do
+    Header{..} <- header
+    -- Ignore names
+    _ <- A.take namesSize
+    bools <- boolCaps boolSize
+    -- Align on an even byte
+    when (odd boolSize) (void $ A.take 1)
+    nums <- numCaps numIntegers
+    strs <- stringCaps numOffsets stringSize
+    -- TODO: extended info
+    return $ TIDatabase bools nums strs
+
+boolCaps :: Int -- ^ Number of caps
+         -> Parser TCBMap
+boolCaps =
+    return . TCBMap . M.fromList . zip keys . map (== 1) . B.unpack
+        <=< A.take
+  where
+    {-keys :: [BoolTermCap]-}
+    keys = [minBound ..]
+
+-- Negative values indicate missing capability.
+numCaps :: Int -- ^ Number of caps
+        -> Parser TCNMap
+numCaps = return . TCNMap . M.fromList . filter notNeg . zip keys
+    <=< flip A.count anyShortInt
+  where
+    notNeg = ((/= -1) . snd)
+    {-keys :: [BoolTermCap]-}
+    keys = [minBound ..]
+
+stringCaps :: Int -- ^ Number of caps
+           -> Int -- ^ Size of table
+           -> Parser TCSMap
+stringCaps numOffsets stringSize = do
+    offs <- A.count numOffsets anyShortInt
+    stringTable <- A.take stringSize
+    return
+        $ TCSMap $ M.fromList $ map (parseValue stringTable)
+        $ filter notNeg $ zip keys offs
+  where
+    notNeg = ((/= -1) . snd)
+    keys = [minBound ..]
+    parseValue tbl = Arr.second $ parseString tbl
+    parseString table offset =
+        asString
+        $ B.takeWhile (/= 0)  -- null-terminated
+        $ B.drop offset table -- starts at offset
+    asString = map (chr . fromIntegral) . B.unpack
+
+-- | the magic number for term files
+magic :: Parser Int
+magic = shortInt 0o432 <?> "Not a terminfo file (bad magic)"
+
+data Header = Header
+     { namesSize :: Int
+     , boolSize :: Int
+     , numIntegers :: Int
+     , numOffsets :: Int
+     , stringSize :: Int
+     }
+     deriving (Show)
+
+header :: Parser Header
+header = magic >> Header <$> anyShortInt
+                         <*> anyShortInt
+                         <*> anyShortInt
+                         <*> anyShortInt
+                         <*> anyShortInt
diff --git a/System/Terminfo/Internal.hs b/System/Terminfo/Internal.hs
new file mode 100644
--- /dev/null
+++ b/System/Terminfo/Internal.hs
@@ -0,0 +1,66 @@
+module System.Terminfo.Internal
+    ( terminfoDBLocs
+    , locationsPure
+    ) where
+
+import Control.Applicative ((<$>), (<*>), pure)
+import Control.Error
+import System.Environment (lookupEnv)
+import System.FilePath
+
+terminfoDBLocs :: IO [FilePath]
+terminfoDBLocs = locationsPure
+    <$> lookupEnv "TERMINFO"
+    <*> (lookupEnv "HOME" <$$/> ".terminfo")
+    <*> lookupEnv "TERMINFO_DIRS"
+    <*> pure ["/lib/terminfo", "/usr/share/terminfo"]
+
+
+(<$/>) :: Functor f => f FilePath -> FilePath -> f FilePath
+fa <$/> b = (</> b) <$> fa
+infixr 4 <$/>
+
+(<$$/>) :: (Functor f, Functor g)
+        => f (g FilePath)
+        -> FilePath
+        -> f (g FilePath)
+ffa <$$/> b = fmap (<$/> b) ffa
+infixr 4 <$$/>
+
+-- | I hate this name. There is undoubtedly a better way of structuring all
+-- of this, starting way up at 'dirTreeDB'.
+locationsPure :: Maybe FilePath -- ^ Override directory
+              -> Maybe FilePath -- ^ $HOME
+              -> Maybe String   -- ^ $TERMINFO_DIRS
+              -> [FilePath]     -- ^ Defaults
+              -> [FilePath]
+locationsPure ovr usr termdirs defs = case ovr of
+    Just override -> [override]
+    Nothing       -> catMaybes [usr] ++ system
+
+  where
+    system = case termdirs of
+        Just list -> parseTDVar defs list
+        Nothing   -> defs
+
+parseTDVar :: [String] -> String -> [String]
+parseTDVar defs = replace "" defs . split ':'
+
+-- | Replace an element with multiple replacements
+replace :: Eq a => a -> [a] -> [a] -> [a]
+replace old news = foldr (\x acc -> if x == old
+                                       then news ++ acc
+                                       else x : acc)
+                         []
+
+-- | split, as seen in ByteString and Text, but for Strings.
+split :: Char -> String -> [String]
+split s = foldr go [[]]
+  where
+    go c acc = if c /= s
+                  then unshift c acc
+                  else "" : acc
+
+    -- | /perldoc -f unshift/
+    unshift c (xs:xss) = (c:xs) : xss
+    unshift c []       = [[c]]
diff --git a/System/Terminfo/Types.hs b/System/Terminfo/Types.hs
new file mode 100644
--- /dev/null
+++ b/System/Terminfo/Types.hs
@@ -0,0 +1,20 @@
+module System.Terminfo.Types
+    ( TIDatabase(..)
+    , TCBMap(..)
+    , TCNMap(..)
+    , TCSMap(..)
+    ) where
+
+import Data.Map.Lazy (Map)
+
+import System.Terminfo.Caps
+
+data TCBMap = TCBMap (Map BoolTermCap Bool)
+    deriving (Show)
+data TCNMap = TCNMap (Map NumTermCap Int)
+    deriving (Show)
+data TCSMap = TCSMap (Map StrTermCap String)
+    deriving (Show)
+
+data TIDatabase = TIDatabase TCBMap TCNMap TCSMap
+    deriving (Show)
diff --git a/terminfo-hs.cabal b/terminfo-hs.cabal
new file mode 100644
--- /dev/null
+++ b/terminfo-hs.cabal
@@ -0,0 +1,46 @@
+name:                terminfo-hs
+version:             0.1.0.0
+synopsis:            A pure-Haskell (no FFI) module for accessing terminfo databases
+description:         This module can acquire terminfo databases and query
+                     them for terminal capabilities. For details of
+                     terminfo, consult the man pages for term(5) and
+                     terminfo(5).
+
+                     This package is dead simple, and doesn't do anything
+                     fancy with the capabilities themselves. It merely
+                     provides a means for accessing them.
+
+license:             BSD3
+license-file:        LICENSE
+author:              Bryan Richter
+maintainer:          bryan.richter@gmail.com
+copyright:           Bryan Richter, 2013
+category:            System, Terminal
+build-type:          Simple
+cabal-version:       >=1.8
+
+source-repository head
+  type: git
+  location: https://github.com/chreekat/terminfo-hs
+
+source-repository this
+  type: git
+  tag: 0.1.0.0
+  location: https://github.com/chreekat/terminfo-hs/tree/0.1.0.0
+
+library
+  ghc-options:         -Wall
+  exposed-modules:     System.Terminfo
+  other-modules:       System.Terminfo.DBParse,
+                       System.Terminfo.Types
+                       System.Terminfo.Internal
+  build-depends:       base ==4.*, errors ==1.*,
+                       bytestring ==0.*, directory ==1.*,
+                       filepath ==1.*, attoparsec ==0.*,
+                       containers ==0.5.*
+
+test-suite System.Terminfo.Internal
+  type:                exitcode-stdio-1.0
+  main-is:             test.hs
+  build-depends:       base ==4.*, filepath ==1.*, directory ==1.*,
+                       errors ==1.*, QuickCheck
diff --git a/test.hs b/test.hs
new file mode 100644
--- /dev/null
+++ b/test.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+import Test.QuickCheck
+import Test.QuickCheck.All
+import System.Terminfo.Internal
+
+import Data.Maybe
+
+prop_useOverride ovr =
+    forAll notLikely $ \usr ->
+    forAll notLikely $ \tds ->
+    forAll oftenNull $ \defs ->
+    classify (isNothing usr && isNothing tds && null defs)
+        "Nothing else specified"$
+    classify (isJust usr && isJust tds && not (null defs))
+        "Everything else specified"$
+    locationsPure (Just ovr) usr tds defs == [ovr]
+
+notLikely = frequency [(2, return Nothing), (1, fmap Just arbitrary)]
+oftenNull = frequency [(2, return []), (1, arbitrary)]
+
+prop_includeHome usr =
+    forAll notLikely $ \tds ->
+    forAll oftenNull $ \defs ->
+    let
+        withUsr    = locationsPure Nothing (Just usr) tds defs
+        withoutUsr = locationsPure Nothing Nothing    tds defs
+    in
+    classify (isJust tds || not (null defs))
+        "Something besides HOME specified"$
+    withUsr == usr : withoutUsr
+
+prop_terminfoDirsOverridesDefaults tds =
+    forAll (oneof [return [], listOf1 arbitrary]) $ \defs ->
+    classify (not $ null defs) "Defaults specified"$
+    locationsPure Nothing Nothing (Just tds) defs
+        == parseTDVar defs tds
+
+prop_useDefaults defs =
+    locationsPure Nothing Nothing Nothing defs == defs
+
+main = $quickCheckAll
