diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2011-2012 Omari Norman.
+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 Omari Norman 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/Penny.hs b/Penny.hs
new file mode 100644
--- /dev/null
+++ b/Penny.hs
@@ -0,0 +1,88 @@
+{- | Penny - double-entry accounting system
+
+Penny is organized into a tree of modules, each with a name. Check out
+the links for details on each component of Penny.
+
+"Penny.Cabin" - Penny reports. Depends on Lincoln and Liberty.
+
+"Penny.Copper" - the Penny parser. Depends on Lincoln.
+
+"Penny.Liberty" - Penny command line parser helpers. Depends on
+Lincoln and Copper.
+
+"Penny.Lincoln" - the Penny core. Depends on no other Penny components.
+
+"Penny.Shield" the Penny runtime environment
+
+"Penny.Zinc" - the Penny command-line interface. Depends on Cabin,
+Copper, Liberty, and Lincoln.
+
+The dependencies are represented as a dot file in doc/dependencies.dot
+in the Penny git repository.
+
+This module exports a few functions that are useful for building a
+simple command line program with default options. To make anything
+more complicated you may need to import modules from deeper down
+within the hierarchy, but for a program based on the defaults only you
+should be able to import this module only (and nothing else).
+
+-}
+module Penny (
+  -- * Default time zone
+  Copper.DefaultTimeZone (DefaultTimeZone)
+  , Copper.utcDefault
+  , minsToDefaultTimeZone
+    
+    -- * Radix and grouping characters
+  , Copper.RadGroup
+  , Copper.periodComma
+  , Copper.periodSpace
+  , Copper.commaPeriod
+  , Copper.commaSpace
+    
+    -- * Reports
+  , Cabin.allReportsWithDefaults
+    
+    -- * Parser defaults
+  , Z.T(..)
+  , Z.defaultFromRuntime
+    
+    -- * Main function
+  , defaultPenny
+  , customPenny
+    
+    -- * Other useful stuff - for custom reports
+  ) where
+
+import qualified Penny.Cabin as Cabin
+import qualified Penny.Copper as Copper
+import qualified Penny.Lincoln as L
+import qualified Penny.Shield as S
+import qualified Penny.Zinc as Z
+
+-- | Use to make a DefaultTimeZone based on the number of minutes the
+-- time zone is offset from UTC. Make sure the argument you supply is
+-- between (-840) and 840; otherwise your program will crash at
+-- runtime.
+minsToDefaultTimeZone :: Int -> Copper.DefaultTimeZone
+minsToDefaultTimeZone i = case L.minsToOffset i of
+  Nothing -> error $ "penny: error: minutes out of range of "
+             ++ "allowed values for time zone"
+  Just tzo -> Copper.DefaultTimeZone tzo
+
+defaultPenny :: Copper.DefaultTimeZone -> Copper.RadGroup -> IO ()
+defaultPenny dtz rg = do
+  rt <- S.runtime
+  let df = Z.defaultFromRuntime dtz rg
+      rs = Cabin.allReportsWithDefaults dtz rg
+  Z.runPenny rt dtz rg df rs
+
+customPenny ::
+  Copper.DefaultTimeZone
+  -> Copper.RadGroup
+  -> (S.Runtime -> Z.T)
+  -> [Cabin.Report]
+  -> IO ()
+customPenny dtz rg gd rs = do
+  rt <- S.runtime
+  Z.runPenny rt dtz rg gd rs
diff --git a/Penny/Cabin.hs b/Penny/Cabin.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin.hs
@@ -0,0 +1,15 @@
+-- | Cabin - Penny reports
+module Penny.Cabin (allReportsWithDefaults, I.Report(..)) where
+
+import qualified Penny.Cabin.Balance as B
+import qualified Penny.Cabin.Posts as P
+import qualified Penny.Copper as C
+import qualified Penny.Cabin.Interface as I
+
+allReportsWithDefaults ::
+  C.DefaultTimeZone
+  -> C.RadGroup
+  -> [I.Report]
+allReportsWithDefaults dtz rg =
+  [B.defaultBalanceReport, P.defaultPostsReport dtz rg]
+  
diff --git a/Penny/Cabin/Allocate.hs b/Penny/Cabin/Allocate.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Allocate.hs
@@ -0,0 +1,72 @@
+module Penny.Cabin.Allocate (
+  Allocation,
+  allocation,
+  unAllocation,
+  allocate) where
+
+import qualified Control.Monad.Trans.State as St
+import qualified Data.Foldable as F
+import qualified Data.Traversable as T
+import qualified Data.Map as M
+
+newtype Allocation = Allocation { unAllocation :: Int }
+                     deriving (Show, Eq, Ord)
+
+allocation :: Int -> Allocation
+allocation i =
+  if i < 1
+  then error "Allocations must be at least 1"
+  else Allocation i
+
+-- | Divide up a whole number proportionally into several parts. The
+-- sum of the parts is guaranteed to add up to original number.
+allocate ::
+  Ord k
+  => M.Map k Allocation
+  -- ^ Maps arbitrary unique values to the allocations, so that you
+  -- can look up the corresponding allocations.
+
+  -> Int
+  -- ^ Allocated values will add up to this number.
+  
+  -> M.Map k Int
+  -- ^ The given number, allocated into proportional parts.
+
+allocate m t = let
+  tot = F.sum . fmap (toDouble . unAllocation) $ m
+  ratios = fmap ((/tot) . toDouble . unAllocation) m
+  rounded = fmap (round . (* (toDouble t))) ratios
+  toDouble = fromIntegral :: Int -> Double
+  
+  in if M.null m
+     then M.empty
+     else adjust rounded t
+
+adjust ::
+  Ord k
+  => M.Map k Int
+  -> Int
+  -> M.Map k Int
+adjust ws w = let
+  wsInts = fmap fromIntegral ws
+  diff = (fromIntegral w) - F.sum wsInts in
+  if M.null ws
+  then M.empty
+  else if diff == 0
+       then ws
+       else let
+         ws' = St.evalState (T.mapM adjustMap ws) diff
+         in adjust ws' w
+
+-- | The state is the target number minus the current actual total.
+adjustMap :: Int -> St.State Int Int
+adjustMap w = do
+  diff <- St.get
+  case compare diff 0 of
+    EQ -> return w
+    GT -> do
+      St.put (pred diff)
+      return (succ w)
+    LT -> do
+      St.put (succ diff)
+      return (pred w)
diff --git a/Penny/Cabin/Balance.hs b/Penny/Cabin/Balance.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Balance.hs
@@ -0,0 +1,21 @@
+-- | The Penny balance report
+module Penny.Cabin.Balance (balanceReport, O.defaultOptions,
+                            O.Options(..),
+                            defaultBalanceReport) where
+
+import qualified Penny.Cabin.Balance.Parser as P
+import qualified Penny.Cabin.Balance.Options as O
+import qualified Penny.Cabin.Balance.Help as H
+import qualified Penny.Cabin.Interface as I
+import qualified Penny.Shield as S
+
+balanceReport :: (S.Runtime -> O.Options) -> I.Report
+balanceReport getOpts = I.Report H.help "balance" pr
+  where
+    pr = do
+      fn <- P.parser
+      let f rt _ _ bs ps = fn rt (getOpts rt) bs ps
+      return f
+
+defaultBalanceReport :: I.Report
+defaultBalanceReport = balanceReport O.defaultOptions
diff --git a/Penny/Cabin/Balance/Help.hs b/Penny/Cabin/Balance/Help.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Balance/Help.hs
@@ -0,0 +1,34 @@
+module Penny.Cabin.Balance.Help where
+
+import qualified Data.Text as X
+
+help :: X.Text
+help = X.pack helpStr
+
+helpStr :: String
+helpStr = unlines [
+  "balance, bal",
+  "  Show account balances. Accepts ONLY the following options:",
+  "",
+  "    --color yes|no|auto|256",
+  "    yes: show 8 colors always",
+  "    no: never show colors",
+  "    auto: show 8 or 256 colors, but only if stdout is a terminal",
+  "    256: show 256 colors always",
+  "  --background light|dark",
+  "    Use appropriate color scheme for terminal background",
+  "",
+  "  --show-zero-balances",
+  "    Show balances that are zero",
+  "  --hide-zero-balances",
+  "    Hide balances that are zero",
+  "",
+  "  --convert commodity dateTime",
+  "    Convert all commodities to the given commodity using their",
+  "    price at the given date (and, optionally, time.) Fails if",
+  "    any commodity does not have the necessary price.",
+  "  -c commodity",
+  "    Same as \"--convert commodity [right now]\"",
+  ""
+  ]
+  
diff --git a/Penny/Cabin/Balance/Options.hs b/Penny/Cabin/Balance/Options.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Balance/Options.hs
@@ -0,0 +1,37 @@
+-- | Options for the Balance report.
+module Penny.Cabin.Balance.Options where
+
+import qualified Data.Text as X
+import qualified Penny.Lincoln as L
+import qualified Penny.Cabin.Chunk as Chunk
+import qualified Penny.Cabin.Colors as C
+import qualified Penny.Cabin.Options as O
+import qualified Penny.Shield as S
+import qualified Penny.Cabin.Colors.DarkBackground as DB
+import qualified Penny.Copper.DateTime as DT
+import qualified Penny.Lincoln.Balance as Bal
+
+data Options = Options {
+  drCrColors :: C.DrCrColors
+  , baseColors :: C.BaseColors
+  , balanceFormat :: L.BottomLine -> X.Text
+  , colorPref :: Chunk.Colors
+  , showZeroBalances :: O.ShowZeroBalances
+  , convert :: Maybe (L.Commodity, L.DateTime)
+  , defaultTimeZone :: DT.DefaultTimeZone
+  }
+
+balanceAsIs :: L.BottomLine -> X.Text
+balanceAsIs n = case n of
+  L.Zero -> X.pack "--"
+  L.NonZero c -> X.pack . show . L.unQty . Bal.qty $ c
+
+defaultOptions :: S.Runtime -> Options
+defaultOptions rt = Options {
+  drCrColors = DB.drCrColors
+  , baseColors = DB.baseColors
+  , balanceFormat = balanceAsIs
+  , colorPref = O.maxCapableColors rt
+  , showZeroBalances = O.ShowZeroBalances False
+  , convert = Nothing
+  , defaultTimeZone = DT.utcDefault }
diff --git a/Penny/Cabin/Balance/Parser.hs b/Penny/Cabin/Balance/Parser.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Balance/Parser.hs
@@ -0,0 +1,148 @@
+module Penny.Cabin.Balance.Parser (parser) where
+
+import qualified Data.Text as X
+import qualified Data.Text.Lazy as XL
+import Control.Applicative ((<|>), many)
+import Control.Monad ((>=>))
+import qualified Control.Monad.Exception.Synchronous as Ex
+import qualified Penny.Cabin.Colors as Col
+import qualified Penny.Cabin.Colors.DarkBackground as DB
+import qualified Penny.Cabin.Colors.LightBackground as LB
+import qualified Penny.Cabin.Chunk as Chk
+import qualified Penny.Cabin.Balance.Options as O
+import qualified Penny.Cabin.Balance.Tree as Tree
+import qualified Penny.Cabin.Options as CO
+import qualified Penny.Copper.Commodity as CC
+import qualified Penny.Copper.DateTime as CD
+import qualified Penny.Liberty as Ly
+import qualified Penny.Lincoln as L
+import qualified Penny.Shield as S
+import System.Console.MultiArg.Prim (Parser)
+import qualified System.Console.MultiArg.Combinator as C
+import qualified Text.Parsec as Parsec
+
+data Error = BadColorName String
+           | BadBackground String
+           | BadCommodity String
+           | BadDate String
+             deriving Show
+
+parser ::
+  Parser (S.Runtime
+          -> O.Options
+          -> [L.Box Ly.LibertyMeta]
+          -> [L.PricePoint]
+          -> Ex.Exceptional X.Text XL.Text)
+parser = do
+  ls <- many opts
+  let f rt opInit bs ps = do
+        let ls' = map (\fn -> fn rt) ls
+            errOpParsed = (foldl (>=>) return ls') opInit
+        opParsed <- case errOpParsed of
+          Ex.Exception e -> Ex.throw . X.pack . show $ e
+          Ex.Success g -> return g
+        bits <- Tree.report opParsed bs ps
+        return
+          . Chk.chunksToText (O.colorPref opParsed)
+          . concat
+          $ bits
+  return f
+
+processColorArg ::
+  S.Runtime
+  -> String
+  -> Maybe Chk.Colors
+processColorArg rt x
+  | x == "yes" = return Chk.Colors8
+  | x == "no" = return Chk.Colors0
+  | x == "auto" = return (CO.maxCapableColors rt)
+  | x == "256" = return Chk.Colors256
+  | otherwise = Nothing
+
+parseOpt :: [String] -> [Char] -> C.ArgSpec a -> Parser a
+parseOpt ss cs a = C.parseOption [C.OptSpec ss cs a]
+
+color :: Parser (S.Runtime
+                 -> O.Options
+                 -> Ex.Exceptional Error O.Options)
+color = parseOpt ["color"] "" (C.OneArg f)
+  where
+    f a1 rt op = case processColorArg rt a1 of
+      Nothing -> Ex.throw . BadColorName $ a1
+      Just c -> return (op { O.colorPref = c })
+
+processBackgroundArg ::
+  String
+  -> Maybe (Col.DrCrColors, Col.BaseColors)
+processBackgroundArg x
+  | x == "light" = return (LB.drCrColors, LB.baseColors)
+  | x == "dark" = return (DB.drCrColors, DB.baseColors)
+  | otherwise = Nothing
+
+
+background :: Parser (O.Options -> Ex.Exceptional Error O.Options)
+background = parseOpt ["background"] "" (C.OneArg f)
+  where
+    f a1 op = case processBackgroundArg a1 of
+      Nothing -> Ex.throw . BadBackground $ a1
+      Just (dc, base) ->
+        return op { O.drCrColors = dc
+                  , O.baseColors = base }
+
+showZeroBalances ::
+  Parser (O.Options -> Ex.Exceptional a O.Options)
+showZeroBalances = parseOpt ["show-zero-balances"] "" (C.NoArg f)
+  where
+    f op =
+      return (op {O.showZeroBalances = CO.ShowZeroBalances True })
+
+hideZeroBalances ::
+  Parser (O.Options -> Ex.Exceptional a O.Options)
+hideZeroBalances = parseOpt ["hide-zero-balances"] "" (C.NoArg f)
+  where
+    f op =
+      return (op {O.showZeroBalances = CO.ShowZeroBalances False })
+
+convertLong ::
+  Parser (O.Options -> Ex.Exceptional Error O.Options)
+convertLong = parseOpt ["convert"] "" (C.TwoArg f)
+  where
+    f a1 a2 op = do
+      cty <- case Parsec.parse CC.lvl1Cmdty "" (X.pack a1) of
+        Left _ -> Ex.throw . BadCommodity $ a1
+        Right g -> return g
+      let parseDate = CD.dateTime (O.defaultTimeZone op)
+      dt <- case Parsec.parse parseDate "" (X.pack a2) of
+        Left _ -> Ex.throw . BadDate $ a2
+        Right g -> return g
+      let op' = op { O.convert = Just (cty, dt) }
+      return op'
+
+convertShort :: Parser (S.Runtime
+                        -> O.Options
+                        -> Ex.Exceptional Error O.Options)
+convertShort = parseOpt [] ['c'] (C.OneArg f)
+  where
+    f a1 rt op = do
+      cty <- case Parsec.parse CC.lvl1Cmdty "" (X.pack a1) of
+        Left _ -> Ex.throw . BadCommodity $ a1
+        Right g -> return g
+      let dt = S.currentTime rt
+          op' = op { O.convert = Just (cty, dt) }
+      return op'
+        
+
+opts :: Parser (S.Runtime
+                -> O.Options
+                -> Ex.Exceptional Error O.Options)
+opts =
+  color
+  <|> mkTwoArg background
+  <|> mkTwoArg showZeroBalances
+  <|> mkTwoArg hideZeroBalances
+  <|> mkTwoArg convertLong
+  <|> convertShort
+  where
+    mkTwoArg p = do
+      f <- p
+      return (\_ op -> f op)
diff --git a/Penny/Cabin/Balance/Tree.hs b/Penny/Cabin/Balance/Tree.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Balance/Tree.hs
@@ -0,0 +1,448 @@
+-- | Takes postings and places them into a tree for further
+-- processing.
+--
+-- Steps:
+--
+-- * 1. [LT.PostingInfo] -> [PriceConverted]
+--
+-- * 2. [PriceConverted] -> FlatMap
+--
+-- * 3. FlatMap -> RawBals
+--
+-- * 4. RawBals -> (SummedBals, TotalBal)
+--
+-- * 5. (SummedBals, TotalBal) -> (SummedWithIsEven, TotalBal)
+--
+-- * 6. (SummedWithIsEven, TotalBal) -> (PreSpecMap, TotalBal)
+--
+-- * 7. (PreSpecMap, TotalBal) -> [Columns PreSpec]
+--
+-- * 8. [Columns PreSpec] -> [Columns R.ColumnSpec] (strict)
+--
+-- * 9. [Columns R.ColumnSpec] -> [[Chunk.Bit]] (lazy)
+module Penny.Cabin.Balance.Tree (report) where
+
+import Control.Applicative(Applicative(pure, (<*>)), (<$>))
+import qualified Control.Monad.Exception.Synchronous as Ex
+import qualified Control.Monad.Trans.State as St
+import qualified Penny.Cabin.Row as R
+import qualified Data.Foldable as Fdbl
+import qualified Data.Functor.Identity as Id
+import qualified Data.Map as M
+import qualified Data.Monoid as Monoid
+import qualified Penny.Lincoln.NestedMap as NM
+import qualified Data.Text as X
+import qualified Data.Traversable as Tr
+import qualified Penny.Cabin.Options as CO
+import qualified Penny.Cabin.Balance.Options as O
+import qualified Penny.Cabin.Chunk as Chunk
+import qualified Penny.Cabin.Colors as C
+import qualified Penny.Liberty as Ly
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.Queries as Q
+import qualified Penny.Lincoln.Balance as Bal
+import qualified Data.Semigroup as S
+
+-- Step 1. Convert prices.
+data PriceConverted = PriceConverted {
+  pcEntry :: L.Entry
+  , pcAccount :: L.Account }
+
+convertOne ::
+  L.PriceDb
+  -> (L.Commodity, L.DateTime)
+  -> L.Entry
+  -> Ex.Exceptional X.Text L.Entry
+convertOne db (cty, dt) en@(L.Entry dc am@(L.Amount _ fr)) =
+  if fr == cty
+  then return en
+  else do
+    let to = L.To cty
+    am' <- case L.convert db dt to am of
+      Ex.Exception e -> Ex.throw (convertError cty am e)
+      Ex.Success g -> return g
+    return (L.Entry dc am')
+
+convertError ::
+  L.Commodity
+  -> L.Amount
+  -> L.PriceDbError
+  -> X.Text
+convertError to (L.Amount _ fr) e =
+  let fromErr = L.text (L.Delimited (X.singleton ':')
+                        (Fdbl.toList . L.unCommodity $ fr))
+      toErr = L.text (L.Delimited (X.singleton ':')
+                      (Fdbl.toList . L.unCommodity $ to))
+  in case e of
+    L.FromNotFound ->
+      X.pack "no data to convert from commodity "
+      `X.append` fromErr
+    L.ToNotFound ->
+      X.pack "no data to convert to commodity "
+      `X.append` toErr
+    L.CpuNotFound ->
+      X.pack "no data to convert from commodity "
+      `X.append` fromErr
+      `X.append` (X.pack " to commodity ")
+      `X.append` toErr
+      `X.append` (X.pack " at given date and time")
+  
+
+buildDb :: [L.PricePoint] -> L.PriceDb
+buildDb = foldl f L.emptyDb where
+  f db pb = L.addPrice db pb
+
+converter ::
+  O.Options
+  -> [L.PricePoint]
+  -> [L.Box Ly.LibertyMeta]
+  -> Ex.Exceptional X.Text [PriceConverted]
+converter os pbs bs = case O.convert os of
+  Nothing ->
+    let f b = PriceConverted en ac where
+          p = L.boxPostFam b
+          en = Q.entry p
+          ac = Q.account p
+    in return . map f $ bs
+  Just pair ->
+    let f b = let p = L.boxPostFam b
+                  en = Q.entry p
+                  ac = Q.account p
+                 in do
+                   en' <- convertOne db pair en
+                   return (PriceConverted en' ac)
+        db = buildDb pbs
+    in mapM f bs
+
+
+-- Step 2. This puts all the PriceConverteds into a flat map, where
+-- the key is the full account name and the value is the balance. If
+-- the user desired, zero balances are eliminated from the flat map.
+newtype FlatMap = FlatMap { _unFlatMap :: M.Map L.Account Bal.Balance }
+
+toFlatMap :: O.Options -> [PriceConverted] -> FlatMap
+toFlatMap o = FlatMap . foldr f M.empty where
+  remove = not . CO.unShowZeroBalances . O.showZeroBalances $ o
+  f pc m =
+    let a = pcAccount pc
+        bal = Bal.entryToBalance . pcEntry $ pc
+    in case M.lookup a m of
+      Nothing -> M.insert a bal m
+      Just oldBal ->
+        let added = Bal.addBalances oldBal bal
+            newBal = if remove
+                     then Bal.removeZeroCommodities added
+                     else Just added
+        in case newBal of
+          Nothing -> M.delete a m
+          Just b' -> M.insert a b' m
+
+-- Step 3
+newtype RawBal = RawBal { unRawBal :: S.Option Bal.Balance }
+instance Monoid.Monoid RawBal where
+  mappend (RawBal b1) (RawBal b2) = RawBal $ b1 `Monoid.mappend` b2
+  mempty = RawBal Monoid.mempty
+
+type RawBals = NM.NestedMap L.SubAccountName RawBal
+
+-- | Inserts a pair from the FlatMap into the Balances NestedMap.
+insertBalance ::
+  L.Account
+  -> Bal.Balance
+  -> RawBals
+  -> RawBals
+insertBalance a b rbs = let
+  rb = RawBal . S.Option . Just $ b
+  subs = Fdbl.toList . L.unAccount $ a
+  in NM.insert rbs subs rb
+  
+rawBalances :: FlatMap -> RawBals
+rawBalances (FlatMap m) = M.foldrWithKey insertBalance NM.empty m
+
+-- Step 4
+newtype SummedBal =
+  SummedBal { unSummedBal :: S.Option Bal.Balance }
+instance Monoid.Monoid SummedBal where
+  mappend (SummedBal b1) (SummedBal b2) =
+    SummedBal $ b1 `Monoid.mappend` b2
+  mempty = SummedBal Monoid.mempty
+type TotalBal = SummedBal
+type SummedBals = NM.NestedMap L.SubAccountName SummedBal
+
+sumBalances :: RawBals -> (SummedBals, TotalBal)
+sumBalances rb = (sb, (SummedBal . unRawBal $ tb)) where
+  (tb, rawBals) = NM.cumulativeTotal rb
+  sb = fmap (SummedBal . unRawBal) rawBals
+
+-- Step 5
+type SummedWithIsEven = NM.NestedMap L.SubAccountName (SummedBal, Bool)
+
+makeSummedWithIsEven ::
+  (SummedBals, TotalBal)
+  -> (SummedWithIsEven, TotalBal)
+makeSummedWithIsEven (sb, tb) = (swie, tb) where
+  swie = St.evalState (Tr.mapM f sb) False
+  f lbl = do
+    st <- St.get
+    St.put (not st)
+    return (lbl, st)
+
+-- Step 6
+data Columns a = Columns {
+  account :: a
+  , drCr :: a
+  , commodity :: a
+  , quantity :: a
+  } deriving Show
+
+instance Functor Columns where
+  fmap f c = Columns {
+    account = f (account c)
+    , drCr = f (drCr c)
+    , commodity = f (commodity c)
+    , quantity = f (quantity c)
+    }
+
+instance Applicative Columns where
+  pure a = Columns a a a a
+  fn <*> fa = Columns {
+    account = (account fn) (account fa)
+    , drCr = (drCr fn) (drCr fa)
+    , commodity = (commodity fn) (commodity fa)
+    , quantity = (quantity fn) (quantity fa)
+     }
+
+data PreSpec = PreSpec {
+  _justification :: R.Justification
+  , _padSpec :: Chunk.TextSpec
+  , bits :: [Chunk.Chunk] }
+
+type PreSpecMap = NM.NestedMap L.SubAccountName (Columns PreSpec)
+
+-- | Takes a list of triples from bottomLineChunks and creates three
+-- Cells, one each for DrCr, Commodity, and Qty.
+bottomLineBalCells ::
+  Chunk.TextSpec -- ^ Fill colors
+  -> [(Chunk.Chunk, Chunk.Chunk, Chunk.Chunk)]
+  -> (PreSpec, PreSpec, PreSpec)
+bottomLineBalCells spec ts = (mkSpec dc, mkSpec ct, mkSpec qt) where
+  mkSpec ls = PreSpec R.LeftJustify spec ls
+  (dc, ct, qt) = foldr f ([], [], []) ts
+  f (da, ca, qa) (d, c, q) = (da:d, ca:c, qa:q)
+
+
+type IsEven = Bool
+
+-- | Returns a triple (x, y, z), where x is the DrCr chunk, y is the
+-- commodity chunk, and z is the qty chunk.
+bottomLineBalChunks ::
+  O.Options
+  -> IsEven
+  -> (L.Commodity, Bal.BottomLine)
+  -> (Chunk.Chunk, Chunk.Chunk, Chunk.Chunk)
+bottomLineBalChunks os isEven (comm, bl) = (dc, cty, qty) where
+  dc = Chunk.chunk ts dcTxt
+  cty = Chunk.chunk ts ctyTxt
+  qty = Chunk.chunk ts qtyTxt
+  ctyTxt = L.text (L.Delimited (X.singleton ':') (L.textList comm))
+  (ts, dcTxt, qtyTxt) = case bl of
+    Bal.Zero -> let
+      getTs = if isEven then C.evenZero else C.oddZero
+      dcT = X.pack "--"
+      qtyT = dcT
+      in (getTs . O.drCrColors $ os, dcT, qtyT)
+    Bal.NonZero clm -> let
+      (getTs, dcT) = case Bal.drCr clm of
+        L.Debit ->
+          (if isEven then C.evenDebit else C.oddDebit,
+           X.pack "Dr")
+        L.Credit ->
+          (if isEven then C.evenCredit else C.oddCredit,
+           X.pack "Cr")
+      qTxt = (O.balanceFormat os) bl
+      in (getTs . O.drCrColors $ os, dcT, qTxt)
+
+
+fillTextSpec ::
+  O.Options
+  -> IsEven
+  -> Chunk.TextSpec
+fillTextSpec os isEven = let
+  getTs = if isEven then C.evenColors else C.oddColors
+  in getTs . O.baseColors $ os
+  
+
+bottomLineCells ::
+  O.Options
+  -> IsEven
+  -> SummedBal
+  -> (PreSpec, PreSpec, PreSpec)
+bottomLineCells os isEven mayBal = let
+  fill = fillTextSpec os isEven
+  tsZero = if isEven
+           then C.evenZero . O.drCrColors $ os
+           else C.oddZero . O.drCrColors $ os
+  zeroSpec =
+    PreSpec R.LeftJustify
+    tsZero [Chunk.chunk tsZero (X.pack "--")]
+  zeroSpecs = (zeroSpec, zeroSpec, zeroSpec)
+  in case S.getOption . unSummedBal $ mayBal of
+    Nothing -> zeroSpecs
+    Just bal -> bottomLineBalCells fill
+                . map (bottomLineBalChunks os isEven)
+                . M.assocs
+                . Bal.unBalance
+                $ bal
+
+padding :: Int
+padding = 2
+
+accountPreSpec ::
+  O.Options
+  -> IsEven
+  -> Int
+  -> L.SubAccountName
+  -> PreSpec
+accountPreSpec os isEven lvl acct = PreSpec j ts [bit] where
+  j = R.LeftJustify
+  ts = if isEven
+       then C.evenColors . O.baseColors $ os
+       else C.oddColors . O.baseColors $ os
+  bit = Chunk.chunk ts txt where
+    txt = pad `X.append` (L.text acct)
+    pad = X.replicate (padding * lvl) (X.singleton ' ')
+
+
+makePreSpec ::
+  O.Options
+  -> [(L.SubAccountName, (SummedBal, Bool))]
+  -> L.SubAccountName
+  -> (SummedBal, Bool)
+  -> Columns PreSpec
+makePreSpec os ps a (mayBal, isEven) = Columns act dc com qt where
+  lvl = length ps + 1
+  act = accountPreSpec os isEven lvl a
+  (dc, com, qt) = bottomLineCells os isEven mayBal
+
+traverser ::
+  O.Options
+  -> [(L.SubAccountName, (SummedBal, Bool))]
+  -> L.SubAccountName
+  -> (SummedBal, Bool)
+  -> a
+  -> Id.Identity (Maybe (Columns PreSpec))
+traverser os hist a mayBal _ =
+  return (Just $ makePreSpec os hist a mayBal)
+
+makePreSpecMap ::
+  O.Options
+  -> (SummedWithIsEven, TotalBal)
+  -> (PreSpecMap, TotalBal)
+makePreSpecMap os (sb, tb) = (cim, tb) where
+  cim = Id.runIdentity (NM.traverseWithTrail (traverser os) sb)
+
+-- Step 7
+makeTotalCells ::
+  O.Options
+  -> SummedBal
+  -> Columns PreSpec
+makeTotalCells os mayBal = Columns act dc com qt where
+  act = accountPreSpec os True 0 tot
+  tot = L.SubAccountName $ L.TextNonEmpty 'T' (X.pack "otal")
+  (dc, com, qt) = bottomLineCells os True mayBal
+
+makeColumnList :: O.Options -> (PreSpecMap, TotalBal) -> [Columns PreSpec]
+makeColumnList os (cim, tb) = totCols : restCols where
+  totCols = makeTotalCells os tb
+  restCols = Fdbl.toList cim
+
+
+-- Step 8
+
+-- | When given a list of columns, determine the widest row in each
+-- column.
+maxWidths :: [Columns PreSpec] -> Columns R.Width
+maxWidths = Fdbl.foldl' maxWidthPerColumn (pure (R.Width 0))
+
+-- | Applied to a Columns of PreSpec and a Colums of widths, return a
+-- Columns that has the wider of the two values.
+maxWidthPerColumn ::
+  Columns R.Width
+  -> Columns PreSpec
+  -> Columns R.Width
+maxWidthPerColumn w p = f <$> w <*> p where
+  f old new = max old (maximum . map Chunk.chunkWidth . bits $ new)
+  
+-- | Changes a single set of Columns to a set of ColumnSpec of the
+-- given width.
+preSpecToSpec ::
+  Columns R.Width
+  -> Columns PreSpec
+  -> Columns R.ColumnSpec
+preSpecToSpec ws p = f <$> ws <*> p where
+  f width (PreSpec j ps bs) = R.ColumnSpec j width ps bs
+
+resizeColumnsInList :: [Columns PreSpec] -> [Columns R.ColumnSpec]
+resizeColumnsInList cs = map (preSpecToSpec w) cs where
+  w = maxWidths cs
+
+
+-- Step 9
+widthSpacerAcct :: Int
+widthSpacerAcct = 4
+
+widthSpacerDrCr :: Int
+widthSpacerDrCr = 1
+
+widthSpacerCommodity :: Int
+widthSpacerCommodity = 1
+
+colsToBits ::
+  O.Options
+  -> IsEven
+  -> Columns R.ColumnSpec
+  -> [Chunk.Chunk]
+colsToBits os isEven (Columns a dc c q) = let
+  fillSpec = if isEven
+             then C.evenColors . O.baseColors $ os
+             else C.oddColors . O.baseColors $ os
+  spacer w = R.ColumnSpec j (Chunk.Width w) fillSpec []
+  j = R.LeftJustify
+  cs = a
+       : spacer widthSpacerAcct
+       : dc
+       : spacer widthSpacerDrCr
+       : c
+       : spacer widthSpacerCommodity
+       : q
+       : []
+  in R.row cs
+
+colsListToBits ::
+  O.Options
+  -> [Columns R.ColumnSpec]
+  -> [[Chunk.Chunk]]
+colsListToBits os = zipWith f bools where
+  f b c = colsToBits os b c
+  bools = iterate not True
+
+
+-- Tie it all together
+
+report ::
+  O.Options
+  -> [L.Box Ly.LibertyMeta]
+  -> [L.PricePoint]
+  -> Ex.Exceptional X.Text [[Chunk.Chunk]]
+report os ps rs = do
+  pcs <- converter os rs ps
+  return
+    . colsListToBits os
+    . resizeColumnsInList
+    . makeColumnList os
+    . makePreSpecMap os
+    . makeSummedWithIsEven
+    . sumBalances
+    . rawBalances
+    . toFlatMap os
+    $ pcs
diff --git a/Penny/Cabin/Chunk.hs b/Penny/Cabin/Chunk.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Chunk.hs
@@ -0,0 +1,2408 @@
+-- | Handles colors and special effects for text. This module was
+-- written using the control sequences documented for xterm in
+-- ctlseqs.txt, which is included in the xterm source code (on Debian
+-- GNU/Linux systems, it is at
+-- \/usr\/share\/doc\/xterm\/ctlseqs.txt.gz). The code in here should also
+-- work for any terminal which recognizes ISO 6429 escape sequences
+-- (also known as ANSI escape sequences), but only for 8 colors. This
+-- module also generates sequences for 256 color xterms; though this
+-- works fine with xterm, it might not work on other terminals (I
+-- believe it works for other terminals commonly found on Linux
+-- systems, such as gnome-terminal and Konsole, but I have not tested
+-- these terminals as I do not use them.) Perhaps it also works on
+-- Windows or Mac OS X systems and their terminals, though I have not
+-- tested them (in particular, Windows could be problematic as not all
+-- Windows terminals support ISO 6429.)
+--
+-- In theory there are more portable ways to generate color codes,
+-- such as through curses (or is it ncurses?) but support for ISO 6429
+-- is widespread enough that I am prepared to say that if your
+-- terminal does not support it, too bad; just use the colorless mode.
+module Penny.Cabin.Chunk (
+  -- * Colors
+  Colors(Colors0, Colors8, Colors256),
+  Background8,
+  Background256,
+  Foreground8,
+  Foreground256,
+
+  -- * Chunks
+  Chunk,
+  chunk,
+  Width(Width, unWidth),
+  chunkWidth,
+  chunksToText,
+  
+  -- * Effects
+  Bold(Bold, unBold),
+  Underline(Underline, unUnderline),
+  Flash(Flash, unFlash),
+  Inverse(Inverse, unInverse),
+
+  -- * Style and TextSpec
+  
+  -- | A style is a bundle of attributes that describes text
+  -- attributes, such as its color and whether it is bold.
+  StyleCommon (StyleCommon, bold, underline, flash, inverse),
+  Style8 (Style8, foreground8, background8, common8),
+  Style256 (Style256, foreground256, background256, common256),
+  defaultStyleCommon,
+  defaultStyle8,
+  defaultStyle256,
+  
+  TextSpec (TextSpec, style8, style256),
+  defaultTextSpec,
+  
+  -- * Specific colors
+  -- * 8 color foreground colors
+  color8_f_default,
+  color8_f_black,
+  color8_f_red,
+  color8_f_green,
+  color8_f_yellow,
+  color8_f_blue,
+  color8_f_magenta,
+  color8_f_cyan,
+  color8_f_white,
+
+  -- ** 8 color background colors
+  color8_b_default,
+  color8_b_black,
+  color8_b_red,
+  color8_b_green,
+  color8_b_yellow,
+  color8_b_blue,
+  color8_b_magenta,
+  color8_b_cyan,
+  color8_b_white,
+
+  -- * 256 color foreground colors
+  color256_f_default,
+  color256_f_0,
+  color256_f_1,
+  color256_f_2,
+  color256_f_3,
+  color256_f_4,
+  color256_f_5,
+  color256_f_6,
+  color256_f_7,
+  color256_f_8,
+  color256_f_9,
+  color256_f_10,
+  color256_f_11,
+  color256_f_12,
+  color256_f_13,
+  color256_f_14,
+  color256_f_15,
+  color256_f_16,
+  color256_f_17,
+  color256_f_18,
+  color256_f_19,
+  color256_f_20,
+  color256_f_21,
+  color256_f_22,
+  color256_f_23,
+  color256_f_24,
+  color256_f_25,
+  color256_f_26,
+  color256_f_27,
+  color256_f_28,
+  color256_f_29,
+  color256_f_30,
+  color256_f_31,
+  color256_f_32,
+  color256_f_33,
+  color256_f_34,
+  color256_f_35,
+  color256_f_36,
+  color256_f_37,
+  color256_f_38,
+  color256_f_39,
+  color256_f_40,
+  color256_f_41,
+  color256_f_42,
+  color256_f_43,
+  color256_f_44,
+  color256_f_45,
+  color256_f_46,
+  color256_f_47,
+  color256_f_48,
+  color256_f_49,
+  color256_f_50,
+  color256_f_51,
+  color256_f_52,
+  color256_f_53,
+  color256_f_54,
+  color256_f_55,
+  color256_f_56,
+  color256_f_57,
+  color256_f_58,
+  color256_f_59,
+  color256_f_60,
+  color256_f_61,
+  color256_f_62,
+  color256_f_63,
+  color256_f_64,
+  color256_f_65,
+  color256_f_66,
+  color256_f_67,
+  color256_f_68,
+  color256_f_69,
+  color256_f_70,
+  color256_f_71,
+  color256_f_72,
+  color256_f_73,
+  color256_f_74,
+  color256_f_75,
+  color256_f_76,
+  color256_f_77,
+  color256_f_78,
+  color256_f_79,
+  color256_f_80,
+  color256_f_81,
+  color256_f_82,
+  color256_f_83,
+  color256_f_84,
+  color256_f_85,
+  color256_f_86,
+  color256_f_87,
+  color256_f_88,
+  color256_f_89,
+  color256_f_90,
+  color256_f_91,
+  color256_f_92,
+  color256_f_93,
+  color256_f_94,
+  color256_f_95,
+  color256_f_96,
+  color256_f_97,
+  color256_f_98,
+  color256_f_99,
+  color256_f_100,
+  color256_f_101,
+  color256_f_102,
+  color256_f_103,
+  color256_f_104,
+  color256_f_105,
+  color256_f_106,
+  color256_f_107,
+  color256_f_108,
+  color256_f_109,
+  color256_f_110,
+  color256_f_111,
+  color256_f_112,
+  color256_f_113,
+  color256_f_114,
+  color256_f_115,
+  color256_f_116,
+  color256_f_117,
+  color256_f_118,
+  color256_f_119,
+  color256_f_120,
+  color256_f_121,
+  color256_f_122,
+  color256_f_123,
+  color256_f_124,
+  color256_f_125,
+  color256_f_126,
+  color256_f_127,
+  color256_f_128,
+  color256_f_129,
+  color256_f_130,
+  color256_f_131,
+  color256_f_132,
+  color256_f_133,
+  color256_f_134,
+  color256_f_135,
+  color256_f_136,
+  color256_f_137,
+  color256_f_138,
+  color256_f_139,
+  color256_f_140,
+  color256_f_141,
+  color256_f_142,
+  color256_f_143,
+  color256_f_144,
+  color256_f_145,
+  color256_f_146,
+  color256_f_147,
+  color256_f_148,
+  color256_f_149,
+  color256_f_150,
+  color256_f_151,
+  color256_f_152,
+  color256_f_153,
+  color256_f_154,
+  color256_f_155,
+  color256_f_156,
+  color256_f_157,
+  color256_f_158,
+  color256_f_159,
+  color256_f_160,
+  color256_f_161,
+  color256_f_162,
+  color256_f_163,
+  color256_f_164,
+  color256_f_165,
+  color256_f_166,
+  color256_f_167,
+  color256_f_168,
+  color256_f_169,
+  color256_f_170,
+  color256_f_171,
+  color256_f_172,
+  color256_f_173,
+  color256_f_174,
+  color256_f_175,
+  color256_f_176,
+  color256_f_177,
+  color256_f_178,
+  color256_f_179,
+  color256_f_180,
+  color256_f_181,
+  color256_f_182,
+  color256_f_183,
+  color256_f_184,
+  color256_f_185,
+  color256_f_186,
+  color256_f_187,
+  color256_f_188,
+  color256_f_189,
+  color256_f_190,
+  color256_f_191,
+  color256_f_192,
+  color256_f_193,
+  color256_f_194,
+  color256_f_195,
+  color256_f_196,
+  color256_f_197,
+  color256_f_198,
+  color256_f_199,
+  color256_f_200,
+  color256_f_201,
+  color256_f_202,
+  color256_f_203,
+  color256_f_204,
+  color256_f_205,
+  color256_f_206,
+  color256_f_207,
+  color256_f_208,
+  color256_f_209,
+  color256_f_210,
+  color256_f_211,
+  color256_f_212,
+  color256_f_213,
+  color256_f_214,
+  color256_f_215,
+  color256_f_216,
+  color256_f_217,
+  color256_f_218,
+  color256_f_219,
+  color256_f_220,
+  color256_f_221,
+  color256_f_222,
+  color256_f_223,
+  color256_f_224,
+  color256_f_225,
+  color256_f_226,
+  color256_f_227,
+  color256_f_228,
+  color256_f_229,
+  color256_f_230,
+  color256_f_231,
+  color256_f_232,
+  color256_f_233,
+  color256_f_234,
+  color256_f_235,
+  color256_f_236,
+  color256_f_237,
+  color256_f_238,
+  color256_f_239,
+  color256_f_240,
+  color256_f_241,
+  color256_f_242,
+  color256_f_243,
+  color256_f_244,
+  color256_f_245,
+  color256_f_246,
+  color256_f_247,
+  color256_f_248,
+  color256_f_249,
+  color256_f_250,
+  color256_f_251,
+  color256_f_252,
+  color256_f_253,
+  color256_f_254,
+  color256_f_255,
+
+  -- ** 256 color background colors
+  color256_b_default,
+  color256_b_0,
+  color256_b_1,
+  color256_b_2,
+  color256_b_3,
+  color256_b_4,
+  color256_b_5,
+  color256_b_6,
+  color256_b_7,
+  color256_b_8,
+  color256_b_9,
+  color256_b_10,
+  color256_b_11,
+  color256_b_12,
+  color256_b_13,
+  color256_b_14,
+  color256_b_15,
+  color256_b_16,
+  color256_b_17,
+  color256_b_18,
+  color256_b_19,
+  color256_b_20,
+  color256_b_21,
+  color256_b_22,
+  color256_b_23,
+  color256_b_24,
+  color256_b_25,
+  color256_b_26,
+  color256_b_27,
+  color256_b_28,
+  color256_b_29,
+  color256_b_30,
+  color256_b_31,
+  color256_b_32,
+  color256_b_33,
+  color256_b_34,
+  color256_b_35,
+  color256_b_36,
+  color256_b_37,
+  color256_b_38,
+  color256_b_39,
+  color256_b_40,
+  color256_b_41,
+  color256_b_42,
+  color256_b_43,
+  color256_b_44,
+  color256_b_45,
+  color256_b_46,
+  color256_b_47,
+  color256_b_48,
+  color256_b_49,
+  color256_b_50,
+  color256_b_51,
+  color256_b_52,
+  color256_b_53,
+  color256_b_54,
+  color256_b_55,
+  color256_b_56,
+  color256_b_57,
+  color256_b_58,
+  color256_b_59,
+  color256_b_60,
+  color256_b_61,
+  color256_b_62,
+  color256_b_63,
+  color256_b_64,
+  color256_b_65,
+  color256_b_66,
+  color256_b_67,
+  color256_b_68,
+  color256_b_69,
+  color256_b_70,
+  color256_b_71,
+  color256_b_72,
+  color256_b_73,
+  color256_b_74,
+  color256_b_75,
+  color256_b_76,
+  color256_b_77,
+  color256_b_78,
+  color256_b_79,
+  color256_b_80,
+  color256_b_81,
+  color256_b_82,
+  color256_b_83,
+  color256_b_84,
+  color256_b_85,
+  color256_b_86,
+  color256_b_87,
+  color256_b_88,
+  color256_b_89,
+  color256_b_90,
+  color256_b_91,
+  color256_b_92,
+  color256_b_93,
+  color256_b_94,
+  color256_b_95,
+  color256_b_96,
+  color256_b_97,
+  color256_b_98,
+  color256_b_99,
+  color256_b_100,
+  color256_b_101,
+  color256_b_102,
+  color256_b_103,
+  color256_b_104,
+  color256_b_105,
+  color256_b_106,
+  color256_b_107,
+  color256_b_108,
+  color256_b_109,
+  color256_b_110,
+  color256_b_111,
+  color256_b_112,
+  color256_b_113,
+  color256_b_114,
+  color256_b_115,
+  color256_b_116,
+  color256_b_117,
+  color256_b_118,
+  color256_b_119,
+  color256_b_120,
+  color256_b_121,
+  color256_b_122,
+  color256_b_123,
+  color256_b_124,
+  color256_b_125,
+  color256_b_126,
+  color256_b_127,
+  color256_b_128,
+  color256_b_129,
+  color256_b_130,
+  color256_b_131,
+  color256_b_132,
+  color256_b_133,
+  color256_b_134,
+  color256_b_135,
+  color256_b_136,
+  color256_b_137,
+  color256_b_138,
+  color256_b_139,
+  color256_b_140,
+  color256_b_141,
+  color256_b_142,
+  color256_b_143,
+  color256_b_144,
+  color256_b_145,
+  color256_b_146,
+  color256_b_147,
+  color256_b_148,
+  color256_b_149,
+  color256_b_150,
+  color256_b_151,
+  color256_b_152,
+  color256_b_153,
+  color256_b_154,
+  color256_b_155,
+  color256_b_156,
+  color256_b_157,
+  color256_b_158,
+  color256_b_159,
+  color256_b_160,
+  color256_b_161,
+  color256_b_162,
+  color256_b_163,
+  color256_b_164,
+  color256_b_165,
+  color256_b_166,
+  color256_b_167,
+  color256_b_168,
+  color256_b_169,
+  color256_b_170,
+  color256_b_171,
+  color256_b_172,
+  color256_b_173,
+  color256_b_174,
+  color256_b_175,
+  color256_b_176,
+  color256_b_177,
+  color256_b_178,
+  color256_b_179,
+  color256_b_180,
+  color256_b_181,
+  color256_b_182,
+  color256_b_183,
+  color256_b_184,
+  color256_b_185,
+  color256_b_186,
+  color256_b_187,
+  color256_b_188,
+  color256_b_189,
+  color256_b_190,
+  color256_b_191,
+  color256_b_192,
+  color256_b_193,
+  color256_b_194,
+  color256_b_195,
+  color256_b_196,
+  color256_b_197,
+  color256_b_198,
+  color256_b_199,
+  color256_b_200,
+  color256_b_201,
+  color256_b_202,
+  color256_b_203,
+  color256_b_204,
+  color256_b_205,
+  color256_b_206,
+  color256_b_207,
+  color256_b_208,
+  color256_b_209,
+  color256_b_210,
+  color256_b_211,
+  color256_b_212,
+  color256_b_213,
+  color256_b_214,
+  color256_b_215,
+  color256_b_216,
+  color256_b_217,
+  color256_b_218,
+  color256_b_219,
+  color256_b_220,
+  color256_b_221,
+  color256_b_222,
+  color256_b_223,
+  color256_b_224,
+  color256_b_225,
+  color256_b_226,
+  color256_b_227,
+  color256_b_228,
+  color256_b_229,
+  color256_b_230,
+  color256_b_231,
+  color256_b_232,
+  color256_b_233,
+  color256_b_234,
+  color256_b_235,
+  color256_b_236,
+  color256_b_237,
+  color256_b_238,
+  color256_b_239,
+  color256_b_240,
+  color256_b_241,
+  color256_b_242,
+  color256_b_243,
+  color256_b_244,
+  color256_b_245,
+  color256_b_246,
+  color256_b_247,
+  color256_b_248,
+  color256_b_249,
+  color256_b_250,
+  color256_b_251,
+  color256_b_252,
+  color256_b_253,
+  color256_b_254,
+  color256_b_255
+
+  ) where
+
+
+import Data.Monoid (Monoid, mempty, mappend)
+import Data.Text (Text)
+import qualified Data.Text as X
+import qualified Data.Text.Lazy as XL
+import qualified Data.Text.Lazy.Builder as TB
+
+--
+-- Colors
+--
+
+-- | How many colors to actually show.
+data Colors = Colors0 | Colors8 | Colors256
+            deriving Show
+
+-- | Background color in an 8 color setting.
+newtype Background8 = Background8 { unBackground8 :: Code }
+
+-- | Background color in a 256 color setting.
+newtype Background256 = Background256 { unBackground256 :: Code }
+
+-- | Foreground color in an 8 color setting.
+newtype Foreground8 = Foreground8 { unForeground8 :: Code }
+
+-- | Foreground color in a 256 color setting.
+newtype Foreground256 = Foreground256 { unForeground256 :: Code }
+
+
+--
+-- Chunks
+--
+
+-- | A chunk is some textual data coupled with a description of what
+-- color the text is. The chunk knows what colors to use for both
+-- foreground color and background color, in both an 8 color terminal
+-- and a 256 color terminal. The chunk has only one set of color
+-- descriptions. To change colors, you must use a new chunk.
+--
+-- There is no way to combine chunks. To print large numbers of
+-- chunks, lazily build a list of them and then print them using
+-- chunksToText.
+data Chunk = Chunk TextSpec Text
+
+chunk :: TextSpec -> Text -> Chunk
+chunk = Chunk
+
+-- | How wide the text of a chunk is.
+newtype Width = Width { unWidth :: Int }
+                deriving (Show, Eq, Ord)
+
+instance Monoid Width where
+  mempty = Width 0
+  mappend (Width w1) (Width w2) = Width $ w1 + w2
+
+chunkWidth :: Chunk -> Width
+chunkWidth (Chunk _ t) = Width . X.length $ t
+
+-- | Transforms chunks to a lazy Text. This function runs lazily and
+-- in constant time and space.
+chunksToText :: Colors -> [Chunk] -> XL.Text
+chunksToText c = TB.toLazyText . foldr (builder c) (printReset c)
+
+
+--
+-- Effects
+--
+newtype Bold = Bold { unBold :: Bool }
+newtype Underline = Underline { unUnderline :: Bool }
+newtype Flash = Flash { unFlash :: Bool }
+newtype Inverse = Inverse { unInverse :: Bool }
+
+--
+-- Styles
+--
+
+-- | Style elements that apply in both 8 and 256 color
+-- terminals. However, the elements are described separately for 8 and
+-- 256 color terminals, so that the text appearance can change
+-- depending on how many colors a terminal has.
+data StyleCommon = StyleCommon {
+  bold :: Bold
+  , underline :: Underline
+  , flash :: Flash
+  , inverse :: Inverse }
+
+-- | Describes text appearance (foreground and background colors, as
+-- well as other attributes such as bold) for an 8 color terminal.
+data Style8 = Style8 {
+  foreground8 :: Foreground8
+  , background8 :: Background8
+  , common8 :: StyleCommon }
+
+-- | Describes text appearance (foreground and background colors, as
+-- well as other attributes such as bold) for a 256 color terminal.
+data Style256 = Style256 {
+  foreground256 :: Foreground256
+  , background256 :: Background256
+  , common256 :: StyleCommon }
+
+-- | Has all bold, flash, underline, and inverse turned off.
+defaultStyleCommon :: StyleCommon
+defaultStyleCommon = StyleCommon {
+  bold = Bold False
+  , underline = Underline False
+  , flash = Flash False
+  , inverse = Inverse False }
+
+-- | Uses the default terminal colors (which will vary depending on
+-- the terminal).
+defaultStyle8 :: Style8
+defaultStyle8 = Style8 {
+  foreground8 = color8_f_default
+  , background8 = color8_b_default
+  , common8 = defaultStyleCommon }
+
+-- | Uses the default terminal colors (which will vary depending on
+-- the terminal).
+defaultStyle256 :: Style256
+defaultStyle256 = Style256 {
+  foreground256 = color256_f_default
+  , background256 = color256_b_default
+  , common256 = defaultStyleCommon }
+
+--
+-- TextSpec
+--
+
+-- | The TextSpec bundles together the styles for the 8 and 256 color
+-- terminals, so that the text can be portrayed on any terminal.
+data TextSpec = TextSpec {
+  style8 :: Style8
+  , style256 :: Style256 }
+
+-- | A TextSpec with the default colors on 8 and 256 color terminals,
+-- with all attributes turned off.
+defaultTextSpec :: TextSpec
+defaultTextSpec = TextSpec {
+  style8 = defaultStyle8
+  , style256 = defaultStyle256 }
+
+--
+-- Internal
+--
+
+(+++) :: TB.Builder -> TB.Builder -> TB.Builder
+(+++) = mappend
+infixr 5 +++
+
+builder :: Colors -> Chunk -> TB.Builder -> TB.Builder
+builder c (Chunk ts t) acc =
+  printReset c
+  +++ textSpecCodes c ts 
+  +++ TB.fromText t
+  +++ acc
+
+textSpecCodes :: Colors -> TextSpec -> TB.Builder
+textSpecCodes c s = case c of
+  Colors0 -> mempty
+  Colors8 -> style8Colors $ style8 s
+  Colors256 -> style256Colors $ style256 s
+
+style8Colors :: Style8 -> TB.Builder
+style8Colors s =
+  (unCode . unForeground8 . foreground8 $ s)
+  +++ (unCode . unBackground8 . background8 $ s)
+  +++ (common (common8 s))
+
+style256Colors :: Style256 -> TB.Builder
+style256Colors s =
+  (unCode . unForeground256 . foreground256 $ s)
+  +++ (unCode . unBackground256 . background256 $ s)
+  +++ (common (common256 s))
+
+common :: StyleCommon -> TB.Builder
+common s =
+  (if unBold . bold $ s then unCode boldOn else mempty)
+  +++ (if unUnderline . underline $ s
+       then unCode underlineOn else mempty)
+  +++ (if unFlash . flash $ s
+       then unCode flashOn else mempty)
+  +++ (if unInverse . inverse $ s
+       then unCode inverseOn else mempty)
+
+newtype Code = Code { unCode :: TB.Builder }
+
+code :: String -> Code
+code s = Code $
+         TB.fromString (toEnum 27:'[':[])
+         +++ TB.fromString s
+         +++ TB.singleton 'm'
+
+boldOn :: Code
+boldOn = code "1"
+
+underlineOn :: Code
+underlineOn = code "4"
+
+flashOn :: Code
+flashOn = code "5"
+
+inverseOn :: Code
+inverseOn = code "7"
+
+resetOn :: Code
+resetOn = code "0"
+
+printReset :: Colors -> TB.Builder
+printReset c = case c of
+  Colors0 -> mempty
+  _ -> unCode resetOn
+
+--
+-- Color basement
+--
+color8_f_default :: Foreground8
+color8_f_default = Foreground8 . Code $ mempty
+
+color8_f_black :: Foreground8
+color8_f_black = Foreground8 . code $ "30"
+
+color8_f_red :: Foreground8
+color8_f_red = Foreground8 . code $ "31"
+
+color8_f_green :: Foreground8
+color8_f_green = Foreground8 . code $ "32"
+
+color8_f_yellow :: Foreground8
+color8_f_yellow = Foreground8 . code $ "33"
+
+color8_f_blue :: Foreground8
+color8_f_blue = Foreground8 . code $ "34"
+
+color8_f_magenta :: Foreground8
+color8_f_magenta = Foreground8 . code $ "35"
+
+color8_f_cyan :: Foreground8
+color8_f_cyan = Foreground8 . code $ "36"
+
+color8_f_white :: Foreground8
+color8_f_white = Foreground8 . code $ "37"
+
+color8_b_default :: Background8
+color8_b_default = Background8 . Code $ mempty
+
+color8_b_black :: Background8
+color8_b_black = Background8 . code $ "40"
+
+color8_b_red :: Background8
+color8_b_red = Background8 . code $ "41"
+
+color8_b_green :: Background8
+color8_b_green = Background8 . code $ "42"
+
+color8_b_yellow :: Background8
+color8_b_yellow = Background8 . code $ "43"
+
+color8_b_blue :: Background8
+color8_b_blue = Background8 . code $ "44"
+
+color8_b_magenta :: Background8
+color8_b_magenta = Background8 . code $ "45"
+
+color8_b_cyan :: Background8
+color8_b_cyan = Background8 . code $ "46"
+
+color8_b_white :: Background8
+color8_b_white = Background8 . code $ "47"
+
+color256_f_default :: Foreground256
+color256_f_default = Foreground256 . Code $ mempty
+
+color256_f_0 :: Foreground256
+color256_f_0 = Foreground256 . code $ "38;5;0"
+
+color256_f_1 :: Foreground256
+color256_f_1 = Foreground256 . code $ "38;5;1"
+
+color256_f_2 :: Foreground256
+color256_f_2 = Foreground256 . code $ "38;5;2"
+
+color256_f_3 :: Foreground256
+color256_f_3 = Foreground256 . code $ "38;5;3"
+
+color256_f_4 :: Foreground256
+color256_f_4 = Foreground256 . code $ "38;5;4"
+
+color256_f_5 :: Foreground256
+color256_f_5 = Foreground256 . code $ "38;5;5"
+
+color256_f_6 :: Foreground256
+color256_f_6 = Foreground256 . code $ "38;5;6"
+
+color256_f_7 :: Foreground256
+color256_f_7 = Foreground256 . code $ "38;5;7"
+
+color256_f_8 :: Foreground256
+color256_f_8 = Foreground256 . code $ "38;5;8"
+
+color256_f_9 :: Foreground256
+color256_f_9 = Foreground256 . code $ "38;5;9"
+
+color256_f_10 :: Foreground256
+color256_f_10 = Foreground256 . code $ "38;5;10"
+
+color256_f_11 :: Foreground256
+color256_f_11 = Foreground256 . code $ "38;5;11"
+
+color256_f_12 :: Foreground256
+color256_f_12 = Foreground256 . code $ "38;5;12"
+
+color256_f_13 :: Foreground256
+color256_f_13 = Foreground256 . code $ "38;5;13"
+
+color256_f_14 :: Foreground256
+color256_f_14 = Foreground256 . code $ "38;5;14"
+
+color256_f_15 :: Foreground256
+color256_f_15 = Foreground256 . code $ "38;5;15"
+
+color256_f_16 :: Foreground256
+color256_f_16 = Foreground256 . code $ "38;5;16"
+
+color256_f_17 :: Foreground256
+color256_f_17 = Foreground256 . code $ "38;5;17"
+
+color256_f_18 :: Foreground256
+color256_f_18 = Foreground256 . code $ "38;5;18"
+
+color256_f_19 :: Foreground256
+color256_f_19 = Foreground256 . code $ "38;5;19"
+
+color256_f_20 :: Foreground256
+color256_f_20 = Foreground256 . code $ "38;5;20"
+
+color256_f_21 :: Foreground256
+color256_f_21 = Foreground256 . code $ "38;5;21"
+
+color256_f_22 :: Foreground256
+color256_f_22 = Foreground256 . code $ "38;5;22"
+
+color256_f_23 :: Foreground256
+color256_f_23 = Foreground256 . code $ "38;5;23"
+
+color256_f_24 :: Foreground256
+color256_f_24 = Foreground256 . code $ "38;5;24"
+
+color256_f_25 :: Foreground256
+color256_f_25 = Foreground256 . code $ "38;5;25"
+
+color256_f_26 :: Foreground256
+color256_f_26 = Foreground256 . code $ "38;5;26"
+
+color256_f_27 :: Foreground256
+color256_f_27 = Foreground256 . code $ "38;5;27"
+
+color256_f_28 :: Foreground256
+color256_f_28 = Foreground256 . code $ "38;5;28"
+
+color256_f_29 :: Foreground256
+color256_f_29 = Foreground256 . code $ "38;5;29"
+
+color256_f_30 :: Foreground256
+color256_f_30 = Foreground256 . code $ "38;5;30"
+
+color256_f_31 :: Foreground256
+color256_f_31 = Foreground256 . code $ "38;5;31"
+
+color256_f_32 :: Foreground256
+color256_f_32 = Foreground256 . code $ "38;5;32"
+
+color256_f_33 :: Foreground256
+color256_f_33 = Foreground256 . code $ "38;5;33"
+
+color256_f_34 :: Foreground256
+color256_f_34 = Foreground256 . code $ "38;5;34"
+
+color256_f_35 :: Foreground256
+color256_f_35 = Foreground256 . code $ "38;5;35"
+
+color256_f_36 :: Foreground256
+color256_f_36 = Foreground256 . code $ "38;5;36"
+
+color256_f_37 :: Foreground256
+color256_f_37 = Foreground256 . code $ "38;5;37"
+
+color256_f_38 :: Foreground256
+color256_f_38 = Foreground256 . code $ "38;5;38"
+
+color256_f_39 :: Foreground256
+color256_f_39 = Foreground256 . code $ "38;5;39"
+
+color256_f_40 :: Foreground256
+color256_f_40 = Foreground256 . code $ "38;5;40"
+
+color256_f_41 :: Foreground256
+color256_f_41 = Foreground256 . code $ "38;5;41"
+
+color256_f_42 :: Foreground256
+color256_f_42 = Foreground256 . code $ "38;5;42"
+
+color256_f_43 :: Foreground256
+color256_f_43 = Foreground256 . code $ "38;5;43"
+
+color256_f_44 :: Foreground256
+color256_f_44 = Foreground256 . code $ "38;5;44"
+
+color256_f_45 :: Foreground256
+color256_f_45 = Foreground256 . code $ "38;5;45"
+
+color256_f_46 :: Foreground256
+color256_f_46 = Foreground256 . code $ "38;5;46"
+
+color256_f_47 :: Foreground256
+color256_f_47 = Foreground256 . code $ "38;5;47"
+
+color256_f_48 :: Foreground256
+color256_f_48 = Foreground256 . code $ "38;5;48"
+
+color256_f_49 :: Foreground256
+color256_f_49 = Foreground256 . code $ "38;5;49"
+
+color256_f_50 :: Foreground256
+color256_f_50 = Foreground256 . code $ "38;5;50"
+
+color256_f_51 :: Foreground256
+color256_f_51 = Foreground256 . code $ "38;5;51"
+
+color256_f_52 :: Foreground256
+color256_f_52 = Foreground256 . code $ "38;5;52"
+
+color256_f_53 :: Foreground256
+color256_f_53 = Foreground256 . code $ "38;5;53"
+
+color256_f_54 :: Foreground256
+color256_f_54 = Foreground256 . code $ "38;5;54"
+
+color256_f_55 :: Foreground256
+color256_f_55 = Foreground256 . code $ "38;5;55"
+
+color256_f_56 :: Foreground256
+color256_f_56 = Foreground256 . code $ "38;5;56"
+
+color256_f_57 :: Foreground256
+color256_f_57 = Foreground256 . code $ "38;5;57"
+
+color256_f_58 :: Foreground256
+color256_f_58 = Foreground256 . code $ "38;5;58"
+
+color256_f_59 :: Foreground256
+color256_f_59 = Foreground256 . code $ "38;5;59"
+
+color256_f_60 :: Foreground256
+color256_f_60 = Foreground256 . code $ "38;5;60"
+
+color256_f_61 :: Foreground256
+color256_f_61 = Foreground256 . code $ "38;5;61"
+
+color256_f_62 :: Foreground256
+color256_f_62 = Foreground256 . code $ "38;5;62"
+
+color256_f_63 :: Foreground256
+color256_f_63 = Foreground256 . code $ "38;5;63"
+
+color256_f_64 :: Foreground256
+color256_f_64 = Foreground256 . code $ "38;5;64"
+
+color256_f_65 :: Foreground256
+color256_f_65 = Foreground256 . code $ "38;5;65"
+
+color256_f_66 :: Foreground256
+color256_f_66 = Foreground256 . code $ "38;5;66"
+
+color256_f_67 :: Foreground256
+color256_f_67 = Foreground256 . code $ "38;5;67"
+
+color256_f_68 :: Foreground256
+color256_f_68 = Foreground256 . code $ "38;5;68"
+
+color256_f_69 :: Foreground256
+color256_f_69 = Foreground256 . code $ "38;5;69"
+
+color256_f_70 :: Foreground256
+color256_f_70 = Foreground256 . code $ "38;5;70"
+
+color256_f_71 :: Foreground256
+color256_f_71 = Foreground256 . code $ "38;5;71"
+
+color256_f_72 :: Foreground256
+color256_f_72 = Foreground256 . code $ "38;5;72"
+
+color256_f_73 :: Foreground256
+color256_f_73 = Foreground256 . code $ "38;5;73"
+
+color256_f_74 :: Foreground256
+color256_f_74 = Foreground256 . code $ "38;5;74"
+
+color256_f_75 :: Foreground256
+color256_f_75 = Foreground256 . code $ "38;5;75"
+
+color256_f_76 :: Foreground256
+color256_f_76 = Foreground256 . code $ "38;5;76"
+
+color256_f_77 :: Foreground256
+color256_f_77 = Foreground256 . code $ "38;5;77"
+
+color256_f_78 :: Foreground256
+color256_f_78 = Foreground256 . code $ "38;5;78"
+
+color256_f_79 :: Foreground256
+color256_f_79 = Foreground256 . code $ "38;5;79"
+
+color256_f_80 :: Foreground256
+color256_f_80 = Foreground256 . code $ "38;5;80"
+
+color256_f_81 :: Foreground256
+color256_f_81 = Foreground256 . code $ "38;5;81"
+
+color256_f_82 :: Foreground256
+color256_f_82 = Foreground256 . code $ "38;5;82"
+
+color256_f_83 :: Foreground256
+color256_f_83 = Foreground256 . code $ "38;5;83"
+
+color256_f_84 :: Foreground256
+color256_f_84 = Foreground256 . code $ "38;5;84"
+
+color256_f_85 :: Foreground256
+color256_f_85 = Foreground256 . code $ "38;5;85"
+
+color256_f_86 :: Foreground256
+color256_f_86 = Foreground256 . code $ "38;5;86"
+
+color256_f_87 :: Foreground256
+color256_f_87 = Foreground256 . code $ "38;5;87"
+
+color256_f_88 :: Foreground256
+color256_f_88 = Foreground256 . code $ "38;5;88"
+
+color256_f_89 :: Foreground256
+color256_f_89 = Foreground256 . code $ "38;5;89"
+
+color256_f_90 :: Foreground256
+color256_f_90 = Foreground256 . code $ "38;5;90"
+
+color256_f_91 :: Foreground256
+color256_f_91 = Foreground256 . code $ "38;5;91"
+
+color256_f_92 :: Foreground256
+color256_f_92 = Foreground256 . code $ "38;5;92"
+
+color256_f_93 :: Foreground256
+color256_f_93 = Foreground256 . code $ "38;5;93"
+
+color256_f_94 :: Foreground256
+color256_f_94 = Foreground256 . code $ "38;5;94"
+
+color256_f_95 :: Foreground256
+color256_f_95 = Foreground256 . code $ "38;5;95"
+
+color256_f_96 :: Foreground256
+color256_f_96 = Foreground256 . code $ "38;5;96"
+
+color256_f_97 :: Foreground256
+color256_f_97 = Foreground256 . code $ "38;5;97"
+
+color256_f_98 :: Foreground256
+color256_f_98 = Foreground256 . code $ "38;5;98"
+
+color256_f_99 :: Foreground256
+color256_f_99 = Foreground256 . code $ "38;5;99"
+
+color256_f_100 :: Foreground256
+color256_f_100 = Foreground256 . code $ "38;5;100"
+
+color256_f_101 :: Foreground256
+color256_f_101 = Foreground256 . code $ "38;5;101"
+
+color256_f_102 :: Foreground256
+color256_f_102 = Foreground256 . code $ "38;5;102"
+
+color256_f_103 :: Foreground256
+color256_f_103 = Foreground256 . code $ "38;5;103"
+
+color256_f_104 :: Foreground256
+color256_f_104 = Foreground256 . code $ "38;5;104"
+
+color256_f_105 :: Foreground256
+color256_f_105 = Foreground256 . code $ "38;5;105"
+
+color256_f_106 :: Foreground256
+color256_f_106 = Foreground256 . code $ "38;5;106"
+
+color256_f_107 :: Foreground256
+color256_f_107 = Foreground256 . code $ "38;5;107"
+
+color256_f_108 :: Foreground256
+color256_f_108 = Foreground256 . code $ "38;5;108"
+
+color256_f_109 :: Foreground256
+color256_f_109 = Foreground256 . code $ "38;5;109"
+
+color256_f_110 :: Foreground256
+color256_f_110 = Foreground256 . code $ "38;5;110"
+
+color256_f_111 :: Foreground256
+color256_f_111 = Foreground256 . code $ "38;5;111"
+
+color256_f_112 :: Foreground256
+color256_f_112 = Foreground256 . code $ "38;5;112"
+
+color256_f_113 :: Foreground256
+color256_f_113 = Foreground256 . code $ "38;5;113"
+
+color256_f_114 :: Foreground256
+color256_f_114 = Foreground256 . code $ "38;5;114"
+
+color256_f_115 :: Foreground256
+color256_f_115 = Foreground256 . code $ "38;5;115"
+
+color256_f_116 :: Foreground256
+color256_f_116 = Foreground256 . code $ "38;5;116"
+
+color256_f_117 :: Foreground256
+color256_f_117 = Foreground256 . code $ "38;5;117"
+
+color256_f_118 :: Foreground256
+color256_f_118 = Foreground256 . code $ "38;5;118"
+
+color256_f_119 :: Foreground256
+color256_f_119 = Foreground256 . code $ "38;5;119"
+
+color256_f_120 :: Foreground256
+color256_f_120 = Foreground256 . code $ "38;5;120"
+
+color256_f_121 :: Foreground256
+color256_f_121 = Foreground256 . code $ "38;5;121"
+
+color256_f_122 :: Foreground256
+color256_f_122 = Foreground256 . code $ "38;5;122"
+
+color256_f_123 :: Foreground256
+color256_f_123 = Foreground256 . code $ "38;5;123"
+
+color256_f_124 :: Foreground256
+color256_f_124 = Foreground256 . code $ "38;5;124"
+
+color256_f_125 :: Foreground256
+color256_f_125 = Foreground256 . code $ "38;5;125"
+
+color256_f_126 :: Foreground256
+color256_f_126 = Foreground256 . code $ "38;5;126"
+
+color256_f_127 :: Foreground256
+color256_f_127 = Foreground256 . code $ "38;5;127"
+
+color256_f_128 :: Foreground256
+color256_f_128 = Foreground256 . code $ "38;5;128"
+
+color256_f_129 :: Foreground256
+color256_f_129 = Foreground256 . code $ "38;5;129"
+
+color256_f_130 :: Foreground256
+color256_f_130 = Foreground256 . code $ "38;5;130"
+
+color256_f_131 :: Foreground256
+color256_f_131 = Foreground256 . code $ "38;5;131"
+
+color256_f_132 :: Foreground256
+color256_f_132 = Foreground256 . code $ "38;5;132"
+
+color256_f_133 :: Foreground256
+color256_f_133 = Foreground256 . code $ "38;5;133"
+
+color256_f_134 :: Foreground256
+color256_f_134 = Foreground256 . code $ "38;5;134"
+
+color256_f_135 :: Foreground256
+color256_f_135 = Foreground256 . code $ "38;5;135"
+
+color256_f_136 :: Foreground256
+color256_f_136 = Foreground256 . code $ "38;5;136"
+
+color256_f_137 :: Foreground256
+color256_f_137 = Foreground256 . code $ "38;5;137"
+
+color256_f_138 :: Foreground256
+color256_f_138 = Foreground256 . code $ "38;5;138"
+
+color256_f_139 :: Foreground256
+color256_f_139 = Foreground256 . code $ "38;5;139"
+
+color256_f_140 :: Foreground256
+color256_f_140 = Foreground256 . code $ "38;5;140"
+
+color256_f_141 :: Foreground256
+color256_f_141 = Foreground256 . code $ "38;5;141"
+
+color256_f_142 :: Foreground256
+color256_f_142 = Foreground256 . code $ "38;5;142"
+
+color256_f_143 :: Foreground256
+color256_f_143 = Foreground256 . code $ "38;5;143"
+
+color256_f_144 :: Foreground256
+color256_f_144 = Foreground256 . code $ "38;5;144"
+
+color256_f_145 :: Foreground256
+color256_f_145 = Foreground256 . code $ "38;5;145"
+
+color256_f_146 :: Foreground256
+color256_f_146 = Foreground256 . code $ "38;5;146"
+
+color256_f_147 :: Foreground256
+color256_f_147 = Foreground256 . code $ "38;5;147"
+
+color256_f_148 :: Foreground256
+color256_f_148 = Foreground256 . code $ "38;5;148"
+
+color256_f_149 :: Foreground256
+color256_f_149 = Foreground256 . code $ "38;5;149"
+
+color256_f_150 :: Foreground256
+color256_f_150 = Foreground256 . code $ "38;5;150"
+
+color256_f_151 :: Foreground256
+color256_f_151 = Foreground256 . code $ "38;5;151"
+
+color256_f_152 :: Foreground256
+color256_f_152 = Foreground256 . code $ "38;5;152"
+
+color256_f_153 :: Foreground256
+color256_f_153 = Foreground256 . code $ "38;5;153"
+
+color256_f_154 :: Foreground256
+color256_f_154 = Foreground256 . code $ "38;5;154"
+
+color256_f_155 :: Foreground256
+color256_f_155 = Foreground256 . code $ "38;5;155"
+
+color256_f_156 :: Foreground256
+color256_f_156 = Foreground256 . code $ "38;5;156"
+
+color256_f_157 :: Foreground256
+color256_f_157 = Foreground256 . code $ "38;5;157"
+
+color256_f_158 :: Foreground256
+color256_f_158 = Foreground256 . code $ "38;5;158"
+
+color256_f_159 :: Foreground256
+color256_f_159 = Foreground256 . code $ "38;5;159"
+
+color256_f_160 :: Foreground256
+color256_f_160 = Foreground256 . code $ "38;5;160"
+
+color256_f_161 :: Foreground256
+color256_f_161 = Foreground256 . code $ "38;5;161"
+
+color256_f_162 :: Foreground256
+color256_f_162 = Foreground256 . code $ "38;5;162"
+
+color256_f_163 :: Foreground256
+color256_f_163 = Foreground256 . code $ "38;5;163"
+
+color256_f_164 :: Foreground256
+color256_f_164 = Foreground256 . code $ "38;5;164"
+
+color256_f_165 :: Foreground256
+color256_f_165 = Foreground256 . code $ "38;5;165"
+
+color256_f_166 :: Foreground256
+color256_f_166 = Foreground256 . code $ "38;5;166"
+
+color256_f_167 :: Foreground256
+color256_f_167 = Foreground256 . code $ "38;5;167"
+
+color256_f_168 :: Foreground256
+color256_f_168 = Foreground256 . code $ "38;5;168"
+
+color256_f_169 :: Foreground256
+color256_f_169 = Foreground256 . code $ "38;5;169"
+
+color256_f_170 :: Foreground256
+color256_f_170 = Foreground256 . code $ "38;5;170"
+
+color256_f_171 :: Foreground256
+color256_f_171 = Foreground256 . code $ "38;5;171"
+
+color256_f_172 :: Foreground256
+color256_f_172 = Foreground256 . code $ "38;5;172"
+
+color256_f_173 :: Foreground256
+color256_f_173 = Foreground256 . code $ "38;5;173"
+
+color256_f_174 :: Foreground256
+color256_f_174 = Foreground256 . code $ "38;5;174"
+
+color256_f_175 :: Foreground256
+color256_f_175 = Foreground256 . code $ "38;5;175"
+
+color256_f_176 :: Foreground256
+color256_f_176 = Foreground256 . code $ "38;5;176"
+
+color256_f_177 :: Foreground256
+color256_f_177 = Foreground256 . code $ "38;5;177"
+
+color256_f_178 :: Foreground256
+color256_f_178 = Foreground256 . code $ "38;5;178"
+
+color256_f_179 :: Foreground256
+color256_f_179 = Foreground256 . code $ "38;5;179"
+
+color256_f_180 :: Foreground256
+color256_f_180 = Foreground256 . code $ "38;5;180"
+
+color256_f_181 :: Foreground256
+color256_f_181 = Foreground256 . code $ "38;5;181"
+
+color256_f_182 :: Foreground256
+color256_f_182 = Foreground256 . code $ "38;5;182"
+
+color256_f_183 :: Foreground256
+color256_f_183 = Foreground256 . code $ "38;5;183"
+
+color256_f_184 :: Foreground256
+color256_f_184 = Foreground256 . code $ "38;5;184"
+
+color256_f_185 :: Foreground256
+color256_f_185 = Foreground256 . code $ "38;5;185"
+
+color256_f_186 :: Foreground256
+color256_f_186 = Foreground256 . code $ "38;5;186"
+
+color256_f_187 :: Foreground256
+color256_f_187 = Foreground256 . code $ "38;5;187"
+
+color256_f_188 :: Foreground256
+color256_f_188 = Foreground256 . code $ "38;5;188"
+
+color256_f_189 :: Foreground256
+color256_f_189 = Foreground256 . code $ "38;5;189"
+
+color256_f_190 :: Foreground256
+color256_f_190 = Foreground256 . code $ "38;5;190"
+
+color256_f_191 :: Foreground256
+color256_f_191 = Foreground256 . code $ "38;5;191"
+
+color256_f_192 :: Foreground256
+color256_f_192 = Foreground256 . code $ "38;5;192"
+
+color256_f_193 :: Foreground256
+color256_f_193 = Foreground256 . code $ "38;5;193"
+
+color256_f_194 :: Foreground256
+color256_f_194 = Foreground256 . code $ "38;5;194"
+
+color256_f_195 :: Foreground256
+color256_f_195 = Foreground256 . code $ "38;5;195"
+
+color256_f_196 :: Foreground256
+color256_f_196 = Foreground256 . code $ "38;5;196"
+
+color256_f_197 :: Foreground256
+color256_f_197 = Foreground256 . code $ "38;5;197"
+
+color256_f_198 :: Foreground256
+color256_f_198 = Foreground256 . code $ "38;5;198"
+
+color256_f_199 :: Foreground256
+color256_f_199 = Foreground256 . code $ "38;5;199"
+
+color256_f_200 :: Foreground256
+color256_f_200 = Foreground256 . code $ "38;5;200"
+
+color256_f_201 :: Foreground256
+color256_f_201 = Foreground256 . code $ "38;5;201"
+
+color256_f_202 :: Foreground256
+color256_f_202 = Foreground256 . code $ "38;5;202"
+
+color256_f_203 :: Foreground256
+color256_f_203 = Foreground256 . code $ "38;5;203"
+
+color256_f_204 :: Foreground256
+color256_f_204 = Foreground256 . code $ "38;5;204"
+
+color256_f_205 :: Foreground256
+color256_f_205 = Foreground256 . code $ "38;5;205"
+
+color256_f_206 :: Foreground256
+color256_f_206 = Foreground256 . code $ "38;5;206"
+
+color256_f_207 :: Foreground256
+color256_f_207 = Foreground256 . code $ "38;5;207"
+
+color256_f_208 :: Foreground256
+color256_f_208 = Foreground256 . code $ "38;5;208"
+
+color256_f_209 :: Foreground256
+color256_f_209 = Foreground256 . code $ "38;5;209"
+
+color256_f_210 :: Foreground256
+color256_f_210 = Foreground256 . code $ "38;5;210"
+
+color256_f_211 :: Foreground256
+color256_f_211 = Foreground256 . code $ "38;5;211"
+
+color256_f_212 :: Foreground256
+color256_f_212 = Foreground256 . code $ "38;5;212"
+
+color256_f_213 :: Foreground256
+color256_f_213 = Foreground256 . code $ "38;5;213"
+
+color256_f_214 :: Foreground256
+color256_f_214 = Foreground256 . code $ "38;5;214"
+
+color256_f_215 :: Foreground256
+color256_f_215 = Foreground256 . code $ "38;5;215"
+
+color256_f_216 :: Foreground256
+color256_f_216 = Foreground256 . code $ "38;5;216"
+
+color256_f_217 :: Foreground256
+color256_f_217 = Foreground256 . code $ "38;5;217"
+
+color256_f_218 :: Foreground256
+color256_f_218 = Foreground256 . code $ "38;5;218"
+
+color256_f_219 :: Foreground256
+color256_f_219 = Foreground256 . code $ "38;5;219"
+
+color256_f_220 :: Foreground256
+color256_f_220 = Foreground256 . code $ "38;5;220"
+
+color256_f_221 :: Foreground256
+color256_f_221 = Foreground256 . code $ "38;5;221"
+
+color256_f_222 :: Foreground256
+color256_f_222 = Foreground256 . code $ "38;5;222"
+
+color256_f_223 :: Foreground256
+color256_f_223 = Foreground256 . code $ "38;5;223"
+
+color256_f_224 :: Foreground256
+color256_f_224 = Foreground256 . code $ "38;5;224"
+
+color256_f_225 :: Foreground256
+color256_f_225 = Foreground256 . code $ "38;5;225"
+
+color256_f_226 :: Foreground256
+color256_f_226 = Foreground256 . code $ "38;5;226"
+
+color256_f_227 :: Foreground256
+color256_f_227 = Foreground256 . code $ "38;5;227"
+
+color256_f_228 :: Foreground256
+color256_f_228 = Foreground256 . code $ "38;5;228"
+
+color256_f_229 :: Foreground256
+color256_f_229 = Foreground256 . code $ "38;5;229"
+
+color256_f_230 :: Foreground256
+color256_f_230 = Foreground256 . code $ "38;5;230"
+
+color256_f_231 :: Foreground256
+color256_f_231 = Foreground256 . code $ "38;5;231"
+
+color256_f_232 :: Foreground256
+color256_f_232 = Foreground256 . code $ "38;5;232"
+
+color256_f_233 :: Foreground256
+color256_f_233 = Foreground256 . code $ "38;5;233"
+
+color256_f_234 :: Foreground256
+color256_f_234 = Foreground256 . code $ "38;5;234"
+
+color256_f_235 :: Foreground256
+color256_f_235 = Foreground256 . code $ "38;5;235"
+
+color256_f_236 :: Foreground256
+color256_f_236 = Foreground256 . code $ "38;5;236"
+
+color256_f_237 :: Foreground256
+color256_f_237 = Foreground256 . code $ "38;5;237"
+
+color256_f_238 :: Foreground256
+color256_f_238 = Foreground256 . code $ "38;5;238"
+
+color256_f_239 :: Foreground256
+color256_f_239 = Foreground256 . code $ "38;5;239"
+
+color256_f_240 :: Foreground256
+color256_f_240 = Foreground256 . code $ "38;5;240"
+
+color256_f_241 :: Foreground256
+color256_f_241 = Foreground256 . code $ "38;5;241"
+
+color256_f_242 :: Foreground256
+color256_f_242 = Foreground256 . code $ "38;5;242"
+
+color256_f_243 :: Foreground256
+color256_f_243 = Foreground256 . code $ "38;5;243"
+
+color256_f_244 :: Foreground256
+color256_f_244 = Foreground256 . code $ "38;5;244"
+
+color256_f_245 :: Foreground256
+color256_f_245 = Foreground256 . code $ "38;5;245"
+
+color256_f_246 :: Foreground256
+color256_f_246 = Foreground256 . code $ "38;5;246"
+
+color256_f_247 :: Foreground256
+color256_f_247 = Foreground256 . code $ "38;5;247"
+
+color256_f_248 :: Foreground256
+color256_f_248 = Foreground256 . code $ "38;5;248"
+
+color256_f_249 :: Foreground256
+color256_f_249 = Foreground256 . code $ "38;5;249"
+
+color256_f_250 :: Foreground256
+color256_f_250 = Foreground256 . code $ "38;5;250"
+
+color256_f_251 :: Foreground256
+color256_f_251 = Foreground256 . code $ "38;5;251"
+
+color256_f_252 :: Foreground256
+color256_f_252 = Foreground256 . code $ "38;5;252"
+
+color256_f_253 :: Foreground256
+color256_f_253 = Foreground256 . code $ "38;5;253"
+
+color256_f_254 :: Foreground256
+color256_f_254 = Foreground256 . code $ "38;5;254"
+
+color256_f_255 :: Foreground256
+color256_f_255 = Foreground256 . code $ "38;5;255"
+
+color256_b_default :: Background256
+color256_b_default = Background256 . Code $ mempty
+
+color256_b_0 :: Background256
+color256_b_0 = Background256 . code $ "48;5;0"
+
+color256_b_1 :: Background256
+color256_b_1 = Background256 . code $ "48;5;1"
+
+color256_b_2 :: Background256
+color256_b_2 = Background256 . code $ "48;5;2"
+
+color256_b_3 :: Background256
+color256_b_3 = Background256 . code $ "48;5;3"
+
+color256_b_4 :: Background256
+color256_b_4 = Background256 . code $ "48;5;4"
+
+color256_b_5 :: Background256
+color256_b_5 = Background256 . code $ "48;5;5"
+
+color256_b_6 :: Background256
+color256_b_6 = Background256 . code $ "48;5;6"
+
+color256_b_7 :: Background256
+color256_b_7 = Background256 . code $ "48;5;7"
+
+color256_b_8 :: Background256
+color256_b_8 = Background256 . code $ "48;5;8"
+
+color256_b_9 :: Background256
+color256_b_9 = Background256 . code $ "48;5;9"
+
+color256_b_10 :: Background256
+color256_b_10 = Background256 . code $ "48;5;10"
+
+color256_b_11 :: Background256
+color256_b_11 = Background256 . code $ "48;5;11"
+
+color256_b_12 :: Background256
+color256_b_12 = Background256 . code $ "48;5;12"
+
+color256_b_13 :: Background256
+color256_b_13 = Background256 . code $ "48;5;13"
+
+color256_b_14 :: Background256
+color256_b_14 = Background256 . code $ "48;5;14"
+
+color256_b_15 :: Background256
+color256_b_15 = Background256 . code $ "48;5;15"
+
+color256_b_16 :: Background256
+color256_b_16 = Background256 . code $ "48;5;16"
+
+color256_b_17 :: Background256
+color256_b_17 = Background256 . code $ "48;5;17"
+
+color256_b_18 :: Background256
+color256_b_18 = Background256 . code $ "48;5;18"
+
+color256_b_19 :: Background256
+color256_b_19 = Background256 . code $ "48;5;19"
+
+color256_b_20 :: Background256
+color256_b_20 = Background256 . code $ "48;5;20"
+
+color256_b_21 :: Background256
+color256_b_21 = Background256 . code $ "48;5;21"
+
+color256_b_22 :: Background256
+color256_b_22 = Background256 . code $ "48;5;22"
+
+color256_b_23 :: Background256
+color256_b_23 = Background256 . code $ "48;5;23"
+
+color256_b_24 :: Background256
+color256_b_24 = Background256 . code $ "48;5;24"
+
+color256_b_25 :: Background256
+color256_b_25 = Background256 . code $ "48;5;25"
+
+color256_b_26 :: Background256
+color256_b_26 = Background256 . code $ "48;5;26"
+
+color256_b_27 :: Background256
+color256_b_27 = Background256 . code $ "48;5;27"
+
+color256_b_28 :: Background256
+color256_b_28 = Background256 . code $ "48;5;28"
+
+color256_b_29 :: Background256
+color256_b_29 = Background256 . code $ "48;5;29"
+
+color256_b_30 :: Background256
+color256_b_30 = Background256 . code $ "48;5;30"
+
+color256_b_31 :: Background256
+color256_b_31 = Background256 . code $ "48;5;31"
+
+color256_b_32 :: Background256
+color256_b_32 = Background256 . code $ "48;5;32"
+
+color256_b_33 :: Background256
+color256_b_33 = Background256 . code $ "48;5;33"
+
+color256_b_34 :: Background256
+color256_b_34 = Background256 . code $ "48;5;34"
+
+color256_b_35 :: Background256
+color256_b_35 = Background256 . code $ "48;5;35"
+
+color256_b_36 :: Background256
+color256_b_36 = Background256 . code $ "48;5;36"
+
+color256_b_37 :: Background256
+color256_b_37 = Background256 . code $ "48;5;37"
+
+color256_b_38 :: Background256
+color256_b_38 = Background256 . code $ "48;5;38"
+
+color256_b_39 :: Background256
+color256_b_39 = Background256 . code $ "48;5;39"
+
+color256_b_40 :: Background256
+color256_b_40 = Background256 . code $ "48;5;40"
+
+color256_b_41 :: Background256
+color256_b_41 = Background256 . code $ "48;5;41"
+
+color256_b_42 :: Background256
+color256_b_42 = Background256 . code $ "48;5;42"
+
+color256_b_43 :: Background256
+color256_b_43 = Background256 . code $ "48;5;43"
+
+color256_b_44 :: Background256
+color256_b_44 = Background256 . code $ "48;5;44"
+
+color256_b_45 :: Background256
+color256_b_45 = Background256 . code $ "48;5;45"
+
+color256_b_46 :: Background256
+color256_b_46 = Background256 . code $ "48;5;46"
+
+color256_b_47 :: Background256
+color256_b_47 = Background256 . code $ "48;5;47"
+
+color256_b_48 :: Background256
+color256_b_48 = Background256 . code $ "48;5;48"
+
+color256_b_49 :: Background256
+color256_b_49 = Background256 . code $ "48;5;49"
+
+color256_b_50 :: Background256
+color256_b_50 = Background256 . code $ "48;5;50"
+
+color256_b_51 :: Background256
+color256_b_51 = Background256 . code $ "48;5;51"
+
+color256_b_52 :: Background256
+color256_b_52 = Background256 . code $ "48;5;52"
+
+color256_b_53 :: Background256
+color256_b_53 = Background256 . code $ "48;5;53"
+
+color256_b_54 :: Background256
+color256_b_54 = Background256 . code $ "48;5;54"
+
+color256_b_55 :: Background256
+color256_b_55 = Background256 . code $ "48;5;55"
+
+color256_b_56 :: Background256
+color256_b_56 = Background256 . code $ "48;5;56"
+
+color256_b_57 :: Background256
+color256_b_57 = Background256 . code $ "48;5;57"
+
+color256_b_58 :: Background256
+color256_b_58 = Background256 . code $ "48;5;58"
+
+color256_b_59 :: Background256
+color256_b_59 = Background256 . code $ "48;5;59"
+
+color256_b_60 :: Background256
+color256_b_60 = Background256 . code $ "48;5;60"
+
+color256_b_61 :: Background256
+color256_b_61 = Background256 . code $ "48;5;61"
+
+color256_b_62 :: Background256
+color256_b_62 = Background256 . code $ "48;5;62"
+
+color256_b_63 :: Background256
+color256_b_63 = Background256 . code $ "48;5;63"
+
+color256_b_64 :: Background256
+color256_b_64 = Background256 . code $ "48;5;64"
+
+color256_b_65 :: Background256
+color256_b_65 = Background256 . code $ "48;5;65"
+
+color256_b_66 :: Background256
+color256_b_66 = Background256 . code $ "48;5;66"
+
+color256_b_67 :: Background256
+color256_b_67 = Background256 . code $ "48;5;67"
+
+color256_b_68 :: Background256
+color256_b_68 = Background256 . code $ "48;5;68"
+
+color256_b_69 :: Background256
+color256_b_69 = Background256 . code $ "48;5;69"
+
+color256_b_70 :: Background256
+color256_b_70 = Background256 . code $ "48;5;70"
+
+color256_b_71 :: Background256
+color256_b_71 = Background256 . code $ "48;5;71"
+
+color256_b_72 :: Background256
+color256_b_72 = Background256 . code $ "48;5;72"
+
+color256_b_73 :: Background256
+color256_b_73 = Background256 . code $ "48;5;73"
+
+color256_b_74 :: Background256
+color256_b_74 = Background256 . code $ "48;5;74"
+
+color256_b_75 :: Background256
+color256_b_75 = Background256 . code $ "48;5;75"
+
+color256_b_76 :: Background256
+color256_b_76 = Background256 . code $ "48;5;76"
+
+color256_b_77 :: Background256
+color256_b_77 = Background256 . code $ "48;5;77"
+
+color256_b_78 :: Background256
+color256_b_78 = Background256 . code $ "48;5;78"
+
+color256_b_79 :: Background256
+color256_b_79 = Background256 . code $ "48;5;79"
+
+color256_b_80 :: Background256
+color256_b_80 = Background256 . code $ "48;5;80"
+
+color256_b_81 :: Background256
+color256_b_81 = Background256 . code $ "48;5;81"
+
+color256_b_82 :: Background256
+color256_b_82 = Background256 . code $ "48;5;82"
+
+color256_b_83 :: Background256
+color256_b_83 = Background256 . code $ "48;5;83"
+
+color256_b_84 :: Background256
+color256_b_84 = Background256 . code $ "48;5;84"
+
+color256_b_85 :: Background256
+color256_b_85 = Background256 . code $ "48;5;85"
+
+color256_b_86 :: Background256
+color256_b_86 = Background256 . code $ "48;5;86"
+
+color256_b_87 :: Background256
+color256_b_87 = Background256 . code $ "48;5;87"
+
+color256_b_88 :: Background256
+color256_b_88 = Background256 . code $ "48;5;88"
+
+color256_b_89 :: Background256
+color256_b_89 = Background256 . code $ "48;5;89"
+
+color256_b_90 :: Background256
+color256_b_90 = Background256 . code $ "48;5;90"
+
+color256_b_91 :: Background256
+color256_b_91 = Background256 . code $ "48;5;91"
+
+color256_b_92 :: Background256
+color256_b_92 = Background256 . code $ "48;5;92"
+
+color256_b_93 :: Background256
+color256_b_93 = Background256 . code $ "48;5;93"
+
+color256_b_94 :: Background256
+color256_b_94 = Background256 . code $ "48;5;94"
+
+color256_b_95 :: Background256
+color256_b_95 = Background256 . code $ "48;5;95"
+
+color256_b_96 :: Background256
+color256_b_96 = Background256 . code $ "48;5;96"
+
+color256_b_97 :: Background256
+color256_b_97 = Background256 . code $ "48;5;97"
+
+color256_b_98 :: Background256
+color256_b_98 = Background256 . code $ "48;5;98"
+
+color256_b_99 :: Background256
+color256_b_99 = Background256 . code $ "48;5;99"
+
+color256_b_100 :: Background256
+color256_b_100 = Background256 . code $ "48;5;100"
+
+color256_b_101 :: Background256
+color256_b_101 = Background256 . code $ "48;5;101"
+
+color256_b_102 :: Background256
+color256_b_102 = Background256 . code $ "48;5;102"
+
+color256_b_103 :: Background256
+color256_b_103 = Background256 . code $ "48;5;103"
+
+color256_b_104 :: Background256
+color256_b_104 = Background256 . code $ "48;5;104"
+
+color256_b_105 :: Background256
+color256_b_105 = Background256 . code $ "48;5;105"
+
+color256_b_106 :: Background256
+color256_b_106 = Background256 . code $ "48;5;106"
+
+color256_b_107 :: Background256
+color256_b_107 = Background256 . code $ "48;5;107"
+
+color256_b_108 :: Background256
+color256_b_108 = Background256 . code $ "48;5;108"
+
+color256_b_109 :: Background256
+color256_b_109 = Background256 . code $ "48;5;109"
+
+color256_b_110 :: Background256
+color256_b_110 = Background256 . code $ "48;5;110"
+
+color256_b_111 :: Background256
+color256_b_111 = Background256 . code $ "48;5;111"
+
+color256_b_112 :: Background256
+color256_b_112 = Background256 . code $ "48;5;112"
+
+color256_b_113 :: Background256
+color256_b_113 = Background256 . code $ "48;5;113"
+
+color256_b_114 :: Background256
+color256_b_114 = Background256 . code $ "48;5;114"
+
+color256_b_115 :: Background256
+color256_b_115 = Background256 . code $ "48;5;115"
+
+color256_b_116 :: Background256
+color256_b_116 = Background256 . code $ "48;5;116"
+
+color256_b_117 :: Background256
+color256_b_117 = Background256 . code $ "48;5;117"
+
+color256_b_118 :: Background256
+color256_b_118 = Background256 . code $ "48;5;118"
+
+color256_b_119 :: Background256
+color256_b_119 = Background256 . code $ "48;5;119"
+
+color256_b_120 :: Background256
+color256_b_120 = Background256 . code $ "48;5;120"
+
+color256_b_121 :: Background256
+color256_b_121 = Background256 . code $ "48;5;121"
+
+color256_b_122 :: Background256
+color256_b_122 = Background256 . code $ "48;5;122"
+
+color256_b_123 :: Background256
+color256_b_123 = Background256 . code $ "48;5;123"
+
+color256_b_124 :: Background256
+color256_b_124 = Background256 . code $ "48;5;124"
+
+color256_b_125 :: Background256
+color256_b_125 = Background256 . code $ "48;5;125"
+
+color256_b_126 :: Background256
+color256_b_126 = Background256 . code $ "48;5;126"
+
+color256_b_127 :: Background256
+color256_b_127 = Background256 . code $ "48;5;127"
+
+color256_b_128 :: Background256
+color256_b_128 = Background256 . code $ "48;5;128"
+
+color256_b_129 :: Background256
+color256_b_129 = Background256 . code $ "48;5;129"
+
+color256_b_130 :: Background256
+color256_b_130 = Background256 . code $ "48;5;130"
+
+color256_b_131 :: Background256
+color256_b_131 = Background256 . code $ "48;5;131"
+
+color256_b_132 :: Background256
+color256_b_132 = Background256 . code $ "48;5;132"
+
+color256_b_133 :: Background256
+color256_b_133 = Background256 . code $ "48;5;133"
+
+color256_b_134 :: Background256
+color256_b_134 = Background256 . code $ "48;5;134"
+
+color256_b_135 :: Background256
+color256_b_135 = Background256 . code $ "48;5;135"
+
+color256_b_136 :: Background256
+color256_b_136 = Background256 . code $ "48;5;136"
+
+color256_b_137 :: Background256
+color256_b_137 = Background256 . code $ "48;5;137"
+
+color256_b_138 :: Background256
+color256_b_138 = Background256 . code $ "48;5;138"
+
+color256_b_139 :: Background256
+color256_b_139 = Background256 . code $ "48;5;139"
+
+color256_b_140 :: Background256
+color256_b_140 = Background256 . code $ "48;5;140"
+
+color256_b_141 :: Background256
+color256_b_141 = Background256 . code $ "48;5;141"
+
+color256_b_142 :: Background256
+color256_b_142 = Background256 . code $ "48;5;142"
+
+color256_b_143 :: Background256
+color256_b_143 = Background256 . code $ "48;5;143"
+
+color256_b_144 :: Background256
+color256_b_144 = Background256 . code $ "48;5;144"
+
+color256_b_145 :: Background256
+color256_b_145 = Background256 . code $ "48;5;145"
+
+color256_b_146 :: Background256
+color256_b_146 = Background256 . code $ "48;5;146"
+
+color256_b_147 :: Background256
+color256_b_147 = Background256 . code $ "48;5;147"
+
+color256_b_148 :: Background256
+color256_b_148 = Background256 . code $ "48;5;148"
+
+color256_b_149 :: Background256
+color256_b_149 = Background256 . code $ "48;5;149"
+
+color256_b_150 :: Background256
+color256_b_150 = Background256 . code $ "48;5;150"
+
+color256_b_151 :: Background256
+color256_b_151 = Background256 . code $ "48;5;151"
+
+color256_b_152 :: Background256
+color256_b_152 = Background256 . code $ "48;5;152"
+
+color256_b_153 :: Background256
+color256_b_153 = Background256 . code $ "48;5;153"
+
+color256_b_154 :: Background256
+color256_b_154 = Background256 . code $ "48;5;154"
+
+color256_b_155 :: Background256
+color256_b_155 = Background256 . code $ "48;5;155"
+
+color256_b_156 :: Background256
+color256_b_156 = Background256 . code $ "48;5;156"
+
+color256_b_157 :: Background256
+color256_b_157 = Background256 . code $ "48;5;157"
+
+color256_b_158 :: Background256
+color256_b_158 = Background256 . code $ "48;5;158"
+
+color256_b_159 :: Background256
+color256_b_159 = Background256 . code $ "48;5;159"
+
+color256_b_160 :: Background256
+color256_b_160 = Background256 . code $ "48;5;160"
+
+color256_b_161 :: Background256
+color256_b_161 = Background256 . code $ "48;5;161"
+
+color256_b_162 :: Background256
+color256_b_162 = Background256 . code $ "48;5;162"
+
+color256_b_163 :: Background256
+color256_b_163 = Background256 . code $ "48;5;163"
+
+color256_b_164 :: Background256
+color256_b_164 = Background256 . code $ "48;5;164"
+
+color256_b_165 :: Background256
+color256_b_165 = Background256 . code $ "48;5;165"
+
+color256_b_166 :: Background256
+color256_b_166 = Background256 . code $ "48;5;166"
+
+color256_b_167 :: Background256
+color256_b_167 = Background256 . code $ "48;5;167"
+
+color256_b_168 :: Background256
+color256_b_168 = Background256 . code $ "48;5;168"
+
+color256_b_169 :: Background256
+color256_b_169 = Background256 . code $ "48;5;169"
+
+color256_b_170 :: Background256
+color256_b_170 = Background256 . code $ "48;5;170"
+
+color256_b_171 :: Background256
+color256_b_171 = Background256 . code $ "48;5;171"
+
+color256_b_172 :: Background256
+color256_b_172 = Background256 . code $ "48;5;172"
+
+color256_b_173 :: Background256
+color256_b_173 = Background256 . code $ "48;5;173"
+
+color256_b_174 :: Background256
+color256_b_174 = Background256 . code $ "48;5;174"
+
+color256_b_175 :: Background256
+color256_b_175 = Background256 . code $ "48;5;175"
+
+color256_b_176 :: Background256
+color256_b_176 = Background256 . code $ "48;5;176"
+
+color256_b_177 :: Background256
+color256_b_177 = Background256 . code $ "48;5;177"
+
+color256_b_178 :: Background256
+color256_b_178 = Background256 . code $ "48;5;178"
+
+color256_b_179 :: Background256
+color256_b_179 = Background256 . code $ "48;5;179"
+
+color256_b_180 :: Background256
+color256_b_180 = Background256 . code $ "48;5;180"
+
+color256_b_181 :: Background256
+color256_b_181 = Background256 . code $ "48;5;181"
+
+color256_b_182 :: Background256
+color256_b_182 = Background256 . code $ "48;5;182"
+
+color256_b_183 :: Background256
+color256_b_183 = Background256 . code $ "48;5;183"
+
+color256_b_184 :: Background256
+color256_b_184 = Background256 . code $ "48;5;184"
+
+color256_b_185 :: Background256
+color256_b_185 = Background256 . code $ "48;5;185"
+
+color256_b_186 :: Background256
+color256_b_186 = Background256 . code $ "48;5;186"
+
+color256_b_187 :: Background256
+color256_b_187 = Background256 . code $ "48;5;187"
+
+color256_b_188 :: Background256
+color256_b_188 = Background256 . code $ "48;5;188"
+
+color256_b_189 :: Background256
+color256_b_189 = Background256 . code $ "48;5;189"
+
+color256_b_190 :: Background256
+color256_b_190 = Background256 . code $ "48;5;190"
+
+color256_b_191 :: Background256
+color256_b_191 = Background256 . code $ "48;5;191"
+
+color256_b_192 :: Background256
+color256_b_192 = Background256 . code $ "48;5;192"
+
+color256_b_193 :: Background256
+color256_b_193 = Background256 . code $ "48;5;193"
+
+color256_b_194 :: Background256
+color256_b_194 = Background256 . code $ "48;5;194"
+
+color256_b_195 :: Background256
+color256_b_195 = Background256 . code $ "48;5;195"
+
+color256_b_196 :: Background256
+color256_b_196 = Background256 . code $ "48;5;196"
+
+color256_b_197 :: Background256
+color256_b_197 = Background256 . code $ "48;5;197"
+
+color256_b_198 :: Background256
+color256_b_198 = Background256 . code $ "48;5;198"
+
+color256_b_199 :: Background256
+color256_b_199 = Background256 . code $ "48;5;199"
+
+color256_b_200 :: Background256
+color256_b_200 = Background256 . code $ "48;5;200"
+
+color256_b_201 :: Background256
+color256_b_201 = Background256 . code $ "48;5;201"
+
+color256_b_202 :: Background256
+color256_b_202 = Background256 . code $ "48;5;202"
+
+color256_b_203 :: Background256
+color256_b_203 = Background256 . code $ "48;5;203"
+
+color256_b_204 :: Background256
+color256_b_204 = Background256 . code $ "48;5;204"
+
+color256_b_205 :: Background256
+color256_b_205 = Background256 . code $ "48;5;205"
+
+color256_b_206 :: Background256
+color256_b_206 = Background256 . code $ "48;5;206"
+
+color256_b_207 :: Background256
+color256_b_207 = Background256 . code $ "48;5;207"
+
+color256_b_208 :: Background256
+color256_b_208 = Background256 . code $ "48;5;208"
+
+color256_b_209 :: Background256
+color256_b_209 = Background256 . code $ "48;5;209"
+
+color256_b_210 :: Background256
+color256_b_210 = Background256 . code $ "48;5;210"
+
+color256_b_211 :: Background256
+color256_b_211 = Background256 . code $ "48;5;211"
+
+color256_b_212 :: Background256
+color256_b_212 = Background256 . code $ "48;5;212"
+
+color256_b_213 :: Background256
+color256_b_213 = Background256 . code $ "48;5;213"
+
+color256_b_214 :: Background256
+color256_b_214 = Background256 . code $ "48;5;214"
+
+color256_b_215 :: Background256
+color256_b_215 = Background256 . code $ "48;5;215"
+
+color256_b_216 :: Background256
+color256_b_216 = Background256 . code $ "48;5;216"
+
+color256_b_217 :: Background256
+color256_b_217 = Background256 . code $ "48;5;217"
+
+color256_b_218 :: Background256
+color256_b_218 = Background256 . code $ "48;5;218"
+
+color256_b_219 :: Background256
+color256_b_219 = Background256 . code $ "48;5;219"
+
+color256_b_220 :: Background256
+color256_b_220 = Background256 . code $ "48;5;220"
+
+color256_b_221 :: Background256
+color256_b_221 = Background256 . code $ "48;5;221"
+
+color256_b_222 :: Background256
+color256_b_222 = Background256 . code $ "48;5;222"
+
+color256_b_223 :: Background256
+color256_b_223 = Background256 . code $ "48;5;223"
+
+color256_b_224 :: Background256
+color256_b_224 = Background256 . code $ "48;5;224"
+
+color256_b_225 :: Background256
+color256_b_225 = Background256 . code $ "48;5;225"
+
+color256_b_226 :: Background256
+color256_b_226 = Background256 . code $ "48;5;226"
+
+color256_b_227 :: Background256
+color256_b_227 = Background256 . code $ "48;5;227"
+
+color256_b_228 :: Background256
+color256_b_228 = Background256 . code $ "48;5;228"
+
+color256_b_229 :: Background256
+color256_b_229 = Background256 . code $ "48;5;229"
+
+color256_b_230 :: Background256
+color256_b_230 = Background256 . code $ "48;5;230"
+
+color256_b_231 :: Background256
+color256_b_231 = Background256 . code $ "48;5;231"
+
+color256_b_232 :: Background256
+color256_b_232 = Background256 . code $ "48;5;232"
+
+color256_b_233 :: Background256
+color256_b_233 = Background256 . code $ "48;5;233"
+
+color256_b_234 :: Background256
+color256_b_234 = Background256 . code $ "48;5;234"
+
+color256_b_235 :: Background256
+color256_b_235 = Background256 . code $ "48;5;235"
+
+color256_b_236 :: Background256
+color256_b_236 = Background256 . code $ "48;5;236"
+
+color256_b_237 :: Background256
+color256_b_237 = Background256 . code $ "48;5;237"
+
+color256_b_238 :: Background256
+color256_b_238 = Background256 . code $ "48;5;238"
+
+color256_b_239 :: Background256
+color256_b_239 = Background256 . code $ "48;5;239"
+
+color256_b_240 :: Background256
+color256_b_240 = Background256 . code $ "48;5;240"
+
+color256_b_241 :: Background256
+color256_b_241 = Background256 . code $ "48;5;241"
+
+color256_b_242 :: Background256
+color256_b_242 = Background256 . code $ "48;5;242"
+
+color256_b_243 :: Background256
+color256_b_243 = Background256 . code $ "48;5;243"
+
+color256_b_244 :: Background256
+color256_b_244 = Background256 . code $ "48;5;244"
+
+color256_b_245 :: Background256
+color256_b_245 = Background256 . code $ "48;5;245"
+
+color256_b_246 :: Background256
+color256_b_246 = Background256 . code $ "48;5;246"
+
+color256_b_247 :: Background256
+color256_b_247 = Background256 . code $ "48;5;247"
+
+color256_b_248 :: Background256
+color256_b_248 = Background256 . code $ "48;5;248"
+
+color256_b_249 :: Background256
+color256_b_249 = Background256 . code $ "48;5;249"
+
+color256_b_250 :: Background256
+color256_b_250 = Background256 . code $ "48;5;250"
+
+color256_b_251 :: Background256
+color256_b_251 = Background256 . code $ "48;5;251"
+
+color256_b_252 :: Background256
+color256_b_252 = Background256 . code $ "48;5;252"
+
+color256_b_253 :: Background256
+color256_b_253 = Background256 . code $ "48;5;253"
+
+color256_b_254 :: Background256
+color256_b_254 = Background256 . code $ "48;5;254"
+
+color256_b_255 :: Background256
+color256_b_255 = Background256 . code $ "48;5;255"
+
diff --git a/Penny/Cabin/Chunk/Switch.hs b/Penny/Cabin/Chunk/Switch.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Chunk/Switch.hs
@@ -0,0 +1,26 @@
+module Penny.Cabin.Chunk.Switch where
+
+import qualified Penny.Cabin.Chunk as C
+
+-- | Switch the foreground colors for new ones.
+switchForeground ::
+  C.Foreground8
+  -> C.Foreground256
+  -> C.TextSpec
+  -> C.TextSpec
+switchForeground c8 c256 ts = ts' where
+  ts' = C.TextSpec s8' s256'
+  s8' = (C.style8 ts) { C.foreground8 = c8 }
+  s256' = (C.style256 ts) { C.foreground256 = c256 }
+
+-- | Switch the background colors for new ones.
+switchBackground ::
+  C.Background8
+  -> C.Background256
+  -> C.TextSpec
+  -> C.TextSpec
+switchBackground c8 c256 ts = ts' where
+  ts' = C.TextSpec s8' s256'
+  s8' = (C.style8 ts) { C.background8 = c8 }
+  s256' = (C.style256 ts) { C.background256 = c256 }
+
diff --git a/Penny/Cabin/Colors.hs b/Penny/Cabin/Colors.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Colors.hs
@@ -0,0 +1,50 @@
+module Penny.Cabin.Colors where
+
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.Balance as Bal
+import qualified Penny.Lincoln.Bits as Bits
+import qualified Penny.Cabin.Chunk as C
+import qualified Penny.Cabin.Meta as M
+
+-- | The colors for debits and credits. These are used for the entry's
+-- amount and for the total amounts that accompany each entry. Here,
+-- @even@ and @odd@ refer to the the posting's visible number. @Zero@
+-- is for the balance entries whose balance is zero.
+data DrCrColors =
+  DrCrColors { evenDebit :: !C.TextSpec
+             , evenCredit :: !C.TextSpec
+             , oddDebit :: !C.TextSpec
+             , oddCredit :: !C.TextSpec
+             , evenZero :: !C.TextSpec
+             , oddZero :: !C.TextSpec }
+
+-- | The colors for all fields other than entries and totals.
+data BaseColors =
+  BaseColors { evenColors :: !C.TextSpec
+             , oddColors :: !C.TextSpec }
+
+-- | Change a BaseColors to a TextSpec.
+colors :: M.VisibleNum -> BaseColors -> C.TextSpec
+colors vn b = let n = L.forward . M.unVisibleNum $ vn in
+  if odd n then oddColors b else evenColors b
+
+-- | Change a DrCrColors to a BaseColors; you can then use 'colors' to
+-- change that to a TextSpec.
+drCrToBaseColors :: Bits.DrCr -> DrCrColors -> BaseColors
+drCrToBaseColors dc col = case dc of
+  Bits.Debit -> BaseColors (evenDebit col) (oddDebit col)
+  Bits.Credit -> BaseColors (evenCredit col) (oddCredit col)
+
+-- | Change a DrCrColors to a BaseColors, based on a balance (unlike
+-- entries, balances might be zero.)
+bottomLineToBaseColors :: DrCrColors -> Bal.BottomLine -> BaseColors
+bottomLineToBaseColors col no = case no of
+  Bal.Zero -> BaseColors (evenZero col) (oddZero col)
+  Bal.NonZero column -> drCrToBaseColors (Bal.drCr column) col
+  
+-- | TextSpec to use when showing the lack of a balance.
+noBalanceColors :: M.VisibleNum -> DrCrColors -> C.TextSpec
+noBalanceColors vn dc =
+  if odd . L.forward . M.unVisibleNum $ vn
+  then oddZero dc
+  else evenZero dc
diff --git a/Penny/Cabin/Colors/DarkBackground.hs b/Penny/Cabin/Colors/DarkBackground.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Colors/DarkBackground.hs
@@ -0,0 +1,38 @@
+-- | The default colors; the user may override.
+module Penny.Cabin.Colors.DarkBackground where
+
+import qualified Penny.Cabin.Colors as PC
+import qualified Penny.Cabin.Chunk as CC
+import qualified Penny.Cabin.Chunk.Switch as S
+
+baseColors :: PC.BaseColors
+baseColors = PC.BaseColors evenTextSpec oddTextSpec
+
+drCrColors :: PC.DrCrColors
+drCrColors = PC.DrCrColors { PC.evenDebit = debit evenTextSpec
+                           , PC.evenCredit = credit evenTextSpec
+                           , PC.oddDebit = debit oddTextSpec
+                           , PC.oddCredit = credit oddTextSpec
+                           , PC.evenZero = zero evenTextSpec
+                           , PC.oddZero = zero oddTextSpec }
+
+evenTextSpec :: CC.TextSpec
+evenTextSpec = CC.defaultTextSpec
+
+oddTextSpec :: CC.TextSpec
+oddTextSpec = S.switchBackground CC.color8_b_default
+              CC.color256_b_235 evenTextSpec
+              
+-- | Debits in 256 colors are orange; in 8 colors, magenta
+debit :: CC.TextSpec -> CC.TextSpec
+debit = S.switchForeground CC.color8_f_magenta CC.color256_f_208
+
+-- | Credits in 256 colors are cyan; in 8 colors, cyan
+credit :: CC.TextSpec -> CC.TextSpec
+credit = S.switchForeground CC.color8_f_cyan (CC.color256_f_45)
+
+-- | Zero values are white
+zero :: CC.TextSpec -> CC.TextSpec
+zero = S.switchForeground CC.color8_f_white (CC.color256_f_15)
+
+
diff --git a/Penny/Cabin/Colors/LightBackground.hs b/Penny/Cabin/Colors/LightBackground.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Colors/LightBackground.hs
@@ -0,0 +1,35 @@
+-- | The default colors; the user may override.
+module Penny.Cabin.Colors.LightBackground where
+
+import qualified Penny.Cabin.Colors as PC
+import qualified Penny.Cabin.Chunk as CC
+import qualified Penny.Cabin.Chunk.Switch as S
+
+baseColors :: PC.BaseColors
+baseColors = PC.BaseColors evenTextSpec oddTextSpec
+
+drCrColors :: PC.DrCrColors
+drCrColors = PC.DrCrColors { PC.evenDebit = debit evenTextSpec
+                           , PC.evenCredit = credit evenTextSpec
+                           , PC.oddDebit = debit oddTextSpec
+                           , PC.oddCredit = credit oddTextSpec
+                           , PC.evenZero = zero evenTextSpec
+                           , PC.oddZero = zero oddTextSpec }
+
+evenTextSpec :: CC.TextSpec
+evenTextSpec = CC.defaultTextSpec
+
+oddTextSpec :: CC.TextSpec
+oddTextSpec = S.switchBackground CC.color8_b_default
+              (CC.color256_b_247) evenTextSpec
+              
+debit :: CC.TextSpec -> CC.TextSpec
+debit = S.switchForeground CC.color8_f_magenta (CC.color256_f_13)
+
+credit :: CC.TextSpec -> CC.TextSpec
+credit = S.switchForeground CC.color8_f_cyan (CC.color256_f_14)
+
+zero :: CC.TextSpec -> CC.TextSpec
+zero = S.switchForeground CC.color8_f_white (CC.color256_f_15)
+
+
diff --git a/Penny/Cabin/Interface.hs b/Penny/Cabin/Interface.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Interface.hs
@@ -0,0 +1,55 @@
+-- | An interface for other Penny components to use. A report is
+-- anything that is a 'Report'.
+module Penny.Cabin.Interface where
+
+import Control.Monad.Exception.Synchronous (Exceptional)
+import qualified Data.Text as X
+import qualified Data.Text.Lazy as XL
+import Text.Matchers.Text (CaseSensitive)
+import System.Console.MultiArg.Prim (Parser)
+
+import qualified Penny.Lincoln as L
+import qualified Penny.Liberty as Ly
+import Penny.Shield (Runtime)
+
+type ReportFunc =
+  Runtime
+  -- ^ Information only known at runtime, such as the
+  -- environment. Does not include any information that is derived
+  -- from parsing the command line.
+
+  -> CaseSensitive
+  -- ^ Result from previous parses indicating whether the user desires
+  -- case sensitivity (this may have been changed in the filtering
+  -- options)
+  
+  -> (CaseSensitive -> X.Text -> Exceptional X.Text (X.Text -> Bool))
+  -- ^ Result from previous parsers indicating the matcher factory the
+  -- user wishes to use
+  
+  -> [L.Box Ly.LibertyMeta]
+  -- ^ Postings that will be included in the report
+  
+  -> [L.PricePoint]
+  -- ^ PricePoints to be included in the report
+  
+  
+  -> Exceptional X.Text XL.Text
+  -- ^ The exception type is a strict Text, containing the error
+  -- message. The success type is a lazy Text, containing the
+  -- resulting report.
+
+
+data Report =
+  Report { help :: X.Text
+           -- ^ A strict Text containing a help message.
+           
+         , name :: String
+           -- ^ The name of the report
+           
+         , parseReport :: Parser ReportFunc
+           -- ^ The parser must parse everything beginning with the
+           -- first word after the name of the report (the parser does
+           -- not parse the name of the report) up until, but not
+           -- including, the first non-option word.
+         }
diff --git a/Penny/Cabin/Meta.hs b/Penny/Cabin/Meta.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Meta.hs
@@ -0,0 +1,15 @@
+module Penny.Cabin.Meta (VisibleNum, unVisibleNum,
+                         visibleNums ) where
+
+import qualified Penny.Lincoln as L
+
+newtype VisibleNum = VisibleNum { unVisibleNum :: L.Serial }
+                     deriving (Eq, Show)
+
+visibleNums ::
+  (VisibleNum -> a -> b)
+  -> [L.Box a]
+  -> [L.Box b]
+visibleNums f = L.serialItems s' where
+  s' ser (L.Box m pf) = L.Box (f (VisibleNum ser) m) pf
+
diff --git a/Penny/Cabin/Options.hs b/Penny/Cabin/Options.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Options.hs
@@ -0,0 +1,33 @@
+-- | Options applicable to multiple Cabin reports.
+
+module Penny.Cabin.Options where
+
+import qualified Penny.Cabin.Chunk as C
+import qualified Penny.Shield as S
+
+-- | The user's color preference.
+data ColorPref = Pref0 | Pref8 | Pref256 | PrefAuto
+               deriving Show
+
+maxCapableColors :: S.Runtime -> C.Colors
+maxCapableColors r = case S.output r of
+  S.NotTTY -> C.Colors0
+  S.IsTTY ->
+    case lookup "TERM" (S.environment r) of
+      Nothing -> C.Colors8
+      (Just t) -> if t == "xterm-256color"
+                  then C.Colors256
+                  else C.Colors8
+
+autoColors :: ColorPref -> S.Runtime -> C.Colors
+autoColors c r = case c of
+  Pref0 -> C.Colors0
+  Pref8 -> C.Colors8
+  Pref256 -> C.Colors256
+  PrefAuto -> maxCapableColors r
+
+-- | Whether to show zero balances in reports.
+newtype ShowZeroBalances =
+  ShowZeroBalances { unShowZeroBalances :: Bool }
+  deriving (Show, Eq)
+
diff --git a/Penny/Cabin/Posts.hs b/Penny/Cabin/Posts.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts.hs
@@ -0,0 +1,146 @@
+-- | The Penny Postings report
+--
+-- The Postings report displays postings in a tabular format designed
+-- to be read by humans. Some terminology used in the Postings report:
+--
+-- [@row@] The smallest unit that spans from left to right. A row,
+-- however, might consist of more than one screen line. For example,
+-- the running balance is shown on the far right side of the Postings
+-- report. The running balance might consist of more than one
+-- commodity. Each commodity is displayed on its own screen
+-- line. However, all these lines put together are displayed in a
+-- single row.
+--
+-- [@column@] The smallest unit that spans from top to bottom.
+--
+-- [@tranche@] Each posting is displayed in several rows. The group of
+-- rows that is displayed for a single posting is called a tranche.
+--
+-- [@tranche row@] Each tranche has a particular number of rows
+-- (currently four); each of these rows is known as a tranche row.
+--
+-- [@field@] Corresponds to a particular element of the posting, such
+-- as whether it is a debit or credit or its payee. The user can
+-- select which fields to see.
+--
+-- [@allocation@] The width of the Payee and Account fields is
+-- variable. Generally their width will adjust to fill the entire
+-- width of the screen. The allocations of the Payee and Account
+-- fields determine how much of the remaining space each field will
+-- receive.
+--
+-- The Postings report is easily customized from the command line to
+-- show various fields. However, the order of the fields is not
+-- configurable without editing the source code (sorry).
+
+module Penny.Cabin.Posts (
+  -- * Defaults
+  defaultPostsReport
+  
+  -- * Custom report
+  , customPostsReport
+
+  -- * Options
+  , O.T(..)
+  , O.ReportWidth(..)
+  , O.ymd, O.qtyAsIs, O.balanceAsIs, O.defaultWidth
+  , O.columnsVarToWidth, O.defaultOptions, O.widthFromRuntime
+  , O.defaultFields
+  ) where
+
+import Control.Applicative ((<$>))
+import qualified Control.Monad.Exception.Synchronous as Ex
+import qualified Data.Text as X
+import qualified Data.Text.Lazy as XL
+import qualified Penny.Cabin.Chunk as Chk
+import qualified Penny.Cabin.Interface as I
+import qualified Penny.Cabin.Posts.Options as O
+import qualified Penny.Cabin.Posts.Meta as M
+import Penny.Cabin.Posts.Chunk (makeChunk)
+import Penny.Cabin.Posts.Parser (parseOptions)
+import Penny.Cabin.Posts.Help (help)
+import qualified Penny.Copper as C
+import qualified Penny.Lincoln as L
+import qualified Penny.Liberty as Ly
+import qualified Penny.Shield as S
+import System.Console.MultiArg.Prim (Parser)
+import Text.Matchers.Text (CaseSensitive)
+
+-- | When applied to a DefaultTimeZone and a RadGroup, returns a
+-- report with the default options.
+defaultPostsReport ::
+  C.DefaultTimeZone
+  -> C.RadGroup
+  -> I.Report
+defaultPostsReport dtz rg = makeReport f where
+  f = parseReportOpts (\rt -> O.defaultOptions dtz rg rt)
+
+-- | Generate a custom Posts report.
+customPostsReport ::
+  (S.Runtime -> O.T)
+  -- ^ Function that, when applied to a Runtime, returns the default
+  -- options for the posts report. The options will be overridden by
+  -- any options on the command line.
+
+  -> I.Report
+customPostsReport = makeReport . parseReportOpts
+
+-- | When passed a ParseReportOpts, makes a Report.
+makeReport ::
+  Parser I.ReportFunc
+  -> I.Report
+makeReport f =
+  I.Report { I.help = help
+           , I.name = "posts"
+           , I.parseReport = f }
+
+type Factory = CaseSensitive -> X.Text
+                 -> Ex.Exceptional X.Text (X.Text -> Bool)
+
+parseReportOpts ::
+  (S.Runtime -> O.T)
+  -> Parser (S.Runtime
+             -> CaseSensitive
+             -> Factory
+             -> [L.Box Ly.LibertyMeta]
+             -> a
+             -> Ex.Exceptional X.Text XL.Text)
+parseReportOpts frt = do
+  getOpts <- parseOptions
+  let f rt cs fty bs _ = do
+        let optsDefault = frt rt
+            optsInit = optsDefault { O.sensitive = cs
+                                   , O.factory = fty }
+        optsParsed <- case getOpts rt optsInit of
+          Ex.Exception e -> Ex.throw . X.pack . show $ e
+          Ex.Success g -> return g
+        makeReportTxt optsParsed bs
+  return f
+            
+
+-- | Using the options parsed from the command line, print out the
+-- report.
+makeReportTxt ::
+  O.T
+  -> [L.Box Ly.LibertyMeta]
+  -> Ex.Exceptional X.Text XL.Text
+makeReportTxt op bs = fmap mkText postMetaBoxes
+  where
+    e = X.pack "posts report: filter expression parse failure"
+    postMetaBoxes = Ex.fromMaybe e (filterAndAssignMeta op bs)
+    mkText = Chk.chunksToText (O.colorPref op) . makeChunk op
+    
+
+-- | Takes a list of Box with LibertyMeta and options as processed
+-- from the command line.  Filters the list of Box and processes the
+-- post filter, then assigns the post metadata. Fails if the
+-- expression fails to produce a result.
+filterAndAssignMeta ::
+  O.T
+  -> [L.Box Ly.LibertyMeta]
+  -> Maybe [L.Box M.PostMeta]
+filterAndAssignMeta op bs =
+  M.addMetadata (O.showZeroBalances op)
+  <$> M.filterBoxes (O.tokens op) (O.postFilter op) bs
+
+
diff --git a/Penny/Cabin/Posts/Allocate.hs b/Penny/Cabin/Posts/Allocate.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts/Allocate.hs
@@ -0,0 +1,65 @@
+module Penny.Cabin.Posts.Allocate (
+  Allocation,
+  allocation,
+  unAllocation,
+  allocate) where
+
+import qualified Control.Monad.Trans.State as St
+import qualified Data.Foldable as F
+import qualified Data.Traversable as T
+
+-- | Allocations are integers. The absolute value of the integer is
+-- used, so for practical purposes @-4@ is the same allocation as @4@.
+newtype Allocation = Allocation { unAllocation :: Int }
+                     deriving (Show, Eq, Ord)
+
+allocation :: Int -> Allocation
+allocation = Allocation
+
+-- | Properties of the allocated result:
+--
+-- * If the sum of the absolute values of the allocations is zero,
+-- then each of the elements in the result will also be zero.
+--
+-- * Otherwise, if any allocation is zero, then the corresponding
+-- amount in the result will also be zero. The sum of the results will
+-- equal the total requested.
+allocate ::
+  (Functor f, F.Foldable f, T.Traversable f)
+  => f Allocation
+  -> Int
+  -> f Int
+allocate m t = let
+  tot = F.sum . fmap (toDouble . abs . unAllocation) $ m
+  ratios = fmap ((/tot) . toDouble . abs . unAllocation) m
+  rounded = fmap (round . (* (toDouble t))) ratios
+  toDouble = fromIntegral :: Int -> Double
+  in if tot == 0
+     then fmap (const 0) m
+     else adjust rounded t
+
+adjust :: (Functor f, F.Foldable f, T.Traversable f)
+  => f Int
+  -> Int
+  -> f Int
+adjust ws w = let
+  wsInts = fmap fromIntegral ws
+  diff = (fromIntegral w) - F.sum wsInts in
+  if diff == 0
+  then ws
+  else let
+    ws' = St.evalState (T.mapM adjustMap ws) diff
+    in adjust ws' w
+
+-- | The state is the target number minus the current actual total.
+adjustMap :: Int -> St.State Int Int
+adjustMap w = if w == 0 then return 0 else do
+  diff <- St.get
+  case compare diff 0 of
+    EQ -> return w
+    GT -> do
+      St.put (pred diff)
+      return (succ w)
+    LT -> do
+      St.put (succ diff)
+      return (pred w)
diff --git a/Penny/Cabin/Posts/Allocated.hs b/Penny/Cabin/Posts/Allocated.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts/Allocated.hs
@@ -0,0 +1,284 @@
+-- | Calculates the allocated cells -- the Payee cell and the Account
+-- cell. Here is the logic for this process:
+--
+-- 1. If neither Payee nor Account appears, do nothing.
+--
+-- 2. Obtain the width of the growing cells, including the
+-- spacers. One of the spacers attached to a field might be omitted:
+--
+-- a. If the rightmost growing field is TotalQty, include all spacers.
+--
+-- b. If the rightmost growing field is to the left of Payee, include
+-- all spacers.
+--
+-- c. If the rightmost growing field is to the right of Account but is
+-- not TotalQty, omit its spacer.
+--
+-- 2. Subtract from this sum the width of the Payee and Account
+-- spacers:
+--
+-- a. Subtract the width of Payee spacer if it appears.
+--
+-- b. Subtract the width of the Account spacer if it appears.
+--
+-- 3. If the remaining width is 0 or less, do nothing. Return, but
+-- indicate in return value that neither Payee nor Account is showing.
+--
+-- 4. Allocate the remaining width. If only Payee or Account appears,
+-- it gets all the width; otherwise, allocate the widths. No special
+-- arrangements are made if either field gets an allocation of 0.
+--
+-- 5. Fill cell contents. Return filled cells.
+module Penny.Cabin.Posts.Allocated (payeeAndAcct, Fields(..)) where
+
+import Control.Applicative(Applicative((<*>), pure), (<$>))
+import Data.Maybe (catMaybes, isJust)
+import Data.List (intersperse)
+import qualified Data.Foldable as Fdbl
+import qualified Data.Sequence as Seq
+import qualified Data.Traversable as T
+import qualified Data.Text as X
+import qualified Penny.Cabin.Chunk as C
+import qualified Penny.Cabin.Row as R
+import qualified Penny.Cabin.Posts.Allocate as A
+import qualified Penny.Cabin.Colors as PC
+import qualified Penny.Cabin.Posts.Fields as F
+import qualified Penny.Cabin.Posts.Growers as G
+import qualified Penny.Cabin.Posts.Meta as M
+import qualified Penny.Cabin.Posts.Options as Options
+import qualified Penny.Cabin.Posts.Options as O
+import qualified Penny.Cabin.Posts.Spacers as S
+import qualified Penny.Cabin.Posts.Spacers as Spacers
+import qualified Penny.Cabin.TextFormat as TF
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.Queries as Q
+import qualified Penny.Lincoln.HasText as HT
+
+type Box = L.Box M.PostMeta 
+
+data Fields a = Fields {
+  payee :: a
+  , account :: a
+  } deriving (Eq, Show)
+
+-- | Creates Payee and Account cells. The user must have requested the
+-- cells. In addition, no cells are created if there is not enough
+-- space for them in the report. Returns a Fields; each element of the
+-- Fields is Nothing if no cells were created (either because the user
+-- did not ask for them, or because there was no room) or Just cs i,
+-- where cs is a list of all the cells, and i is the width of all the
+-- cells.
+payeeAndAcct ::
+  G.Fields (Maybe Int)
+  -> Options.T
+  -> [Box]
+  -> Fields (Maybe ([R.ColumnSpec], Int))
+payeeAndAcct fs os = allocateCells os ws where
+  ws = fieldWidth os ss fs rw
+  ss = O.spacers os
+  rw = O.width os
+
+
+-- | Allocates cells. Returns a pair, with the first element being the
+-- list of allocated cells, and the second indicating the width of the
+-- cells, which will be greater than zero.
+allocateCells ::
+  Options.T
+  -> Fields Int
+  -> [Box]
+  -> Fields (Maybe ([R.ColumnSpec], Int))
+allocateCells os fs is = let
+  cellMakers = Fields allocPayee allocAcct
+  mkCells width maker =
+    if width > 0
+    then Just (map (maker width os) is)
+    else Nothing
+  unShrunkCells = mkCells <$> fs <*> cellMakers
+  in fmap (fmap removeExtraSpace) unShrunkCells
+
+
+-- | After first being allocated by allocPayee and allocAcct, cells
+-- are as wide as the total space allocated. This function removes the
+-- extra space, making all the cells as wide as the widest
+-- cell. Returns the resized cells and the new width.
+removeExtraSpace :: [R.ColumnSpec] -> ([R.ColumnSpec], Int)
+removeExtraSpace cs = (trimmed, len) where
+  len = Fdbl.foldl' f 0 cs where
+    f acc c = max acc (Fdbl.foldl' g 0 (R.bits c)) where
+      g inAcc chk = max inAcc (C.unWidth . C.chunkWidth $ chk)
+  trimmed = map f cs where
+    f c = c { R.width = C.Width len }
+
+-- | Gets the width of the two allocated fields.
+fieldWidth ::
+  Options.T
+  -> Spacers.T Int
+  -> G.Fields (Maybe Int)
+  -> O.ReportWidth
+  -> Fields Int
+fieldWidth os ss fs (O.ReportWidth rw) = let
+  flds = optionsToFields os
+  grownWidth = sumGrowersAndSpacers fs ss
+  widthForCells = rw - grownWidth - allocSpacerWidth
+  payeeSpacerWidth = if payee flds then abs (S.payee ss) else 0
+  acctSpacerWidth = if account flds then abs (S.account ss) else 0
+  allocSpacerWidth = payeeSpacerWidth + acctSpacerWidth
+  allocs = (\bool alloc -> if bool then alloc else A.allocation 0)
+           <$> flds
+           <*> Fields (O.payeeAllocation os) (O.accountAllocation os)
+  in if widthForCells < 1
+     then pure 0
+     else A.allocate allocs widthForCells
+
+
+optionsToFields :: Options.T -> Fields Bool
+optionsToFields os = let f = O.fields os in Fields {
+  payee = F.payee f
+  , account = F.account f }
+
+
+-- | Sums spacers for growing cells. This function is intended for use
+-- only by the functions that allocate cells for the report, so it
+-- assumes that either the Payee or the Account field is showing. Sums
+-- all spacers, UNLESS the rightmost field is from PostingDrCr to
+-- TotalCmdty, in which case the rightmost spacer is omitted. Apply to
+-- the second element of the tuple returned by growCells (which
+-- reflects which fields actually have width) and to the accompanying
+-- Spacers.
+sumSpacers ::
+  G.Fields (Maybe a)
+  -> Spacers.T Int
+  -> Int
+sumSpacers fs =
+  sum
+  . map fst
+  . appearingSpacers
+  . catMaybes
+  . Fdbl.toList
+  . fmap toWidth
+  . pairedWithSpacers fs
+  
+
+-- | Takes a triple:
+--
+-- * The first element is Just _ if the field appears in the report;
+-- Nothing if not
+--
+-- * The second element is Maybe Int for the width of the spacer
+-- (TotalQty has no spacer, so it will be Nothing)
+--
+-- * The third element is the EFields tag
+--
+-- Returns Nothing if the field does not appear in the report. Returns
+-- Just a pair if the field does appear in the report, where the first
+-- element is the width of the spacer, and the second element is the
+-- EFields tag.
+toWidth :: (Maybe a, Maybe Int, t) -> Maybe (Int, t)
+toWidth (maybeShowing, maybeWidth, tag) =
+  if isJust maybeShowing
+  then case maybeWidth of
+    Just w -> Just (w, tag)
+    Nothing -> Just (0, tag)
+  else Nothing
+
+
+-- | Given a list of all spacers that are attached to the fields that
+-- are present in a report, return a list of the spacers that will
+-- actually appear in the report. The rightmost spacer does not appear
+-- if it is to the right of Account (unless there is a TotalQty field,
+-- in which case, all spacers appear because TotalQty has no spacer.)
+appearingSpacers :: [(Int, G.EFields)] -> [(Int, G.EFields)]
+appearingSpacers ss = case ss of
+  [] -> []
+  l -> case snd $ last l of
+    G.ETotalQty -> l
+    t -> if t > G.ENumber
+         then init l
+         else l
+
+-- | Applied to two arguments: first, a Fields, and second, a
+-- Spacers. Combines each Field with its corresponding Spacer and with
+-- the GFields, which indicates each particular field.
+pairedWithSpacers ::
+  G.Fields a
+  -> Spacers.T b
+  -> G.Fields (a, Maybe b, G.EFields)
+pairedWithSpacers f s =
+  (\(a, b) c -> (a, b, c))
+  <$> G.pairWithSpacer f s
+  <*> G.eFields
+
+-- | Sums the contents of growing cells and their accompanying
+-- spacers; makes the adjustments described in sumSpacers.
+sumGrowersAndSpacers ::
+  G.Fields (Maybe Int)
+  -> Spacers.T Int
+  -> Int
+sumGrowersAndSpacers fs ss = spacers + flds where
+  spacers = sumSpacers fs ss
+  flds = Fdbl.foldr f 0 fs where
+    f maybeI acc = case maybeI of
+      Nothing -> acc
+      Just i -> acc + i
+
+
+allocPayee :: Int -> Options.T -> Box -> R.ColumnSpec
+allocPayee w os i = let
+  pb = L.boxPostFam i
+  ts = PC.colors (M.visibleNum . L.boxMeta $ i) (O.baseColors os)
+  c = R.ColumnSpec j (C.Width w) ts sq
+  j = R.LeftJustify
+  sq = case Q.payee pb of
+    Nothing -> []
+    Just pye -> let
+      wrapped =
+        Fdbl.toList
+        . TF.unLines 
+        . TF.wordWrap w
+        . TF.txtWords
+        . HT.text
+        $ pye
+      toBit (TF.Words seqTxts) =
+        C.chunk ts
+        . X.unwords
+        . Fdbl.toList
+        $ seqTxts
+      in fmap toBit wrapped
+  in c
+
+
+allocAcct :: Int -> Options.T -> Box -> R.ColumnSpec
+allocAcct aw os i = let
+  pb = L.boxPostFam i
+  ts = PC.colors (M.visibleNum . L.boxMeta $ i) (O.baseColors os) in
+  R.ColumnSpec R.LeftJustify (C.Width aw) ts $ let
+    target = TF.Target aw
+    shortest = TF.Shortest . O.subAccountLength $ os
+    a = Q.account pb
+    ws = TF.Words . Seq.fromList . HT.textList $ a
+    (TF.Words shortened) = TF.shorten shortest target ws
+    in [C.chunk ts
+       . X.concat
+       . intersperse (X.singleton ':')
+       . Fdbl.toList
+       $ shortened]
+
+instance Functor Fields where
+  fmap f i = Fields {
+    payee = f (payee i)
+    , account = f (account i) }
+
+instance Applicative Fields where
+  pure a = Fields a a
+  ff <*> fa = Fields {
+    payee = payee ff (payee fa)
+    , account = account ff (account fa) }
+
+instance Fdbl.Foldable Fields where
+  foldr f z flds =
+    f (payee flds) (f (account flds) z)
+
+instance T.Traversable Fields where
+  traverse f flds =
+    Fields <$> f (payee flds) <*> f (account flds)
+  
diff --git a/Penny/Cabin/Posts/BottomRows.hs b/Penny/Cabin/Posts/BottomRows.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts/BottomRows.hs
@@ -0,0 +1,623 @@
+-- | Fills the bottom rows, which contain the tags, memo, and
+-- filename. These rows are formatted as follows:
+--
+-- * If the columns for TotalDrCr, TotalCmdty, and TotalQty are all
+-- present, AND if there are at least TWO other columns present, then
+-- there will be a hanging indent. The bottom rows will begin at the
+-- SECOND column and end with the last column to the left of
+-- TotalDrCr. In this case, each bottom row will have three cells: one
+-- padding on the left, one main content, and one padding on the
+-- right.
+--
+-- * Otherwise, if there are NO columns in the top row, these rows
+-- will take the entire width of the report. Each bottom row will have
+-- one cell.
+--
+-- * Otherwise, the bottom rows are as wide as all the top cells
+-- combined. Each bottom row will have one cell.
+
+module Penny.Cabin.Posts.BottomRows (
+  bottomRows, Fields(..), TopRowCells(..), mergeWithSpacers,
+  topRowCells) where
+
+import Control.Applicative((<$>), Applicative(pure,  (<*>)))
+import qualified Data.Foldable as Fdbl
+import Control.Monad (guard)
+import Data.List (intersperse, find)
+import qualified Data.List.NonEmpty as NE
+import Data.Maybe (catMaybes)
+import Data.Monoid (mappend, mempty, First(First, getFirst))
+import qualified Data.Sequence as Seq
+import qualified Data.Text as X
+import qualified Data.Traversable as T
+import qualified Penny.Cabin.Chunk as C
+import qualified Penny.Cabin.Row as R
+import qualified Penny.Cabin.TextFormat as TF
+import qualified Penny.Cabin.Posts.Allocated as A
+import qualified Penny.Cabin.Colors as PC
+import qualified Penny.Cabin.Posts.Fields as Fields
+import qualified Penny.Cabin.Posts.Fields as F
+import qualified Penny.Cabin.Posts.Growers as G
+import qualified Penny.Cabin.Posts.Meta as M
+import qualified Penny.Cabin.Posts.Options as Options
+import qualified Penny.Cabin.Posts.Options as O
+import qualified Penny.Cabin.Posts.Spacers as Spacers
+import qualified Penny.Cabin.Posts.Spacers as S
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.HasText as HT
+import qualified Penny.Lincoln.Queries as Q
+
+type Box = L.Box M.PostMeta 
+
+bottomRows ::
+  G.Fields (Maybe Int)
+  -> A.Fields (Maybe Int)
+  -> Options.T
+  -> [Box]
+  -> Fields (Maybe [[C.Chunk]])
+bottomRows gf af os is = makeRows is pcs where
+  pcs = infoProcessors topSpecs rw wanted
+  wanted = requestedMakers os
+  topSpecs = topCellSpecs gf af (O.spacers os)
+  rw = O.width os
+
+
+data Fields a = Fields {
+  tags :: a
+  , memo :: a
+  , filename :: a
+  } deriving (Show, Eq)
+
+instance Fdbl.Foldable Fields where
+  foldr f z d =
+    f (tags d)
+    (f (memo d)
+     (f (filename d) z))
+
+instance Functor Fields where
+  fmap f (Fields t m fn) =
+    Fields (f t) (f m) (f fn)
+
+instance Applicative Fields where
+  pure a = Fields a a a
+  ff <*> fa = Fields {
+    tags = (tags ff) (tags fa)
+    , memo = (memo ff) (memo fa)
+    , filename = (filename ff) (filename fa)
+    }
+
+bottomRowsFields :: Fields.T a -> Fields a
+bottomRowsFields f = Fields {
+  tags = F.tags f
+  , memo = F.memo f
+  , filename = F.filename f }
+
+
+data Hanging a = Hanging {
+  leftPad :: a
+  , mainCell :: a
+  , rightPad :: a
+  } deriving (Show, Eq)
+
+
+newtype SpacerWidth = SpacerWidth Int deriving (Show, Eq)
+newtype ContentWidth = ContentWidth Int deriving (Show, Eq)
+
+
+hanging ::
+  [TopCellSpec]
+  -> Maybe ((Box -> Int -> (C.TextSpec, R.ColumnSpec))
+            -> Box -> [C.Chunk])
+hanging specs = hangingWidths specs
+                >>= return . hangingInfoProcessor
+
+hangingInfoProcessor ::
+  Hanging Int
+  -> (Box -> Int -> (C.TextSpec, R.ColumnSpec))
+  -> Box
+  -> [C.Chunk]
+hangingInfoProcessor widths mkr info = row where
+  row = R.row [left, mid, right]
+  (ts, mid) = mkr info (mainCell widths)
+  mkPad w = R.ColumnSpec R.LeftJustify (C.Width w) ts []
+  left = mkPad (leftPad widths)
+  right = mkPad (rightPad widths)
+
+widthOfTopColumns ::
+  [TopCellSpec]
+  -> Maybe ((Box -> Int -> (C.TextSpec, R.ColumnSpec))
+            -> Box -> [C.Chunk])
+widthOfTopColumns ts =
+  if null ts
+  then Nothing
+  else Just $ makeSpecificWidth w where
+    w = Fdbl.foldl' f 0 ts
+    f acc (_, maySpcWidth, (ContentWidth cw)) =
+      acc + cw + maybe 0 (\(SpacerWidth sw) -> sw) maySpcWidth
+
+
+widthOfReport ::
+  O.ReportWidth
+  -> (Box -> Int -> (C.TextSpec, R.ColumnSpec))
+  -> Box
+  -> [C.Chunk]
+widthOfReport (O.ReportWidth rw) fn info =
+  makeSpecificWidth rw fn info
+
+chooseProcessor ::
+  [TopCellSpec]
+  -> O.ReportWidth
+  -> (Box -> Int -> (C.TextSpec, R.ColumnSpec))
+  -> Box
+  -> [C.Chunk]
+chooseProcessor specs rw fn = let
+  firstTwo = First (hanging specs)
+             `mappend` First (widthOfTopColumns specs)
+  in case getFirst firstTwo of
+    Nothing -> widthOfReport rw fn
+    Just r -> r fn
+
+infoProcessors ::
+  [TopCellSpec]
+  -> O.ReportWidth
+  -> Fields (Maybe (Box -> Int -> (C.TextSpec, R.ColumnSpec)))
+  -> Fields (Maybe (Box -> [C.Chunk]))
+infoProcessors specs rw flds = let
+  chooser = chooseProcessor specs rw
+  mkProcessor mayFn = case mayFn of
+    Nothing -> Nothing
+    Just fn -> Just $ chooser fn
+  in mkProcessor <$> flds
+
+
+makeRows ::
+  [Box]
+  -> Fields (Maybe (Box -> [C.Chunk]))
+  -> Fields (Maybe [[C.Chunk]])
+makeRows is flds = let
+  mkRow fn = map fn is
+  in fmap (fmap mkRow) flds
+
+
+-- | Calculates column widths for a Hanging report. If it cannot
+-- calculate the widths (because these cells do not support hanging),
+-- returns Nothing.
+hangingWidths :: [TopCellSpec]
+                 -> Maybe (Hanging Int)
+hangingWidths ls = do
+  let len = length ls
+  guard (len > 4)
+  let matchColumn x (c, _, _) = x == c
+  totDrCr <- find (matchColumn ETotalDrCr) ls
+  totCmdty <- find (matchColumn ETotalCmdty) ls
+  totQty <- find (matchColumn ETotalQty) ls
+  let (first:middle) = take (len - 3) ls
+  mid <- NE.nonEmpty middle
+  return $ calcHangingWidths first mid (totDrCr, totCmdty, totQty)
+
+type TopCellSpec = (ETopRowCells, Maybe SpacerWidth, ContentWidth)
+
+-- | Given the first column in the top row, at least one middle
+-- column, and the last three columns, calculate the width of the
+-- three columns in the hanging report.
+calcHangingWidths ::
+  TopCellSpec
+  -> NE.NonEmpty TopCellSpec
+  -> (TopCellSpec, TopCellSpec, TopCellSpec)
+  -> Hanging Int
+calcHangingWidths l m r = Hanging left middle right where
+  calcWidth (_, maybeSp, (ContentWidth c)) =
+    c + maybe 0 (\(SpacerWidth w) -> abs w) maybeSp
+  left = calcWidth l
+  middle = Fdbl.foldl' f 0 m where
+    f acc c = acc + calcWidth c
+  (totDrCr, totCmdty, totQty) = r
+  right = calcWidth totDrCr + calcWidth totCmdty
+          + calcWidth totQty
+
+
+topCellSpecs :: G.Fields (Maybe Int)
+                -> A.Fields (Maybe Int)
+                -> Spacers.T Int
+                -> [TopCellSpec]
+topCellSpecs gFlds aFlds spcs = let
+  allFlds = topRowCells gFlds aFlds
+  cws = fmap (fmap ContentWidth) allFlds
+  merged = mergeWithSpacers cws spcs
+  tripler e (cw, maybeSpc) = (e, (fmap SpacerWidth maybeSpc), cw)
+  list = Fdbl.toList $ tripler <$> eTopRowCells <*> merged
+  toMaybe (e, maybeS, maybeC) = case maybeC of
+    Nothing -> Nothing
+    Just c -> Just (e, maybeS, c)
+  in catMaybes (map toMaybe list)
+  
+
+-- | Merges a TopRowCells with a Spacers. Returns Maybes because
+-- totalQty has no spacer.
+mergeWithSpacers ::
+  TopRowCells a
+  -> Spacers.T b
+  -> TopRowCells (a, Maybe b)
+mergeWithSpacers t s = TopRowCells {
+  globalTransaction = (globalTransaction t, Just (S.globalTransaction s))
+  , revGlobalTransaction = (revGlobalTransaction t, Just (S.revGlobalTransaction s))
+  , globalPosting = (globalPosting t, Just (S.globalPosting s))
+  , revGlobalPosting = (revGlobalPosting t, Just (S.revGlobalPosting s))
+  , fileTransaction = (fileTransaction t, Just (S.fileTransaction s))
+  , revFileTransaction = (revFileTransaction t, Just (S.revFileTransaction s))
+  , filePosting = (filePosting t, Just (S.filePosting s))
+  , revFilePosting = (revFilePosting t, Just (S.revFilePosting s))
+  , filtered = (filtered t, Just (S.filtered s))
+  , revFiltered = (revFiltered t, Just (S.revFiltered s))
+  , sorted = (sorted t, Just (S.sorted s))
+  , revSorted = (revSorted t, Just (S.revSorted s))
+  , visible = (visible t, Just (S.visible s))
+  , revVisible = (revVisible t, Just (S.revVisible s))
+  , lineNum = (lineNum t, Just (S.lineNum s))
+  , date = (date t, Just (S.date s))
+  , flag = (flag t, Just (S.flag s))
+  , number = (number t, Just (S.number s))
+  , payee = (payee t, Just (S.payee s))
+  , account = (account t, Just (S.account s))
+  , postingDrCr = (postingDrCr t, Just (S.postingDrCr s))
+  , postingCmdty = (postingCmdty t, Just (S.postingCmdty s))
+  , postingQty = (postingQty t, Just (S.postingQty s))
+  , totalDrCr = (totalDrCr t, Just (S.totalDrCr s))
+  , totalCmdty = (totalCmdty t, Just (S.totalCmdty s))
+  , totalQty = (totalQty t, Nothing) }
+
+
+-- | Applied to a function that, when applied to the width of a cell,
+-- returns a cell filled with data, returns a Row with that cell.
+makeSpecificWidth :: Int -> (Box -> Int -> (a, R.ColumnSpec))
+                     -> Box -> [C.Chunk]
+makeSpecificWidth w f i = R.row [c] where
+  (_, c) = f i w
+
+
+type Maker = Options.T -> Box -> Int -> (C.TextSpec, R.ColumnSpec)
+
+makers :: Fields Maker
+makers = Fields tagsCell memoCell filenameCell
+
+-- | Applied to an Options, indicating which reports the user wants,
+-- returns a Fields (Maybe Maker) with a Maker in each respective
+-- field that the user wants to see.
+requestedMakers ::
+  Options.T
+  -> Fields (Maybe (Box -> Int -> (C.TextSpec, R.ColumnSpec)))
+requestedMakers os = let
+  flds = bottomRowsFields (O.fields os)
+  filler b mkr = if b then Just $ mkr os else Nothing
+  in filler <$> flds <*> makers
+
+tagsCell :: Options.T -> Box -> Int -> (C.TextSpec, R.ColumnSpec)
+tagsCell os info w = (ts, cell) where
+  vn = M.visibleNum . L.boxMeta $ info
+  cell = R.ColumnSpec R.LeftJustify (C.Width w) ts cs
+  ts = PC.colors vn (O.baseColors os)
+  cs =
+    Fdbl.toList
+    . fmap toBit
+    . TF.unLines
+    . TF.wordWrap w
+    . TF.Words
+    . Seq.fromList
+    . map (X.cons '*')
+    . HT.textList
+    . Q.tags
+    . L.boxPostFam
+    $ info
+  toBit (TF.Words ws) = C.chunk ts t where
+    t = X.concat . intersperse (X.singleton ' ') . Fdbl.toList $ ws
+
+
+memoBits :: C.TextSpec -> L.Memo -> C.Width -> [C.Chunk]
+memoBits ts m (C.Width w) = cs where
+  cs = Fdbl.toList
+       . fmap toBit
+       . TF.unLines
+       . TF.wordWrap w
+       . TF.Words
+       . Seq.fromList
+       . X.words
+       . HT.text
+       . HT.Delimited (X.singleton ' ')
+       . HT.textList
+       $ m
+  toBit (TF.Words ws) = C.chunk ts (X.unwords . Fdbl.toList $ ws)
+
+
+memoCell :: Options.T -> Box -> Int -> (C.TextSpec, R.ColumnSpec)
+memoCell os info width = (ts, cell) where
+  w = C.Width width
+  vn = M.visibleNum . L.boxMeta $ info
+  cell = R.ColumnSpec R.LeftJustify w ts cs
+  ts = PC.colors vn (O.baseColors os)
+  pm = Q.postingMemo . L.boxPostFam $ info
+  tm = Q.transactionMemo . L.boxPostFam $ info
+  nullMemo (L.Memo m) = null m
+  cs = case (nullMemo pm, nullMemo tm) of
+    (True, True) -> mempty
+    (False, True) -> memoBits ts pm w
+    (True, False) -> memoBits ts tm w
+    (False, False) -> memoBits ts pm w `mappend` memoBits ts tm w
+  
+
+filenameCell :: Options.T -> Box -> Int -> (C.TextSpec, R.ColumnSpec)
+filenameCell os info width = (ts, cell) where
+  w = C.Width width
+  vn = M.visibleNum . L.boxMeta $ info
+  cell = R.ColumnSpec R.LeftJustify w ts cs
+  toBit n = C.chunk ts
+            . X.drop (max 0 (X.length n - width)) $ n
+  cs = case Q.filename . L.boxPostFam $ info of
+    Nothing -> []
+    Just fn -> [toBit . L.unFilename $ fn]
+  ts = PC.colors vn (O.baseColors os)
+
+
+
+data TopRowCells a = TopRowCells {
+  globalTransaction      :: a
+  , revGlobalTransaction :: a
+  , globalPosting        :: a
+  , revGlobalPosting     :: a
+  , fileTransaction      :: a
+  , revFileTransaction   :: a
+  , filePosting          :: a
+  , revFilePosting       :: a
+  , filtered             :: a
+  , revFiltered          :: a
+  , sorted               :: a
+  , revSorted            :: a
+  , visible              :: a
+  , revVisible           :: a
+  , lineNum              :: a
+    -- ^ The line number from the posting's metadata
+  , date                 :: a
+  , flag                 :: a
+  , number               :: a
+  , payee                :: a
+  , account              :: a
+  , postingDrCr          :: a
+  , postingCmdty         :: a
+  , postingQty           :: a
+  , totalDrCr            :: a
+  , totalCmdty           :: a
+  , totalQty             :: a }
+  deriving (Show, Eq)
+
+topRowCells :: G.Fields a -> A.Fields a -> TopRowCells a
+topRowCells g a = TopRowCells {
+  globalTransaction      = G.globalTransaction g
+  , revGlobalTransaction = G.revGlobalTransaction g
+  , globalPosting        = G.globalPosting g
+  , revGlobalPosting     = G.revGlobalPosting g
+  , fileTransaction      = G.fileTransaction g
+  , revFileTransaction   = G.revFileTransaction g
+  , filePosting          = G.filePosting g
+  , revFilePosting       = G.revFilePosting g
+  , filtered             = G.filtered g
+  , revFiltered          = G.revFiltered g
+  , sorted               = G.sorted g
+  , revSorted            = G.revSorted g
+  , visible              = G.visible g
+  , revVisible           = G.revVisible g
+  , lineNum              = G.lineNum g
+  , date                 = G.date g
+  , flag                 = G.flag g
+  , number               = G.number g
+  , payee                = A.payee a
+  , account              = A.account a
+  , postingDrCr          = G.postingDrCr g
+  , postingCmdty         = G.postingCmdty g
+  , postingQty           = G.postingQty g
+  , totalDrCr            = G.totalDrCr g
+  , totalCmdty           = G.totalCmdty g
+  , totalQty             = G.totalQty g }
+
+
+data ETopRowCells =
+  EGlobalTransaction
+  | ERevGlobalTransaction
+  | EGlobalPosting
+  | ERevGlobalPosting
+  | EFileTransaction
+  | ERevFileTransaction
+  | EFilePosting
+  | ERevFilePosting
+  | EFiltered
+  | ERevFiltered
+  | ESorted
+  | ERevSorted
+  | EVisible
+  | ERevVisible
+  | ELineNum
+  | EDate
+  | EFlag
+  | ENumber
+  | EPayee
+  | EAccount
+  | EPostingDrCr
+  | EPostingCmdty
+  | EPostingQty
+  | ETotalDrCr
+  | ETotalCmdty
+  | ETotalQty
+  deriving (Show, Eq, Enum)
+
+eTopRowCells :: TopRowCells ETopRowCells
+eTopRowCells = TopRowCells {
+  globalTransaction      = EGlobalTransaction
+  , revGlobalTransaction = ERevGlobalTransaction
+  , globalPosting        = EGlobalPosting
+  , revGlobalPosting     = ERevGlobalPosting
+  , fileTransaction      = EFileTransaction
+  , revFileTransaction   = ERevFileTransaction
+  , filePosting          = EFilePosting
+  , revFilePosting       = ERevFilePosting
+  , filtered             = EFiltered
+  , revFiltered          = ERevFiltered
+  , sorted               = ESorted
+  , revSorted            = ERevSorted
+  , visible              = EVisible
+  , revVisible           = ERevVisible
+  , lineNum              = ELineNum
+  , date                 = EDate
+  , flag                 = EFlag
+  , number               = ENumber
+  , payee                = EPayee
+  , account              = EAccount
+  , postingDrCr          = EPostingDrCr
+  , postingCmdty         = EPostingCmdty
+  , postingQty           = EPostingQty
+  , totalDrCr            = ETotalDrCr
+  , totalCmdty           = ETotalCmdty
+  , totalQty             = ETotalQty }
+
+instance Functor TopRowCells where
+  fmap f t = TopRowCells {
+    globalTransaction      = f (globalTransaction    t)
+    , revGlobalTransaction = f (revGlobalTransaction t)
+    , globalPosting        = f (globalPosting        t)
+    , revGlobalPosting     = f (revGlobalPosting     t)
+    , fileTransaction      = f (fileTransaction      t)
+    , revFileTransaction   = f (revFileTransaction   t)
+    , filePosting          = f (filePosting          t)
+    , revFilePosting       = f (revFilePosting       t)
+    , filtered             = f (filtered             t)
+    , revFiltered          = f (revFiltered          t)
+    , sorted               = f (sorted               t)
+    , revSorted            = f (revSorted            t)
+    , visible              = f (visible              t)
+    , revVisible           = f (revVisible           t)
+    , lineNum              = f (lineNum              t)
+    , date                 = f (date                 t)
+    , flag                 = f (flag                 t)
+    , number               = f (number               t)
+    , payee                = f (payee                t)
+    , account              = f (account              t)
+    , postingDrCr          = f (postingDrCr          t)
+    , postingCmdty         = f (postingCmdty         t)
+    , postingQty           = f (postingQty           t)
+    , totalDrCr            = f (totalDrCr            t)
+    , totalCmdty           = f (totalCmdty           t)
+    , totalQty             = f (totalQty             t) }
+
+instance Applicative TopRowCells where
+  pure a = TopRowCells {
+    globalTransaction      = a
+    , revGlobalTransaction = a
+    , globalPosting        = a
+    , revGlobalPosting     = a
+    , fileTransaction      = a
+    , revFileTransaction   = a
+    , filePosting          = a
+    , revFilePosting       = a
+    , filtered             = a
+    , revFiltered          = a
+    , sorted               = a
+    , revSorted            = a
+    , visible              = a
+    , revVisible           = a
+    , lineNum              = a
+    , date                 = a
+    , flag                 = a
+    , number               = a
+    , payee                = a
+    , account              = a
+    , postingDrCr          = a
+    , postingCmdty         = a
+    , postingQty           = a
+    , totalDrCr            = a
+    , totalCmdty           = a
+    , totalQty             = a }
+
+  ff <*> fa = TopRowCells {
+    globalTransaction      = globalTransaction    ff (globalTransaction    fa)
+    , revGlobalTransaction = revGlobalTransaction ff (revGlobalTransaction fa)
+    , globalPosting        = globalPosting        ff (globalPosting        fa)
+    , revGlobalPosting     = revGlobalPosting     ff (revGlobalPosting     fa)
+    , fileTransaction      = fileTransaction      ff (fileTransaction      fa)
+    , revFileTransaction   = revFileTransaction   ff (revFileTransaction   fa)
+    , filePosting          = filePosting          ff (filePosting          fa)
+    , revFilePosting       = revFilePosting       ff (revFilePosting       fa)
+    , filtered             = filtered             ff (filtered             fa)
+    , revFiltered          = revFiltered          ff (revFiltered          fa)
+    , sorted               = sorted               ff (sorted               fa)
+    , revSorted            = revSorted            ff (revSorted            fa)
+    , visible              = visible              ff (visible              fa)
+    , revVisible           = revVisible           ff (revVisible           fa)
+    , lineNum              = lineNum              ff (lineNum              fa)
+    , date                 = date                 ff (date                 fa)
+    , flag                 = flag                 ff (flag                 fa)
+    , number               = number               ff (number               fa)
+    , payee                = payee                ff (payee                fa)
+    , account              = account              ff (account              fa)
+    , postingDrCr          = postingDrCr          ff (postingDrCr          fa)
+    , postingCmdty         = postingCmdty         ff (postingCmdty         fa)
+    , postingQty           = postingQty           ff (postingQty           fa)
+    , totalDrCr            = totalDrCr            ff (totalDrCr            fa)
+    , totalCmdty           = totalCmdty           ff (totalCmdty           fa)
+    , totalQty             = totalQty             ff (totalQty             fa) }
+
+instance Fdbl.Foldable TopRowCells where
+  foldr f z o =
+    f (globalTransaction o)
+    (f (revGlobalTransaction o)
+     (f (globalPosting o)
+      (f (revGlobalPosting o)
+       (f (fileTransaction o)
+        (f (revFileTransaction o)
+         (f (filePosting o)
+          (f (revFilePosting o)
+           (f (filtered o)
+            (f (revFiltered o)
+             (f (sorted o)
+              (f (revSorted o)
+               (f (visible o)
+                (f (revVisible o)
+                 (f (lineNum o)
+                  (f (date o)
+                   (f (flag o)
+                    (f (number o)
+                     (f (payee o)
+                      (f (account o)
+                       (f (postingDrCr o)
+                        (f (postingCmdty o)
+                         (f (postingQty o)
+                          (f (totalDrCr o)
+                           (f (totalCmdty o)
+                            (f (totalQty o) z)))))))))))))))))))))))))
+
+instance T.Traversable TopRowCells where
+  traverse f t =
+    TopRowCells
+    <$> f (globalTransaction t)
+    <*> f (revGlobalTransaction t)
+    <*> f (globalPosting t)
+    <*> f (revGlobalPosting t)
+    <*> f (fileTransaction t)
+    <*> f (revFileTransaction t)
+    <*> f (filePosting t)
+    <*> f (revFilePosting t)
+    <*> f (filtered t)
+    <*> f (revFiltered t)
+    <*> f (sorted t)
+    <*> f (revSorted t)
+    <*> f (visible t)
+    <*> f (revVisible t)
+    <*> f (lineNum t)
+    <*> f (date t)
+    <*> f (flag t)
+    <*> f (number t)
+    <*> f (payee t)
+    <*> f (account t)
+    <*> f (postingDrCr t)
+    <*> f (postingCmdty t)
+    <*> f (postingQty t)
+    <*> f (totalDrCr t)
+    <*> f (totalCmdty t)
+    <*> f (totalQty t)
+
diff --git a/Penny/Cabin/Posts/Chunk.hs b/Penny/Cabin/Posts/Chunk.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts/Chunk.hs
@@ -0,0 +1,104 @@
+module Penny.Cabin.Posts.Chunk (makeChunk) where
+
+import qualified Data.Foldable as Fdbl
+import Data.List (transpose)
+import Data.Maybe (isNothing, catMaybes)
+import qualified Penny.Cabin.Posts.Growers as G
+import qualified Penny.Cabin.Posts.Allocated as A
+import qualified Penny.Cabin.Posts.BottomRows as B
+import qualified Penny.Cabin.Posts.Options as Options
+import qualified Penny.Cabin.Row as R
+import qualified Penny.Cabin.Chunk as C
+import qualified Penny.Cabin.Colors as PC
+import qualified Penny.Lincoln as L
+import qualified Penny.Cabin.Posts.Meta as M
+
+type Box = L.Box M.PostMeta 
+
+makeChunk :: Options.T -> [Box] -> [C.Chunk]
+makeChunk os is = let
+  fmapSnd flds = fmap (fmap snd) flds
+  fmapFst flds = fmap (fmap fst) flds
+  gFldW = fmapSnd gFlds
+  aFldW = fmapSnd aFlds
+  gFlds = G.growCells os is
+  aFlds = A.payeeAndAcct gFldW os is
+  bFlds = B.bottomRows gFldW aFldW os is
+  topCells = B.topRowCells (fmapFst gFlds) (fmapFst aFlds)
+  withSpacers = B.mergeWithSpacers topCells (Options.spacers os)
+  topRows = makeTopRows (Options.baseColors os) withSpacers
+  bottomRows = makeBottomRows bFlds
+  in makeAllRows topRows bottomRows
+
+
+topRowsCells ::
+  PC.BaseColors
+  -> B.TopRowCells (Maybe [R.ColumnSpec], Maybe Int)
+  -> [[(R.ColumnSpec, Maybe R.ColumnSpec)]]
+topRowsCells bc t = let
+  toWithSpc (mayCs, maySp) = case mayCs of
+    Nothing -> Nothing
+    Just cs -> Just (makeSpacers bc cs maySp)
+  f mayPairList acc = case mayPairList of
+    Nothing -> acc
+    (Just pairList) -> pairList : acc
+  in transpose $ Fdbl.foldr f [] (fmap toWithSpc t)
+
+makeRow :: [(R.ColumnSpec, Maybe R.ColumnSpec)] -> [C.Chunk]
+makeRow = R.row . foldr f [] where
+  f (c, mayC) acc = case mayC of
+    Nothing -> c:acc
+    Just spcr -> c:spcr:acc
+
+
+makeSpacers ::
+  PC.BaseColors
+  -> [R.ColumnSpec]
+  -> Maybe Int
+  -> [(R.ColumnSpec, Maybe R.ColumnSpec)]
+makeSpacers bc cs mayI = case mayI of
+  Nothing -> map (\c -> (c, Nothing)) cs
+  Just i -> makeEvenOddSpacers bc cs i
+
+makeEvenOddSpacers ::
+  PC.BaseColors
+  -> [R.ColumnSpec]
+  -> Int
+  -> [(R.ColumnSpec, Maybe R.ColumnSpec)]
+makeEvenOddSpacers bc cs i = let absI = abs i in
+  if absI == 0
+  then map (\c -> (c, Nothing)) cs
+  else let
+    spcrs = cycle [Just $ mkSpcr evenTs, Just $ mkSpcr oddTs]
+    mkSpcr ts = R.ColumnSpec R.LeftJustify (C.Width absI) ts []
+    evenTs = PC.evenColors bc
+    oddTs = PC.oddColors bc
+    in zip cs spcrs
+
+makeTopRows ::
+  PC.BaseColors
+  -> B.TopRowCells (Maybe [R.ColumnSpec], Maybe Int)
+  -> Maybe [[C.Chunk]]
+makeTopRows bc trc =
+  if Fdbl.all (isNothing . fst) trc
+  then Nothing
+  else Just $ map makeRow . topRowsCells bc $ trc
+
+
+makeBottomRows ::
+  B.Fields (Maybe [[C.Chunk]])
+  -> Maybe [[[C.Chunk]]]
+makeBottomRows flds =
+  if Fdbl.all isNothing flds
+  then Nothing
+  else Just . transpose . catMaybes . Fdbl.toList $ flds
+
+makeAllRows :: Maybe [[C.Chunk]] -> Maybe [[[C.Chunk]]] -> [C.Chunk]
+makeAllRows mayrs mayrrs = case (mayrs, mayrrs) of
+  (Nothing, Nothing) -> []
+  (Just rs, Nothing) -> concat rs
+  (Nothing, Just rrs) -> concat . concat $ rrs
+  (Just rs, Just rrs) -> concat $ zipWith f rs rrs where
+    f topRow botRows = concat [topRow, concat botRows]
+
+
diff --git a/Penny/Cabin/Posts/Fields.hs b/Penny/Cabin/Posts/Fields.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts/Fields.hs
@@ -0,0 +1,166 @@
+-- | Fields that can appear in the Posts report.
+module Penny.Cabin.Posts.Fields where
+
+import Control.Applicative(Applicative(pure, (<*>)))
+import qualified Data.Foldable as F
+
+data T a = T {
+  globalTransaction :: a
+  , revGlobalTransaction :: a
+  , globalPosting :: a
+  , revGlobalPosting :: a
+  , fileTransaction :: a
+  , revFileTransaction :: a
+  , filePosting :: a
+  , revFilePosting :: a
+  , filtered :: a
+  , revFiltered :: a
+  , sorted :: a
+  , revSorted :: a
+  , visible :: a
+  , revVisible :: a
+  , lineNum :: a
+    -- ^ The line number from the posting's metadata
+  , date :: a
+  , flag :: a
+  , number :: a
+  , payee :: a
+  , account :: a
+  , postingDrCr :: a
+  , postingCmdty :: a
+  , postingQty :: a
+  , totalDrCr :: a
+  , totalCmdty :: a
+  , totalQty :: a
+  , tags :: a
+  , memo :: a
+  , filename :: a }
+  deriving (Show, Eq)
+
+instance Functor T where
+  fmap f fa = T {
+    globalTransaction = f (globalTransaction fa)
+    , revGlobalTransaction = f (revGlobalTransaction fa)
+    , globalPosting = f (globalPosting fa)
+    , revGlobalPosting = f (revGlobalPosting fa)
+    , fileTransaction = f (fileTransaction fa)
+    , revFileTransaction = f (revFileTransaction fa)
+    , filePosting = f (filePosting fa)
+    , revFilePosting = f (revFilePosting fa)
+    , filtered = f (filtered fa)
+    , revFiltered = f (revFiltered fa)
+    , sorted = f (sorted fa)
+    , revSorted = f (revSorted fa)
+    , visible = f (visible fa)
+    , revVisible = f (revVisible fa)
+    , lineNum = f (lineNum fa)
+    , date = f (date fa)
+    , flag = f (flag fa)
+    , number = f (number fa)
+    , payee = f (payee fa)
+    , account = f (account fa)
+    , postingDrCr = f (postingDrCr fa)
+    , postingCmdty = f (postingCmdty fa)
+    , postingQty = f (postingQty fa)
+    , totalDrCr = f (totalDrCr fa)
+    , totalCmdty = f (totalCmdty fa)
+    , totalQty = f (totalQty fa)
+    , tags = f (tags fa)
+    , memo = f (memo fa)
+    , filename = f (filename fa) }
+
+instance Applicative T where
+  pure a = T {
+    globalTransaction = a
+    , revGlobalTransaction = a
+    , globalPosting = a
+    , revGlobalPosting = a
+    , fileTransaction = a
+    , revFileTransaction = a
+    , filePosting = a
+    , revFilePosting = a
+    , filtered = a
+    , revFiltered = a
+    , sorted = a
+    , revSorted = a
+    , visible = a
+    , revVisible = a
+    , lineNum = a
+    , date = a
+    , flag = a
+    , number = a
+    , payee = a
+    , account = a
+    , postingDrCr = a
+    , postingCmdty = a
+    , postingQty = a
+    , totalDrCr = a
+    , totalCmdty = a
+    , totalQty = a
+    , tags = a
+    , memo = a
+    , filename = a }
+
+  ff <*> fa = T {
+    globalTransaction = globalTransaction ff (globalTransaction fa)
+    , revGlobalTransaction = revGlobalTransaction ff
+                             (revGlobalTransaction fa)
+    , globalPosting = globalPosting ff (globalPosting fa)
+    , revGlobalPosting = revGlobalPosting ff (revGlobalPosting fa)
+    , fileTransaction = fileTransaction ff (fileTransaction fa)
+    , revFileTransaction = revFileTransaction ff (revFileTransaction fa)
+    , filePosting = filePosting ff (filePosting fa)
+    , revFilePosting = revFilePosting ff (revFilePosting fa)
+    , filtered = filtered ff (filtered fa)
+    , revFiltered = revFiltered ff (revFiltered fa)
+    , sorted = sorted ff (sorted fa)
+    , revSorted = revSorted ff (revSorted fa)
+    , visible = visible ff (visible fa)
+    , revVisible = revVisible ff (revVisible fa)
+    , lineNum = lineNum ff (lineNum fa)
+    , date = date ff (date fa)
+    , flag = flag ff (flag fa)
+    , number = number ff (number fa)
+    , payee = payee ff (payee fa)
+    , account = account ff (account fa)
+    , postingDrCr = postingDrCr ff (postingDrCr fa)
+    , postingCmdty = postingCmdty ff (postingCmdty fa)
+    , postingQty = postingQty ff (postingQty fa)
+    , totalDrCr = totalDrCr ff (totalDrCr fa)
+    , totalCmdty = totalCmdty ff (totalCmdty fa)
+    , totalQty = totalQty ff (totalQty fa)
+    , tags = tags ff (tags fa)
+    , memo = memo ff (memo fa)
+    , filename = filename ff (filename fa) }
+
+instance F.Foldable T where
+  foldr f z t =
+    f (globalTransaction t)
+    (f (revGlobalTransaction t)
+     (f (globalPosting t)
+      (f (revGlobalPosting t)
+       (f (fileTransaction t)
+        (f (revFileTransaction t)
+         (f (filePosting t)
+          (f (revFilePosting t)
+           (f (filtered t)
+            (f (revFiltered t)
+             (f (sorted t)
+              (f (revSorted t)
+               (f (visible t)
+                (f (revVisible t)
+                 (f (lineNum t)
+                  (f (date t)
+                   (f (flag t)
+                    (f (number t)
+                     (f (payee t)
+                      (f (account t)
+                       (f (postingDrCr t)
+                        (f (postingCmdty t)
+                         (f (postingQty t)
+                          (f (totalDrCr t)
+                           (f (totalCmdty t)
+                            (f (totalQty t)
+                             (f (tags t)
+                              (f (memo t)
+                               (f (filename t) z))))))))))))))))))))))))))))
diff --git a/Penny/Cabin/Posts/Growers.hs b/Penny/Cabin/Posts/Growers.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts/Growers.hs
@@ -0,0 +1,610 @@
+-- | Calculates cells that "grow to fit." These cells grow to fit the
+-- widest cell in the column. No information is ever truncated from
+-- these cells (what use is a truncated dollar amount?)
+module Penny.Cabin.Posts.Growers (
+  growCells, Fields(..), grownWidth,
+  eFields, EFields(..), pairWithSpacer) where
+
+import Control.Applicative((<$>), Applicative(pure, (<*>)))
+import qualified Data.Foldable as Fdbl
+import Data.Map (elems, assocs)
+import qualified Data.Semigroup as Semi
+import Data.Semigroup ((<>))
+import Data.Text (Text, pack, empty)
+import qualified Data.Text as X
+import qualified Penny.Cabin.Chunk as C
+import qualified Penny.Cabin.Colors as PC
+import qualified Penny.Cabin.Posts.Options as O
+import qualified Penny.Cabin.Posts.Options as Options
+import qualified Penny.Cabin.Posts.Fields as F
+import qualified Penny.Cabin.Posts.Meta as M
+import qualified Penny.Cabin.Posts.Spacers as S
+import qualified Penny.Cabin.Posts.Spacers as Spacers
+import qualified Penny.Cabin.Row as R
+import qualified Penny.Liberty as Ly
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.Queries as Q
+
+
+type Box = L.Box M.PostMeta 
+
+-- | Grows the cells that will be GrowToFit cells in the report. First
+-- this function fills in all visible cells with text, but leaves the
+-- width undetermined. Then it determines the widest line in each
+-- column. Finally it adjusts each cell in the column so that it is
+-- that maximum width.
+--
+-- Returns a list of rows, and a Fields holding the width of each
+-- cell. Each of these widths will be at least 1; fields that were in
+-- the report but that ended up having no width are changed to
+-- Nothing.
+growCells ::
+  Options.T
+  -> [Box]
+  -> Fields (Maybe ([R.ColumnSpec], Int))
+growCells o infos = toPair <$> wanted <*> growers where
+  toPair b gwr
+    | b = let
+      cs = map (gwr o) infos
+      w = Fdbl.foldl' f 0 cs where
+        f acc c = max acc (widestLine c)
+      cs' = map (sizer (R.Width w)) cs
+      in if w > 0 then Just (cs', w) else Nothing
+    | otherwise = Nothing
+  wanted = growingFields o
+
+widestLine :: PreSpec -> Int
+widestLine (PreSpec _ _ bs) =
+  maximum . map (R.unWidth . C.chunkWidth) $ bs
+
+data PreSpec = PreSpec {
+  _justification :: R.Justification
+  , _padSpec :: C.TextSpec
+  , _bits :: [C.Chunk] }
+
+
+-- | Given a PreSpec and a width, create a ColumnSpec of the right
+-- size.
+sizer :: R.Width -> PreSpec -> R.ColumnSpec
+sizer w (PreSpec j ts bs) = R.ColumnSpec j w ts bs
+
+-- | Makes a left justified cell that is only one line long. The width
+-- is unset.
+oneLine :: Text -> Options.T -> Box -> PreSpec
+oneLine t os b =
+  let bc = Options.baseColors os
+      ts = PC.colors (M.visibleNum . L.boxMeta $ b) bc
+      j = R.LeftJustify
+      bit = C.chunk ts t
+  in PreSpec j ts [bit]
+
+growers :: Fields (Options.T -> Box -> PreSpec)
+growers = Fields {
+  globalTransaction      = getGlobalTransaction
+  , revGlobalTransaction = getRevGlobalTransaction
+  , globalPosting        = getGlobalPosting
+  , revGlobalPosting     = getRevGlobalPosting
+  , fileTransaction      = getFileTransaction
+  , revFileTransaction   = getRevFileTransaction
+  , filePosting          = getFilePosting
+  , revFilePosting       = getRevFilePosting
+  , filtered             = getFiltered
+  , revFiltered          = getRevFiltered
+  , sorted               = getSorted
+  , revSorted            = getRevSorted
+  , visible              = getVisible
+  , revVisible           = getRevVisible
+  , lineNum              = getLineNum
+  , date                 = getDate
+  , flag                 = getFlag
+  , number               = getNumber
+  , postingDrCr          = getPostingDrCr
+  , postingCmdty         = getPostingCmdty
+  , postingQty           = getPostingQty
+  , totalDrCr            = getTotalDrCr
+  , totalCmdty           = getTotalCmdty
+  , totalQty             = getTotalQty }
+
+-- | Make a left justified cell one line long that shows a serial.
+serialCellMaybe ::
+  (L.PostFam -> Maybe Int)
+  -- ^ When applied to a Box, this function returns Just Int if the
+  -- box has a serial, or Nothing if not.
+  
+  -> Options.T -> Box -> PreSpec
+serialCellMaybe f os b = oneLine t os b
+  where
+    t = case f (L.boxPostFam b) of
+      Nothing -> X.empty
+      Just i -> X.pack . show $ i
+
+serialCell ::
+  (M.PostMeta -> Int)
+  -> Options.T -> Box -> PreSpec
+serialCell f os b = oneLine t os b
+  where
+    t = pack . show . f . L.boxMeta $ b
+
+getGlobalTransaction :: Options.T -> Box -> PreSpec
+getGlobalTransaction =
+  serialCellMaybe (fmap (L.forward . L.unGlobalTransaction)
+                   . Q.globalTransaction)
+
+getRevGlobalTransaction :: Options.T -> Box -> PreSpec
+getRevGlobalTransaction =
+  serialCellMaybe (fmap (L.backward . L.unGlobalTransaction)
+                   . Q.globalTransaction)
+
+getGlobalPosting :: Options.T -> Box -> PreSpec
+getGlobalPosting =
+  serialCellMaybe (fmap (L.forward . L.unGlobalPosting)
+                   . Q.globalPosting)
+
+getRevGlobalPosting :: Options.T -> Box -> PreSpec
+getRevGlobalPosting =
+  serialCellMaybe (fmap (L.backward . L.unGlobalPosting)
+                   . Q.globalPosting)
+
+getFileTransaction :: Options.T -> Box -> PreSpec
+getFileTransaction =
+  serialCellMaybe (fmap (L.forward . L.unFileTransaction)
+                   . Q.fileTransaction)
+
+getRevFileTransaction :: Options.T -> Box -> PreSpec
+getRevFileTransaction =
+  serialCellMaybe (fmap (L.backward . L.unFileTransaction)
+                   . Q.fileTransaction)
+
+getFilePosting :: Options.T -> Box -> PreSpec
+getFilePosting =
+  serialCellMaybe (fmap (L.forward . L.unFilePosting)
+                   . Q.filePosting)
+
+getRevFilePosting :: Options.T -> Box -> PreSpec
+getRevFilePosting =
+  serialCellMaybe (fmap (L.backward . L.unFilePosting)
+                   . Q.filePosting)
+
+getSorted :: Options.T -> Box -> PreSpec
+getSorted =
+  serialCell (L.forward . Ly.unSortedNum . M.sortedNum)
+
+getRevSorted :: Options.T -> Box -> PreSpec
+getRevSorted =
+  serialCell (L.backward . Ly.unSortedNum . M.sortedNum)
+
+getFiltered :: Options.T -> Box -> PreSpec
+getFiltered =
+  serialCell (L.forward . Ly.unFilteredNum . M.filteredNum)
+
+getRevFiltered :: Options.T -> Box -> PreSpec
+getRevFiltered =
+  serialCell (L.backward . Ly.unFilteredNum . M.filteredNum)
+
+getVisible :: Options.T -> Box -> PreSpec
+getVisible =
+  serialCell (L.forward . M.unVisibleNum . M.visibleNum)
+
+getRevVisible :: Options.T -> Box -> PreSpec
+getRevVisible =
+  serialCell (L.backward . M.unVisibleNum . M.visibleNum)
+
+
+getLineNum :: Options.T -> Box -> PreSpec
+getLineNum os b = oneLine t os b where
+  lineTxt = pack . show . L.unPostingLine
+  t = maybe empty lineTxt (Q.postingLine . L.boxPostFam $ b)
+
+getDate :: Options.T -> Box -> PreSpec
+getDate os i = oneLine t os i where
+  t = O.dateFormat os i
+
+getFlag :: Options.T -> Box -> PreSpec
+getFlag os i = oneLine t os i where
+  t = maybe empty L.text (Q.flag . L.boxPostFam $ i)
+
+getNumber :: Options.T -> Box -> PreSpec
+getNumber os i = oneLine t os i where
+  t = maybe empty L.text (Q.number . L.boxPostFam $ i)
+
+dcTxt :: L.DrCr -> Text
+dcTxt L.Debit = pack "Dr"
+dcTxt L.Credit = pack "Cr"
+
+-- | Gives a one-line cell that is colored according to whether the
+-- posting is a debit or credit.
+coloredPostingCell :: Text -> Options.T -> Box -> PreSpec
+coloredPostingCell t os i = PreSpec j ts [bit] where
+  j = R.LeftJustify
+  bit = C.chunk ts t
+  dc = Q.drCr . L.boxPostFam $ i
+  ts = PC.colors (M.visibleNum . L.boxMeta $ i)
+       . PC.drCrToBaseColors dc
+       . O.drCrColors
+       $ os
+
+getPostingDrCr :: Options.T -> Box -> PreSpec
+getPostingDrCr os i = coloredPostingCell t os i where
+  t = dcTxt . Q.drCr . L.boxPostFam $ i
+
+getPostingCmdty :: Options.T -> Box -> PreSpec
+getPostingCmdty os i = coloredPostingCell t os i where
+  t = L.text . L.Delimited (X.singleton ':') 
+      . L.textList . Q.commodity . L.boxPostFam $ i
+
+getPostingQty :: Options.T -> Box -> PreSpec
+getPostingQty os i = coloredPostingCell t os i where
+  t = O.qtyFormat os i
+
+getTotalDrCr :: Options.T -> Box -> PreSpec
+getTotalDrCr os i = let
+  vn = M.visibleNum . L.boxMeta $ i
+  ts = PC.colors vn bc
+  bc = PC.drCrToBaseColors dc (O.drCrColors os)
+  dc = Q.drCr . L.boxPostFam $ i
+  bits = case M.balance . L.boxMeta $ i of
+    Nothing -> let
+      spec = PC.noBalanceColors vn (O.drCrColors os)
+      in [C.chunk spec (pack "--")]
+    Just bal -> let
+      toBit bl = let
+        spec = 
+          PC.colors vn
+          . PC.bottomLineToBaseColors (O.drCrColors os)
+          $ bl
+        txt = case bl of
+          L.Zero -> pack "--"
+          L.NonZero (L.Column clmDrCr _) -> dcTxt clmDrCr
+        in C.chunk spec txt
+      in fmap toBit . elems . L.unBalance $ bal
+  j = R.LeftJustify
+  in PreSpec j ts bits
+
+getTotalCmdty :: Options.T -> Box -> PreSpec
+getTotalCmdty os i = let
+  vn = M.visibleNum . L.boxMeta $ i
+  j = R.RightJustify
+  ts = PC.colors vn bc
+  bc = PC.drCrToBaseColors dc (O.drCrColors os)
+  dc = Q.drCr . L.boxPostFam $ i
+  bits = case M.balance . L.boxMeta $ i of
+    Nothing -> let
+      spec = PC.noBalanceColors vn (O.drCrColors os)
+      in [C.chunk spec (pack "--")]
+    Just bal -> let
+      toBit (com, nou) = let
+        spec =
+          PC.colors vn
+          . PC.bottomLineToBaseColors (O.drCrColors os)
+          $ nou
+        txt = L.text
+              . L.Delimited (X.singleton ':')
+              . L.textList
+              $ com
+        in C.chunk spec txt
+      in fmap toBit . assocs . L.unBalance $ bal
+  in PreSpec j ts bits
+
+getTotalQty :: Options.T -> Box -> PreSpec
+getTotalQty os i = let
+  vn = M.visibleNum . L.boxMeta $ i
+  j = R.LeftJustify
+  ts = PC.colors vn bc
+  bc = PC.drCrToBaseColors dc (O.drCrColors os)
+  dc = Q.drCr . L.boxPostFam $ i
+  bits = case M.balance . L.boxMeta $ i of
+    Nothing -> let
+      spec = PC.noBalanceColors vn (O.drCrColors os)
+      in [C.chunk spec (pack "--")]
+    Just bal -> fmap toChunk . assocs . L.unBalance $ bal where
+      toChunk (com, nou) = let
+        spec = 
+          PC.colors vn
+          . PC.bottomLineToBaseColors (O.drCrColors os)
+          $ nou
+        txt = O.balanceFormat os com nou
+        in C.chunk spec txt
+  in PreSpec j ts bits
+
+growingFields :: Options.T -> Fields Bool
+growingFields o = let
+  f = O.fields o in Fields {
+    globalTransaction      = F.globalTransaction  f
+    , revGlobalTransaction = F.revGlobalTransaction  f
+    , globalPosting        = F.globalPosting      f
+    , revGlobalPosting     = F.revGlobalPosting   f
+    , fileTransaction      = F.fileTransaction    f
+    , revFileTransaction   = F.revFileTransaction f
+    , filePosting          = F.filePosting        f
+    , revFilePosting       = F.revFilePosting     f
+    , filtered             = F.filtered           f
+    , revFiltered          = F.revFiltered        f
+    , sorted               = F.sorted             f
+    , revSorted            = F.revSorted          f
+    , visible              = F.visible            f
+    , revVisible           = F.revVisible         f
+    , lineNum              = F.lineNum            f
+    , date                 = F.date               f
+    , flag                 = F.flag               f
+    , number               = F.number             f
+    , postingDrCr          = F.postingDrCr        f
+    , postingCmdty         = F.postingCmdty       f
+    , postingQty           = F.postingQty         f
+    , totalDrCr            = F.totalDrCr          f
+    , totalCmdty           = F.totalCmdty         f
+    , totalQty             = F.totalQty           f }
+
+-- | All growing fields, as an ADT.
+data EFields =
+  EGlobalTransaction
+  | ERevGlobalTransaction
+  | EGlobalPosting
+  | ERevGlobalPosting
+  | EFileTransaction
+  | ERevFileTransaction
+  | EFilePosting
+  | ERevFilePosting
+  | EFiltered
+  | ERevFiltered
+  | ESorted
+  | ERevSorted
+  | EVisible
+  | ERevVisible
+  | ELineNum
+  | EDate
+  | EFlag
+  | ENumber
+  | EPostingDrCr
+  | EPostingCmdty
+  | EPostingQty
+  | ETotalDrCr
+  | ETotalCmdty
+  | ETotalQty
+  deriving (Show, Eq, Ord, Enum)
+
+-- | Returns a Fields where each record has its corresponding EField.
+eFields :: Fields EFields
+eFields = Fields {
+  globalTransaction      = EGlobalTransaction
+  , revGlobalTransaction = ERevGlobalTransaction
+  , globalPosting        = EGlobalPosting
+  , revGlobalPosting     = ERevGlobalPosting
+  , fileTransaction      = EFileTransaction
+  , revFileTransaction   = ERevFileTransaction
+  , filePosting          = EFilePosting
+  , revFilePosting       = ERevFilePosting
+  , filtered             = EFiltered
+  , revFiltered          = ERevFiltered
+  , sorted               = ESorted
+  , revSorted            = ERevSorted
+  , visible              = EVisible
+  , revVisible           = ERevVisible
+  , lineNum              = ELineNum
+  , date                 = EDate
+  , flag                 = EFlag
+  , number               = ENumber
+  , postingDrCr          = EPostingDrCr
+  , postingCmdty         = EPostingCmdty
+  , postingQty           = EPostingQty
+  , totalDrCr            = ETotalDrCr
+  , totalCmdty           = ETotalCmdty
+  , totalQty             = ETotalQty }
+
+-- | All growing fields.
+data Fields a = Fields {
+  globalTransaction      :: a
+  , revGlobalTransaction :: a
+  , globalPosting        :: a
+  , revGlobalPosting     :: a
+  , fileTransaction      :: a
+  , revFileTransaction   :: a
+  , filePosting          :: a
+  , revFilePosting       :: a
+  , filtered             :: a
+  , revFiltered          :: a
+  , sorted               :: a
+  , revSorted            :: a
+  , visible              :: a
+  , revVisible           :: a
+  , lineNum              :: a
+    -- ^ The line number from the posting's metadata
+  , date                 :: a
+  , flag                 :: a
+  , number               :: a
+  , postingDrCr          :: a
+  , postingCmdty         :: a
+  , postingQty           :: a
+  , totalDrCr            :: a
+  , totalCmdty           :: a
+  , totalQty             :: a }
+  deriving (Show, Eq)
+
+instance Fdbl.Foldable Fields where
+  foldr f z i =
+    f (globalTransaction i)
+    (f (revGlobalTransaction i)
+     (f (globalPosting i)
+      (f (revGlobalPosting i)
+       (f (fileTransaction i)
+        (f (revFileTransaction i)
+         (f (filePosting i)
+          (f (revFilePosting i)
+           (f (filtered i)
+            (f (revFiltered i)
+             (f (sorted i)
+              (f (revSorted i)
+               (f (visible i)
+                (f (revVisible i)
+                 (f (lineNum i)
+                  (f (date i)
+                   (f (flag i)
+                    (f (number i)
+                     (f (postingDrCr i)
+                      (f (postingCmdty i)
+                       (f (postingQty i)
+                        (f (totalDrCr i)
+                         (f (totalCmdty i)
+                          (f (totalQty i) z)))))))))))))))))))))))
+
+instance Functor Fields where
+  fmap f i = Fields {
+    globalTransaction      = f (globalTransaction    i)
+    , revGlobalTransaction = f (revGlobalTransaction i)
+    , globalPosting        = f (globalPosting        i)
+    , revGlobalPosting     = f (revGlobalPosting     i)
+    , fileTransaction      = f (fileTransaction      i)
+    , revFileTransaction   = f (revFileTransaction   i)
+    , filePosting          = f (filePosting          i)
+    , revFilePosting       = f (revFilePosting       i)
+    , filtered             = f (filtered             i)
+    , revFiltered          = f (revFiltered          i)
+    , sorted               = f (sorted               i)
+    , revSorted            = f (revSorted            i)
+    , visible              = f (visible              i)
+    , revVisible           = f (revVisible           i)
+    , lineNum              = f (lineNum              i)
+    , date                 = f (date                 i)
+    , flag                 = f (flag                 i)
+    , number               = f (number               i)
+    , postingDrCr          = f (postingDrCr          i)
+    , postingCmdty         = f (postingCmdty         i)
+    , postingQty           = f (postingQty           i)
+    , totalDrCr            = f (totalDrCr            i)
+    , totalCmdty           = f (totalCmdty           i)
+    , totalQty             = f (totalQty             i) }
+
+instance Applicative Fields where
+  pure a = Fields {
+    globalTransaction      = a
+    , revGlobalTransaction = a
+    , globalPosting        = a
+    , revGlobalPosting     = a
+    , fileTransaction      = a
+    , revFileTransaction   = a
+    , filePosting          = a
+    , revFilePosting       = a
+    , filtered             = a
+    , revFiltered          = a
+    , sorted               = a
+    , revSorted            = a
+    , visible              = a
+    , revVisible           = a
+    , lineNum              = a
+    , date                 = a
+    , flag                 = a
+    , number               = a
+    , postingDrCr          = a
+    , postingCmdty         = a
+    , postingQty           = a
+    , totalDrCr            = a
+    , totalCmdty           = a
+    , totalQty             = a }
+
+  fl <*> fa = Fields {
+    globalTransaction      = globalTransaction    fl (globalTransaction    fa)
+    , revGlobalTransaction = revGlobalTransaction fl (revGlobalTransaction fa)
+    , globalPosting        = globalPosting        fl (globalPosting        fa)
+    , revGlobalPosting     = revGlobalPosting     fl (revGlobalPosting     fa)
+    , fileTransaction      = fileTransaction      fl (fileTransaction      fa)
+    , revFileTransaction   = revFileTransaction   fl (revFileTransaction   fa)
+    , filePosting          = filePosting          fl (filePosting          fa)
+    , revFilePosting       = revFilePosting       fl (revFilePosting       fa)
+    , filtered             = filtered             fl (filtered             fa)
+    , revFiltered          = revFiltered          fl (revFiltered          fa)
+    , sorted               = sorted               fl (sorted               fa)
+    , revSorted            = revSorted            fl (revSorted            fa)
+    , visible              = visible              fl (visible              fa)
+    , revVisible           = revVisible           fl (revVisible           fa)
+    , lineNum              = lineNum              fl (lineNum              fa)
+    , date                 = date                 fl (date                 fa)
+    , flag                 = flag                 fl (flag                 fa)
+    , number               = number               fl (number               fa)
+    , postingDrCr          = postingDrCr          fl (postingDrCr          fa)
+    , postingCmdty         = postingCmdty         fl (postingCmdty         fa)
+    , postingQty           = postingQty           fl (postingQty           fa)
+    , totalDrCr            = totalDrCr            fl (totalDrCr            fa)
+    , totalCmdty           = totalCmdty           fl (totalCmdty           fa)
+    , totalQty             = totalQty             fl (totalQty             fa) }
+
+-- | Pairs data from a Fields with its matching spacer field. The
+-- spacer field is returned in a Maybe because the TotalQty field does
+-- not have a spacer.
+pairWithSpacer :: Fields a -> Spacers.T b -> Fields (a, Maybe b)
+pairWithSpacer f s = Fields {
+  globalTransaction      = (globalTransaction    f, Just (S.globalTransaction    s))
+  , revGlobalTransaction = (revGlobalTransaction f, Just (S.revGlobalTransaction s))
+  , globalPosting        = (globalPosting        f, Just (S.globalPosting        s))
+  , revGlobalPosting     = (revGlobalPosting     f, Just (S.revGlobalPosting     s))
+  , fileTransaction      = (fileTransaction      f, Just (S.fileTransaction      s))
+  , revFileTransaction   = (revFileTransaction   f, Just (S.revFileTransaction   s))
+  , filePosting          = (filePosting          f, Just (S.filePosting          s))
+  , revFilePosting       = (revFilePosting       f, Just (S.revFilePosting       s))
+  , filtered             = (filtered             f, Just (S.filtered             s))
+  , revFiltered          = (revFiltered          f, Just (S.revFiltered          s))
+  , sorted               = (sorted               f, Just (S.sorted               s))
+  , revSorted            = (revSorted            f, Just (S.revSorted            s))
+  , visible              = (visible              f, Just (S.visible              s))
+  , revVisible           = (revVisible           f, Just (S.revVisible           s))
+  , lineNum              = (lineNum              f, Just (S.lineNum              s))
+  , date                 = (date                 f, Just (S.date                 s))
+  , flag                 = (flag                 f, Just (S.flag                 s))
+  , number               = (number               f, Just (S.number               s))
+  , postingDrCr          = (postingDrCr          f, Just (S.postingDrCr          s))
+  , postingCmdty         = (postingCmdty         f, Just (S.postingCmdty         s))
+  , postingQty           = (postingQty           f, Just (S.postingQty           s))
+  , totalDrCr            = (totalDrCr            f, Just (S.totalDrCr            s))
+  , totalCmdty           = (totalCmdty           f, Just (S.totalCmdty           s))
+  , totalQty             = (totalQty             f, Nothing                        ) }
+
+-- | Reduces a set of Fields to a single value.
+reduce :: Semi.Semigroup s => Fields s -> s
+reduce f =
+  globalTransaction       f
+  <> revGlobalTransaction f
+  <> globalPosting        f
+  <> revGlobalPosting     f
+  <> fileTransaction      f
+  <> revFileTransaction   f
+  <> filePosting          f
+  <> revFilePosting       f
+  <> filtered             f
+  <> revFiltered          f
+  <> sorted               f
+  <> revSorted            f
+  <> visible              f
+  <> revVisible           f
+  <> lineNum              f
+  <> date                 f
+  <> flag                 f
+  <> number               f
+  <> postingDrCr          f
+  <> postingCmdty         f
+  <> postingQty           f
+  <> totalDrCr            f
+  <> totalCmdty           f
+  <> totalQty             f
+
+-- | Compute the width of all Grown cells, including any applicable
+-- spacer cells.
+grownWidth ::
+  Fields (Maybe Int)
+  -> Spacers.T Int
+  -> Int
+grownWidth fs ss =
+  Semi.getSum
+  . reduce
+  . fmap Semi.Sum
+  . fmap fieldWidth
+  $ pairWithSpacer fs ss
+
+-- | Compute the field width of a single field and its spacer. The
+-- first element of the tuple is the field width, if present; the
+-- second element of the tuple is the width of the spacer. If there is
+-- no field, returns 0.
+fieldWidth :: (Maybe Int, Maybe Int) -> Int
+fieldWidth (m1, m2) = case m1 of
+  Nothing -> 0
+  Just i1 -> case m2 of
+    Just i2 -> if i2 > 0 then i1 + i2 else i1
+    Nothing -> i1
+
diff --git a/Penny/Cabin/Posts/Help.hs b/Penny/Cabin/Posts/Help.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts/Help.hs
@@ -0,0 +1,66 @@
+module Penny.Cabin.Posts.Help where
+
+import qualified Data.Text as X
+
+help :: X.Text
+help = X.pack helpStr
+
+helpStr :: String
+helpStr = unlines [
+  "postings, pos",
+  "  Show postings in order with a running balance.",
+  "  The postings report takes the same options shown",
+  "  from above in the categories \"Posting filters\" to",
+  "  \"Removing postings after sorting and filtering\",",
+  "  with the options affecting which postings are shown on screen.",
+  "  Postings not shown still affect the running balance.",
+  "",
+  "  Additional sequence number filtering options",
+  "    Like the \"Sequence numbers\" options above, these",
+  "    options take the form --option cmp num.",
+  "",
+  "    --filtered, --revFiltered",
+  "      All postings, after filters given in the filter",
+  "      specification portion of the command line are",
+  "      applied",
+  "    --sorted, --revSorted",
+  "      All postings remaining after filtering and after",
+  "      postings have been sorted",
+  "",
+  "  Other additional options:",
+  "",
+  "  --color yes|no|auto|256",
+  "    yes: show 8 colors always",
+  "    no: never show colors",
+  "    auto: show 8 or 256 colors, but only if stdout is a terminal",
+  "    256: show 256 colors always",
+  "  --background light|dark",
+  "    Use appropriate color scheme for terminal background",
+  "",
+  "  --width num",
+  "    Hint for roughly how wide the report should be in columns",
+  "  --show field, --hide field",
+  "    show or hide this field, where field is one of:",
+  "      globalTransaction, revGlobalTransaction,",
+  "      globalPosting, revGlobalPosting,",
+  "      fileTransaction, revFileTransaction,",
+  "      filePosting, revFilePosting,",
+  "      filtered, revFiltered,",
+  "      sorted, revSorted,",
+  "      visible, revVisible,",
+  "      lineNum,",
+  "      date, flag, number, payee, account,",
+  "      postingDrCr, postingCommodity, postingQty,",
+  "      totalDrCr, totalCommodity, totalQty,",
+  "      tags, memo, filename",
+  "  --show-all",
+  "    Show all fields",
+  "  --hide-all",
+  "    Hide all fields",
+  "",
+  "  --show-zero-balances",
+  "    Show balances that are zero",
+  "  --hide-zero-balances",
+  "    Hide balances that are zero",
+  ""
+  ]
diff --git a/Penny/Cabin/Posts/Meta.hs b/Penny/Cabin/Posts/Meta.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts/Meta.hs
@@ -0,0 +1,70 @@
+module Penny.Cabin.Posts.Meta (
+  M.VisibleNum(M.unVisibleNum)
+  , PostMeta(filteredNum, sortedNum, visibleNum, balance)
+  , filterBoxes
+  , addMetadata
+  ) where
+
+import Data.List (mapAccumL)
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.Queries as Q
+import qualified Penny.Liberty as Ly
+import qualified Penny.Liberty.Expressions as Exp
+import qualified Penny.Cabin.Meta as M
+import qualified Penny.Cabin.Options as CO
+
+data PostMeta =
+  PostMeta { filteredNum :: Ly.FilteredNum
+            , sortedNum :: Ly.SortedNum
+            , visibleNum :: M.VisibleNum
+            , balance :: Maybe L.Balance }
+  deriving Show
+
+
+-- | Takes a list of Box with LibertyMeta, and a list of tokens from
+-- the command line, and a list of post filters. Filters the list of
+-- Box and processes the post filter. Fails if the expression fails to
+-- produce a result.
+filterBoxes ::
+  [Exp.Token (L.Box Ly.LibertyMeta -> Bool)]
+  -> [Ly.PostFilterFn]
+  -> [L.Box Ly.LibertyMeta]
+  -> Maybe [L.Box Ly.LibertyMeta]
+filterBoxes tks pfs bs = fmap fltr (Ly.parsePredicate tks)
+  where
+    fltr p =
+      Ly.processPostFilters pfs . filter p $ bs
+
+
+
+-- | Applied to a list of Box that have already been filtered, returns
+-- a list of Box with posting metadata.
+addMetadata ::
+  CO.ShowZeroBalances
+  -> [L.Box Ly.LibertyMeta]
+  -> [L.Box PostMeta]
+addMetadata szb = M.visibleNums f . addBalances szb where
+  f vn (lm, mb) =
+    PostMeta (Ly.filteredNum lm) (Ly.sortedNum lm) vn mb
+
+addBalances ::
+  CO.ShowZeroBalances
+  -> [L.Box Ly.LibertyMeta]
+  -> [(L.Box (Ly.LibertyMeta, Maybe L.Balance))]
+addBalances szb = snd . mapAccumL (balanceAccum szb) Nothing
+
+balanceAccum :: 
+  CO.ShowZeroBalances
+  -> Maybe L.Balance
+  -> L.Box Ly.LibertyMeta
+  -> (Maybe L.Balance, (L.Box (Ly.LibertyMeta, Maybe L.Balance)))
+balanceAccum (CO.ShowZeroBalances szb) mb po =
+  let balThis = L.entryToBalance . Q.entry . L.boxPostFam $ po
+      balNew = case mb of
+        Nothing -> balThis
+        Just balOld -> L.addBalances balOld balThis
+      balNoZeroes = L.removeZeroCommodities balNew
+      bal' = if szb then Just balNew else balNoZeroes
+      po' = L.Box (L.boxMeta po, bal') (L.boxPostFam po)
+  in (bal', po')
+
diff --git a/Penny/Cabin/Posts/Options.hs b/Penny/Cabin/Posts/Options.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts/Options.hs
@@ -0,0 +1,247 @@
+-- | Options for the Posts report.
+module Penny.Cabin.Posts.Options where
+
+
+import Control.Monad.Exception.Synchronous (Exceptional)
+import Data.Text (Text)
+import qualified Data.Text as X
+import Data.Time (formatTime)
+import System.Locale (defaultTimeLocale)
+import qualified Data.Time as Time
+import qualified Text.Matchers.Text as M
+
+import qualified Penny.Liberty as Ly
+import qualified Penny.Liberty.Expressions as Ex
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.Balance as Bal
+import qualified Penny.Lincoln.Bits as Bits
+import qualified Penny.Lincoln.Queries as Q
+import qualified Penny.Cabin.Posts.Allocate as A
+import qualified Penny.Cabin.Chunk as CC
+import qualified Penny.Cabin.Colors as C
+import qualified Penny.Cabin.Options as O
+import qualified Penny.Cabin.Posts.Fields as Fields
+import qualified Penny.Cabin.Posts.Fields as F
+import qualified Penny.Cabin.Posts.Meta as Meta
+import qualified Penny.Cabin.Posts.Spacers as S
+import qualified Penny.Cabin.Posts.Spacers as Spacers
+import qualified Penny.Cabin.Colors.DarkBackground as Dark
+import Penny.Copper.DateTime (DefaultTimeZone)
+import Penny.Copper.Qty (RadGroup)
+import qualified Penny.Shield as S
+
+
+type Box = L.Box Meta.PostMeta
+
+data T =
+  T { drCrColors :: C.DrCrColors
+      -- ^ Colors to use when displaying debits, credits, and
+      -- when displaying balance totals
+
+    , baseColors :: C.BaseColors 
+      -- ^ Colors to use when displaying everything else
+
+    , dateFormat :: Box -> X.Text
+      -- ^ How to display dates. This function is applied to the
+      -- a PostingInfo so it has lots of information, but it
+      -- should return a date for use in the Date field.
+      
+    , qtyFormat :: Box -> X.Text
+      -- ^ How to display the quantity of the posting. This
+      -- function is applied to a PostingInfo so it has lots of
+      -- information, but it should return a formatted string of
+      -- the quantity. Allows you to format digit grouping,
+      -- radix points, perform rounding, etc.
+      
+    , balanceFormat :: Bits.Commodity -> Bal.BottomLine -> X.Text
+      -- ^ How to display balance totals. Similar to
+      -- balanceFormat.
+      
+    , payeeAllocation :: A.Allocation
+      -- ^ This and accountAllocation determine how much space
+      -- payees and accounts receive. They divide up the
+      -- remaining space after everything else is displayed. For
+      -- instance if payeeAllocation is 60 and accountAllocation
+      -- is 40, the payee takes about 60 percent of the
+      -- remaining space and the account takes about 40 percent.
+      
+    , accountAllocation :: A.Allocation 
+      -- ^ See payeeAllocation above
+
+    , width :: ReportWidth
+      -- ^ Gives the default report width. This can be
+      -- overridden on the command line. You can use the
+      -- information from the Runtime to make this as wide as
+      -- the as the current terminal.
+
+    , subAccountLength :: Int
+      -- ^ When shortening the names of sub accounts to make
+      -- them fit, they will be this long.
+
+    , colorPref :: CC.Colors
+      -- ^ How many colors you want to see, or do it
+      -- automatically.
+
+    , timeZone :: DefaultTimeZone
+      -- ^ When dates and times are given on the command line
+      -- and they have no time zone, they are assumed to be in
+      -- this time zone. This has no bearing on how dates are
+      -- formatted in the output; for that, see dateFormat
+      -- above.
+
+    , radGroup :: RadGroup
+      -- ^ The characters used for the radix point for numbers
+      -- given on the command line (e.g. a full stop, or a
+      -- comma) and for the digit group separator for
+      -- numbers parsed from the command line (e.g. a full stop,
+      -- or a comma). Affects how inputs are parsed. Has no bearing
+      -- on how output is formatted; for that, see qtyFormat and
+      -- balanceFormat above.
+      
+    , sensitive :: M.CaseSensitive
+      -- ^ Whether pattern matches are case sensitive by default.
+      
+    , factory :: M.CaseSensitive
+                 -> Text -> Exceptional Text (Text -> Bool)
+      -- ^ Default factory for pattern matching
+      
+    , tokens :: [Ex.Token (L.Box Ly.LibertyMeta -> Bool)]
+      -- ^ Default list of tokens used to filter postings.
+      
+    , postFilter :: [Ly.PostFilterFn]
+      -- ^ The entire posting list is transformed by this
+      -- function after it is filtered according to the tokens.
+      
+    , fields :: Fields.T Bool
+      -- ^ Default fields to show in the report.
+      
+    , spacers :: Spacers.T Int
+      -- ^ Default width for spacer fields. If any of these Ints are
+      -- less than or equal to zero, there will be no spacer. There is
+      -- never a spacer for fields that do not appear in the report.
+      
+    , showZeroBalances :: O.ShowZeroBalances
+    }
+
+newtype ReportWidth = ReportWidth { unReportWidth :: Int }
+                      deriving (Eq, Show, Ord)
+
+ymd :: Box -> X.Text
+ymd p = X.pack (formatTime defaultTimeLocale fmt d) where
+  d = Time.localDay
+      . Bits.localTime
+      . Q.dateTime
+      . L.boxPostFam
+      $ p
+  fmt = "%Y-%m-%d"
+
+qtyAsIs :: Box -> X.Text
+qtyAsIs p = X.pack . show . Bits.unQty . Q.qty . L.boxPostFam $ p
+
+balanceAsIs :: Bits.Commodity -> Bal.BottomLine -> X.Text
+balanceAsIs _ n = case n of
+  Bal.Zero -> X.pack "--"
+  Bal.NonZero c -> X.pack . show . Bits.unQty . Bal.qty $ c
+
+defaultWidth :: ReportWidth
+defaultWidth = ReportWidth 80
+
+columnsVarToWidth :: Maybe String -> ReportWidth
+columnsVarToWidth ms = case ms of
+  Nothing -> defaultWidth
+  Just str -> case reads str of
+    [] -> defaultWidth
+    (i, []):[] -> if i > 0 then ReportWidth i else defaultWidth
+    _ -> defaultWidth
+
+defaultOptions ::
+  DefaultTimeZone
+  -> RadGroup
+  -> S.Runtime
+  -> T
+defaultOptions dtz rg rt =
+  T { drCrColors = Dark.drCrColors
+    , baseColors = Dark.baseColors
+    , dateFormat = ymd
+    , qtyFormat = qtyAsIs
+    , balanceFormat = balanceAsIs
+    , payeeAllocation = A.allocation 40
+    , accountAllocation = A.allocation 60
+    , width = widthFromRuntime rt
+    , subAccountLength = 2
+    , colorPref = O.autoColors O.PrefAuto rt
+    , timeZone = dtz
+    , radGroup = rg
+    , sensitive = M.Insensitive
+    , factory = \s t -> (return (M.within s t))
+    , tokens = []
+    , postFilter = []
+    , fields = defaultFields
+    , spacers = defaultSpacerWidth
+    , showZeroBalances = O.ShowZeroBalances False }
+
+widthFromRuntime :: S.Runtime -> ReportWidth
+widthFromRuntime rt = case S.screenWidth rt of
+  Nothing -> defaultWidth
+  Just w -> ReportWidth . S.unScreenWidth $ w
+
+defaultFields :: Fields.T Bool
+defaultFields =
+  Fields.T { F.globalTransaction    = False
+           , F.revGlobalTransaction = False
+           , F.globalPosting        = False
+           , F.revGlobalPosting     = False
+           , F.fileTransaction      = False
+           , F.revFileTransaction   = False
+           , F.filePosting          = False
+           , F.revFilePosting       = False
+           , F.filtered             = False
+           , F.revFiltered          = False
+           , F.sorted               = False
+           , F.revSorted            = False
+           , F.visible              = False
+           , F.revVisible           = False
+           , F.lineNum              = False
+           , F.date                 = True
+           , F.flag                 = False
+           , F.number               = False
+           , F.payee                = True
+           , F.account              = True
+           , F.postingDrCr          = True
+           , F.postingCmdty         = True
+           , F.postingQty           = True
+           , F.totalDrCr            = True
+           , F.totalCmdty           = True
+           , F.totalQty             = True
+           , F.tags                 = False
+           , F.memo                 = False
+           , F.filename             = False }
+
+defaultSpacerWidth :: Spacers.T Int
+defaultSpacerWidth =
+  Spacers.T { S.globalTransaction    = 1
+            , S.revGlobalTransaction = 1
+            , S.globalPosting        = 1
+            , S.revGlobalPosting     = 1
+            , S.fileTransaction      = 1
+            , S.revFileTransaction   = 1
+            , S.filePosting          = 1
+            , S.revFilePosting       = 1
+            , S.filtered             = 1
+            , S.revFiltered          = 1
+            , S.sorted               = 1
+            , S.revSorted            = 1
+            , S.visible              = 1
+            , S.revVisible           = 1
+            , S.lineNum              = 1
+            , S.date                 = 1
+            , S.flag                 = 1
+            , S.number               = 1
+            , S.payee                = 4
+            , S.account              = 1
+            , S.postingDrCr          = 1
+            , S.postingCmdty         = 1
+            , S.postingQty           = 1
+            , S.totalDrCr            = 1
+            , S.totalCmdty           = 1 }
+
diff --git a/Penny/Cabin/Posts/Parser.hs b/Penny/Cabin/Posts/Parser.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts/Parser.hs
@@ -0,0 +1,326 @@
+module Penny.Cabin.Posts.Parser (parseOptions) where
+
+import Control.Applicative ((<|>), (<$>), pure, many, (<*>))
+import Control.Monad ((>=>))
+import qualified Control.Monad.Exception.Synchronous as Ex
+import Data.Char (toLower)
+import qualified Data.Foldable as F
+import qualified System.Console.MultiArg.Combinator as C
+import System.Console.MultiArg.Prim (Parser)
+
+import qualified Penny.Cabin.Chunk as CC
+import qualified Penny.Cabin.Colors as PC
+import qualified Penny.Cabin.Posts.Fields as Fl
+import qualified Penny.Cabin.Posts.Options as Op
+import qualified Penny.Cabin.Colors.DarkBackground as DB
+import qualified Penny.Cabin.Colors.LightBackground as LB
+import qualified Penny.Cabin.Options as CO
+import qualified Penny.Liberty as Ly
+import qualified Penny.Liberty.Expressions as Exp
+import qualified Penny.Lincoln as L
+import qualified Penny.Shield as S
+
+data Error = BadColorName String
+             | BadBackgroundArg String
+             | BadWidthArg String
+             | NoMatchingFieldName
+             | MultipleMatchingFieldNames [String]
+             | LibertyError Ly.Error
+             | BadNumber String
+             | BadComparator String
+             deriving Show
+
+-- | Parses the command line from the first word remaining up until,
+-- but not including, the first non-option argment.
+parseOptions ::
+  Parser (S.Runtime -> Op.T -> Ex.Exceptional Error Op.T)
+parseOptions = f <$> many parseOption where
+  f ls =
+    let g rt op =
+          let ls' = map (\fn -> fn rt) ls
+          in (foldl (>=>) return ls') op
+    in g
+
+
+parseOption ::
+  Parser (S.Runtime -> Op.T -> Ex.Exceptional Error Op.T)
+parseOption =
+  operand
+  <|> mkTwoArg boxFilters
+  <|> mkTwoArg postFilter
+  <|> mkTwoArg matcherSelect
+  <|> mkTwoArg caseSelect
+  <|> mkTwoArg operator
+  <|> color
+  <|> mkTwoArg background
+  <|> mkTwoArg width
+  <|> mkTwoArg showField
+  <|> mkTwoArg hideField
+  <|> mkTwoArg showAllFields
+  <|> mkTwoArg hideAllFields
+  <|> mkTwoArg showZeroBalances
+  <|> mkTwoArg hideZeroBalances
+  where
+    mkTwoArg p = do
+      f <- p
+      return (\_ o -> f o)
+
+operand :: Parser (S.Runtime -> Op.T -> Ex.Exceptional Error Op.T)
+operand = f <$> Ly.parseOperand
+  where
+    f lyFn rt op =
+      let dtz = Op.timeZone op
+          rg = Op.radGroup op
+          dt = S.currentTime rt
+          cs = Op.sensitive op
+          fty = Op.factory op
+      in case lyFn dt dtz rg cs fty of
+        Ex.Exception e -> Ex.throw . LibertyError $ e
+        Ex.Success (Exp.Operand g) ->
+          let g' = g . L.boxPostFam
+              ts' = Op.tokens op ++ [Exp.TokOperand g']
+          in return op { Op.tokens = ts' }
+
+-- | Processes a option for box-level serials.
+optBoxSerial ::
+  [String]
+  -- ^ Long options
+  
+  -> [Char]
+  -- ^ Short options
+  
+  -> (Ly.LibertyMeta -> Int)
+  -- ^ Pulls the serial from the PostMeta
+  
+  -> Parser (Op.T -> Ex.Exceptional Error Op.T)
+
+optBoxSerial ls ss f = parseOpt ls ss (C.TwoArg g)
+  where
+    g a1 a2 op = do
+      cmp <- Ex.fromMaybe (BadComparator a1) (Ly.parseComparer a1)
+      i <- parseInt a2
+      let h box =
+            let ser = f . L.boxMeta $ box
+            in ser `cmp` i
+          tok = Exp.TokOperand h
+      return op { Op.tokens = Op.tokens op ++ [tok] }
+
+optFilteredNum :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+optFilteredNum = optBoxSerial ["filtered"] "" f
+  where
+    f = L.forward . Ly.unFilteredNum . Ly.filteredNum
+
+optRevFilteredNum :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+optRevFilteredNum = optBoxSerial ["revFiltered"] "" f
+  where
+    f = L.backward . Ly.unFilteredNum . Ly.filteredNum
+
+optSortedNum :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+optSortedNum = optBoxSerial ["sorted"] "" f
+  where
+    f = L.forward . Ly.unSortedNum . Ly.sortedNum
+
+optRevSortedNum :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+optRevSortedNum = optBoxSerial ["revSorted"] "" f
+  where
+    f = L.backward . Ly.unSortedNum . Ly.sortedNum
+
+parseInt :: String -> Ex.Exceptional Error Int
+parseInt s = case reads s of
+  (i, ""):[] -> return i
+  _ -> Ex.throw . BadNumber $ s
+
+boxFilters :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+boxFilters =
+  optFilteredNum
+  <|> optRevFilteredNum
+  <|> optSortedNum
+  <|> optRevSortedNum
+
+
+postFilter :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+postFilter = f <$> Ly.parsePostFilter
+  where
+    f ex op =
+      case ex of
+        Ex.Exception e -> Ex.throw . LibertyError $ e
+        Ex.Success pf ->
+          return op { Op.postFilter = Op.postFilter op ++ [pf] }
+
+matcherSelect :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+matcherSelect = f <$> Ly.parseMatcherSelect
+  where
+    f mf op = return op { Op.factory = mf }
+
+caseSelect :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+caseSelect = f <$> Ly.parseCaseSelect
+  where
+    f cs op = return op { Op.sensitive = cs }
+
+operator :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+operator = f <$> Ly.parseOperator
+  where
+    f oo op = return op { Op.tokens = Op.tokens op ++ [oo] }
+
+parseOpt :: [String] -> [Char] -> C.ArgSpec a -> Parser a
+parseOpt ss cs a = C.parseOption [C.OptSpec ss cs a]
+
+color :: Parser (S.Runtime -> Op.T -> Ex.Exceptional Error Op.T)
+color = parseOpt ["color"] "" (C.OneArg f)
+  where
+    f a1 rt op = case pickColorArg rt a1 of
+      Nothing -> Ex.throw . BadColorName $ a1
+      Just c -> return (op { Op.colorPref = c })
+
+pickColorArg :: S.Runtime -> String -> Maybe CC.Colors
+pickColorArg rt t
+  | t == "yes" = Just CC.Colors8
+  | t == "no" = Just CC.Colors0
+  | t == "256" = Just CC.Colors256
+  | t == "auto" = Just . CO.maxCapableColors $ rt
+  | otherwise = Nothing
+
+pickBackgroundArg :: String -> Maybe (PC.DrCrColors, PC.BaseColors)
+pickBackgroundArg t
+  | t == "light" = Just (LB.drCrColors, LB.baseColors)
+  | t == "dark" = Just (DB.drCrColors, DB.baseColors)
+  | otherwise = Nothing
+
+
+background :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+background = parseOpt ["background"] "" (C.OneArg f)
+  where
+    f a1 op = case pickBackgroundArg a1 of
+      Nothing -> Ex.throw . BadBackgroundArg $ a1
+      Just (dc, bc) -> return (op { Op.drCrColors = dc
+                                  , Op.baseColors = bc } )
+
+
+width :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+width = parseOpt ["width"] "" (C.OneArg f)
+  where
+    f a1 op = case reads a1 of
+      (i, ""):[] -> return (op { Op.width = Op.ReportWidth i })
+      _ -> Ex.throw . BadWidthArg $ a1
+
+showField :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+showField = parseOpt ["show"] "" (C.OneArg f)
+  where
+    f a1 op = do
+      fl <- parseField a1
+      let newFl = fieldOn (Op.fields op) fl
+      return op { Op.fields = newFl }
+
+hideField :: Parser (Op.T -> Ex.Exceptional Error Op.T)
+hideField = parseOpt ["hide"] "" (C.OneArg f)
+  where
+    f a1 op = do
+      fl <- parseField a1
+      let newFl = fieldOff (Op.fields op) fl
+      return op { Op.fields = newFl }
+
+showAllFields :: Parser (Op.T -> Ex.Exceptional a Op.T)
+showAllFields = parseOpt ["show-all"] "" (C.NoArg f)
+  where
+    f op = return (op {Op.fields = pure True})
+
+hideAllFields :: Parser (Op.T -> Ex.Exceptional a Op.T)
+hideAllFields = parseOpt ["hide-all"] "" (C.NoArg f)
+  where
+    f op = return (op {Op.fields = pure False})
+
+showZeroBalances :: Parser (Op.T -> Ex.Exceptional a Op.T)
+showZeroBalances = parseOpt ["show-zero-balances"] "" (C.NoArg f)
+  where
+    f op =
+      return (op {Op.showZeroBalances = CO.ShowZeroBalances True })
+
+hideZeroBalances :: Parser (Op.T -> Ex.Exceptional a Op.T)
+hideZeroBalances = parseOpt ["hide-zero-balances"] "" (C.NoArg f)
+  where
+    f op =
+      return (op {Op.showZeroBalances = CO.ShowZeroBalances False })
+
+-- | Turns a field on if it is True.
+fieldOn ::
+  Fl.T Bool
+  -- ^ Fields as seen so far
+
+  -> Fl.T Bool
+  -- ^ Record that should have one True element indicating a field
+  -- name seen on the command line; other elements should be False
+  
+  -> Fl.T Bool
+  -- ^ Fields as seen so far, with new field added
+
+fieldOn old new = (||) <$> old <*> new
+
+-- | Turns off a field if it is True.
+fieldOff ::
+  Fl.T Bool
+  -- ^ Fields seen so far
+  
+  -> Fl.T Bool
+  -- ^ Record that should have one True element indicating a field
+  -- name seen on the command line; other elements should be False
+  
+  -> Fl.T Bool
+  -- ^ Fields as seen so far, with new field added
+
+fieldOff old new = f <$> old <*> new
+  where
+    f o False = o
+    f _ True = False
+
+parseField :: String -> Ex.Exceptional Error (Fl.T Bool)
+parseField str =
+  let lower = map toLower str
+      checkField s =
+        if (map toLower s) == lower
+        then (s, True)
+        else (s, False)
+      flds = checkField <$> fieldNames
+  in checkFields flds
+
+-- | Checks the fields with the True value to ensure there is only one.
+checkFields :: Fl.T (String, Bool) -> Ex.Exceptional Error (Fl.T Bool)
+checkFields fs =
+  let f (s, b) ls = if b then s:ls else ls
+  in case F.foldr f [] fs of
+    [] -> Ex.throw NoMatchingFieldName
+    _:[] -> return (snd <$> fs)
+    ls -> Ex.throw . MultipleMatchingFieldNames $ ls
+
+
+
+fieldNames :: Fl.T String
+fieldNames = Fl.T {
+  Fl.globalTransaction = "globalTransaction"
+  , Fl.revGlobalTransaction = "revGlobalTransaction"
+  , Fl.globalPosting = "globalPosting"
+  , Fl.revGlobalPosting = "revGlobalPosting"
+  , Fl.fileTransaction = "fileTransaction"
+  , Fl.revFileTransaction = "revFileTransaction"
+  , Fl.filePosting = "filePosting"
+  , Fl.revFilePosting = "revFilePosting"
+  , Fl.filtered = "filtered"
+  , Fl.revFiltered = "revFiltered"
+  , Fl.sorted = "sorted"
+  , Fl.revSorted = "revSorted"
+  , Fl.visible = "visible"
+  , Fl.revVisible = "revVisible"
+  , Fl.lineNum = "lineNum"
+  , Fl.date = "date"
+  , Fl.flag = "flag"
+  , Fl.number = "number"
+  , Fl.payee = "payee"
+  , Fl.account = "account"
+  , Fl.postingDrCr = "postingDrCr"
+  , Fl.postingCmdty = "postingCmdty"
+  , Fl.postingQty = "postingQty"
+  , Fl.totalDrCr = "totalDrCr"
+  , Fl.totalCmdty = "totalCmdty"
+  , Fl.totalQty = "totalQty"
+  , Fl.tags = "tags"
+  , Fl.memo = "memo"
+  , Fl.filename = "filename" }
diff --git a/Penny/Cabin/Posts/Spacers.hs b/Penny/Cabin/Posts/Spacers.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Posts/Spacers.hs
@@ -0,0 +1,108 @@
+-- | Spacer fields in the report. They don't contain any data; they
+-- just provide whitespace. Each spacer immediately follows the named
+-- field.
+module Penny.Cabin.Posts.Spacers where
+
+data T a = T {
+  globalTransaction :: a
+  , revGlobalTransaction :: a
+  , globalPosting :: a
+  , revGlobalPosting :: a
+  , fileTransaction :: a
+  , revFileTransaction :: a
+  , filePosting :: a
+  , revFilePosting :: a
+  , filtered :: a
+  , revFiltered :: a
+  , sorted :: a
+  , revSorted :: a
+  , visible :: a
+  , revVisible :: a
+  , lineNum :: a
+    -- ^ The line number from the posting's metadata
+  , date :: a
+  , flag :: a
+  , number :: a
+  , payee :: a
+  , account :: a
+  , postingDrCr :: a
+  , postingCmdty :: a
+  , postingQty :: a
+  , totalDrCr :: a
+  , totalCmdty :: a }
+  deriving (Show, Eq)
+
+t_globalTransaction :: a -> T a -> T a
+t_globalTransaction a f = f { globalTransaction = a }
+
+t_revGlobalTransaction :: a -> T a -> T a
+t_revGlobalTransaction a f = f { revGlobalTransaction = a }
+
+t_globalPosting :: a -> T a -> T a
+t_globalPosting a f = f { globalPosting = a }
+
+t_revGlobalPosting :: a -> T a -> T a
+t_revGlobalPosting a f = f { revGlobalPosting = a }
+
+t_fileTransaction :: a -> T a -> T a
+t_fileTransaction a f = f { fileTransaction = a }
+
+t_revFileTransaction :: a -> T a -> T a
+t_revFileTransaction a f = f { revFileTransaction = a }
+
+t_filePosting :: a -> T a -> T a
+t_filePosting a f = f { filePosting = a }
+
+t_revFilePosting :: a -> T a -> T a
+t_revFilePosting a f = f { revFilePosting = a }
+
+t_filtered :: a -> T a -> T a
+t_filtered a f = f { filtered = a }
+
+t_revFiltered :: a -> T a -> T a
+t_revFiltered a f = f { revFiltered = a }
+
+t_sorted :: a -> T a -> T a
+t_sorted a f = f { sorted = a }
+
+t_revSorted :: a -> T a -> T a
+t_revSorted a f = f { revSorted = a }
+
+t_visible :: a -> T a -> T a
+t_visible a f = f { visible = a }
+
+t_revVisible :: a -> T a -> T a
+t_revVisible a f = f { revVisible = a }
+
+t_lineNum :: a -> T a -> T a
+t_lineNum a f = f { lineNum = a }
+
+t_date :: a -> T a -> T a
+t_date a f = f { date = a }
+
+t_flag :: a -> T a -> T a
+t_flag a f = f { flag = a }
+
+t_number :: a -> T a -> T a
+t_number a f = f { number = a }
+
+t_payee :: a -> T a -> T a
+t_payee a f = f { payee = a }
+
+t_account :: a -> T a -> T a
+t_account a f = f { account = a }
+
+t_postingDrCr :: a -> T a -> T a
+t_postingDrCr a f = f { postingDrCr = a }
+
+t_postingCmdty :: a -> T a -> T a
+t_postingCmdty a f = f { postingCmdty = a }
+
+t_postingQty :: a -> T a -> T a
+t_postingQty a f = f { postingQty = a }
+
+t_totalDrCr :: a -> T a -> T a
+t_totalDrCr a f = f { totalDrCr = a }
+
+t_totalCmdty :: a -> T a -> T a
+t_totalCmdty a f = f { totalCmdty = a }
diff --git a/Penny/Cabin/Row.hs b/Penny/Cabin/Row.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/Row.hs
@@ -0,0 +1,113 @@
+module Penny.Cabin.Row (
+  Justification(LeftJustify, RightJustify),
+  ColumnSpec(ColumnSpec, justification, width, padSpec, bits),
+  C.Width(Width, unWidth),
+  row ) where
+
+import Data.List (transpose)
+import qualified Data.Text as X
+import qualified Penny.Cabin.Chunk as C
+
+-- | How to justify cells. LeftJustify leaves the right side
+-- ragged. RightJustify leaves the left side ragged.
+data Justification =
+  LeftJustify
+  | RightJustify
+  deriving Show
+
+-- | A cell of text output. You tell the cell how to justify itself
+-- and how wide it is. You also tell it the background colors to
+-- use. The cell will be appropriately justified (that is, text
+-- aligned between left and right margins) and padded (with lines of
+-- blank text added on the bottom as needed) when joined with other
+-- cells into a Row.
+data ColumnSpec =
+  ColumnSpec { justification :: Justification
+             , width :: C.Width
+             , padSpec :: C.TextSpec
+             , bits :: [C.Chunk] }
+
+newtype JustifiedCell = JustifiedCell (Either (C.Chunk, C.Chunk) C.Chunk)
+data JustifiedColumn = JustifiedColumn {
+  justifiedCells :: [JustifiedCell]
+  , _justifiedWidth :: C.Width
+  , _justifiedPadSpec :: C.TextSpec }
+
+newtype PaddedColumns = PaddedColumns [[JustifiedCell]]
+newtype CellsByRow = CellsByRow [[JustifiedCell]]
+newtype CellRowsWithNewlines = CellRowsWithNewlines [[JustifiedCell]]
+
+
+justify ::
+  C.TextSpec
+  -> C.Width
+  -> Justification
+  -> C.Chunk
+  -> JustifiedCell
+justify ts (C.Width w) j b
+  | origWidth < w = JustifiedCell . Left $ pair
+  | otherwise = JustifiedCell . Right $ b
+    where
+      origWidth = C.unWidth . C.chunkWidth $ b
+      pad = C.chunk ts t
+      t = X.replicate (w - origWidth) (X.singleton ' ')
+      pair = case j of
+        LeftJustify -> (b, pad)
+        RightJustify -> (pad, b)
+
+newtype Height = Height { _unHeight :: Int }
+                 deriving (Show, Eq, Ord)
+
+height :: [[a]] -> Height
+height = Height . maximum . map length
+
+row :: [ColumnSpec] -> [C.Chunk]
+row =
+  concat
+  . concat
+  . toBits
+  . toCellRowsWithNewlines
+  . toCellsByRow
+  . bottomPad
+  . map justifiedColumn
+
+justifiedColumn :: ColumnSpec -> JustifiedColumn
+justifiedColumn (ColumnSpec j w ts bs) = JustifiedColumn cs w ts where
+  cs = map (justify ts w j) $ bs
+
+bottomPad :: [JustifiedColumn] -> PaddedColumns
+bottomPad jcs = PaddedColumns pcs where
+  justCells = map justifiedCells jcs
+  (Height h) = height justCells
+  pcs = map toPaddedColumn jcs
+  toPaddedColumn (JustifiedColumn cs (C.Width w) ts) = let
+    l = length cs
+    nPads = h - l
+    pad = C.chunk ts t
+    t = X.replicate w (X.singleton ' ')
+    pads = replicate nPads . JustifiedCell . Right $ pad
+    cs'
+      | l < h = cs ++ pads
+      | otherwise = cs
+    in cs'
+
+
+toCellsByRow :: PaddedColumns -> CellsByRow
+toCellsByRow (PaddedColumns cs) = CellsByRow (transpose cs)
+
+
+toCellRowsWithNewlines :: CellsByRow -> CellRowsWithNewlines
+toCellRowsWithNewlines (CellsByRow bs) =
+  CellRowsWithNewlines bs' where
+    bs' = foldr f [] bs
+    newline = JustifiedCell . Right
+              $ C.chunk C.defaultTextSpec (X.singleton '\n')
+    f cells acc = (cells ++ [newline]) : acc
+    
+
+toBits :: CellRowsWithNewlines -> [[[C.Chunk]]]
+toBits (CellRowsWithNewlines cs) = map (map toB) cs where
+  toB (JustifiedCell c) = case c of
+    Left (lb, rb) -> [lb, rb]
+    Right b -> [b]
+
diff --git a/Penny/Cabin/TextFormat.hs b/Penny/Cabin/TextFormat.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Cabin/TextFormat.hs
@@ -0,0 +1,173 @@
+module Penny.Cabin.TextFormat (
+  Lines(Lines, unLines),
+  Words(Words, unWords),
+  CharsPerLine(unCharsPerLine),
+  txtWords,
+  wordWrap,
+  Target(Target, unTarget),
+  Shortest(Shortest, unShortest),
+  shorten) where
+
+import qualified Control.Monad.Trans.State as St
+import qualified Data.Foldable as F
+import Data.Sequence ((|>), ViewR((:>)), ViewL((:<)))
+import qualified Data.Sequence as S
+import qualified Data.Text as X
+import qualified Data.Traversable as T
+
+data Lines = Lines { unLines :: S.Seq Words } deriving Show
+data Words = Words { unWords :: S.Seq X.Text } deriving Show
+newtype CharsPerLine =
+  CharsPerLine { unCharsPerLine :: Int } deriving Show
+
+-- | Splits a blank-separated text into words.
+txtWords :: X.Text -> Words
+txtWords = Words . S.fromList . X.words
+
+-- | Wraps a sequence of words into a sequence of lines, where each
+-- line is no more than a given maximum number of characters long.
+--
+-- If the maximum number of characters per line is less than 1,
+-- returns a Lines that is empty.
+--
+-- An individual word will be split across multiple lines only if that
+-- word is too long to fit into a single line. No hyphenation is done;
+-- the word is simply broken across two lines.
+wordWrap :: Int -> Words -> Lines
+wordWrap l (Words wsq) =
+  if l < 1
+  then Lines (S.empty)
+  else F.foldl f (Lines S.empty) wsq where
+    f (Lines sws) w = let
+      (back, ws) = case S.viewr sws of
+        S.EmptyR -> (S.empty, Words S.empty)
+        (b :> x) -> (b, x)
+      in case addWord l ws w of
+        (Just ws') -> Lines $ back |> ws'
+        Nothing ->
+          if X.length w > l
+          then addPartialWords l (Lines sws) w
+          else Lines (back |> ws |> (Words (S.singleton w)))
+
+lenWords :: Words -> Int
+lenWords (Words s) = case S.length s of
+  0 -> 0
+  l -> (F.sum . fmap X.length $ s) + (l - 1)
+
+-- | Adds a word to a Words, but only if it will not make the Words
+-- exceed the given length.
+addWord :: Int -> Words -> X.Text -> Maybe Words
+addWord l (Words ws) w =
+  let words' = Words (ws |> w)
+  in if lenWords words' > l
+     then Nothing
+     else Just words'
+
+-- | Adds a word to a Words. If the word is too long to fit, breaks it
+-- and adds the longest portion possible. Returns the new Words, and a
+-- Text with the part of the word that was not added (if any; if all
+-- of the word was added, return an empty Text.)
+addPartialWord :: Int -> Words -> X.Text -> (Words, X.Text)
+addPartialWord l (Words ws) t = case addWord l (Words ws) t of
+  (Just ws') -> (ws', X.empty)
+  Nothing ->
+    let maxChars =
+          if S.null ws then l
+          else max 0 (l - lenWords (Words ws) - 1)
+        (begin, end) = X.splitAt maxChars t
+    in (Words (if X.null begin then ws else ws |> begin), end)
+
+addPartialWords :: Int -> Lines -> X.Text -> Lines
+addPartialWords l (Lines wsq) t = let
+  (back, ws) = case S.viewr wsq of
+    S.EmptyR -> (S.empty, Words S.empty)
+    (b :> x) -> (b, x)
+  (rw, rt) = addPartialWord l ws t
+  in if X.null rt
+     then Lines (back |> rw)
+     else addPartialWords l (Lines (back |> rw |> Words (S.empty))) rt
+
+newtype Target = Target { unTarget :: Int } deriving Show
+newtype Shortest = Shortest { unShortest :: Int } deriving Show
+
+-- | Takes a list of words and shortens it so that it fits in the
+-- space allotted. You specify the minimum length for each word, x. It
+-- will shorten the farthest left word first, until it is only x
+-- characters long; then it will shorten the next word until it is
+-- only x characters long, etc. This proceeds until all words are just
+-- x characters long. Then words are shortened to one
+-- character. Then the leftmost words are deleted as necessary.
+--
+-- Assumes that the words will be printed with a separator, which
+-- matters when lengths are calculated.
+shorten :: Shortest -> Target -> Words -> Words
+shorten (Shortest s) (Target t) wsa@(Words wsq) = let
+  nToRemove = max (lenWords wsa - t) 0
+  (allWords, _) = shortenUntilOne s nToRemove wsq
+  in stripWordsUntil t (Words allWords)
+
+-- | Shorten a word by x characters or until it is y characters long,
+-- whichever comes first. Returns the word and the number of
+-- characters removed.
+shortenUntil :: Int -> Int -> X.Text -> (X.Text, Int)
+shortenUntil by shortest t = let
+  removable = max (X.length t - shortest) 0
+  toRemove = min removable (max by 0)
+  prefix = X.length t - toRemove
+  in (X.take prefix t, toRemove)
+
+-- | Shortens a word until it is x characters long or by the number of
+-- characters indicated in the state, whichever is less. Subtracts the
+-- number of characters removed from the state.
+shortenSt :: Int -> X.Text -> St.State Int X.Text
+shortenSt shortest t = do
+  by <- St.get
+  let (r, nRemoved) = shortenUntil by shortest t
+  St.put (by - nRemoved)
+  return r
+
+-- | Shortens each word in a list, from left to right, until a
+-- particular number of characters have been reduced or until each
+-- word is x characters long, whichever happens first. Returns the new
+-- list and the number of characters that still need to be reduced.
+shortenEachInList ::
+  T.Traversable t
+  => Int -- ^ Shortest word length
+  -> Int -- ^ Total number to remove
+  -> t X.Text
+  -> (t X.Text, Int)
+shortenEachInList shortest by ts = (r, left) where
+  k = T.mapM (shortenSt shortest) ts
+  (r, left) = St.runState k by
+
+shortenUntilOne ::
+  T.Traversable t
+  => Int -- ^ Shortest word length to start with
+  -> Int -- ^ Total number of characters to remove
+  -> t X.Text
+  -> (t X.Text, Int)
+shortenUntilOne shortest by ts = let
+  r@(ts', left) = shortenEachInList shortest by ts
+  in if shortest == 1 || left == 0
+     then r
+     else shortenUntilOne (pred shortest) left ts'
+
+-- | Eliminates words until the length of the words, as indicated by
+-- lenWords, is less than or equal to the value given.
+stripWordsUntil :: Int -> Words -> Words
+stripWordsUntil i wsa@(Words ws) = case S.viewl ws of
+  S.EmptyL -> Words (S.empty)
+  (_ :< rest) ->
+    if lenWords wsa <= (max i 0)
+    then wsa
+    else stripWordsUntil (max i 0) (Words rest)
+
+  
+--
+-- Testing
+--
+_words :: Words
+_words = Words . S.fromList . map X.pack $ ws where 
+  ws = [ "these", "are", "fragilisticwonderfulgood",
+         "good", "", "x", "xy", "xyza",
+         "longlonglongword" ]
diff --git a/Penny/Copper.hs b/Penny/Copper.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper.hs
@@ -0,0 +1,132 @@
+-- | Copper - the Penny parser
+module Penny.Copper (
+  -- * Comments
+  C.Comment(Comment),
+
+  -- * Radix and grouping
+  Q.RadGroup,
+  Q.periodComma, Q.periodSpace, Q.commaPeriod, Q.commaSpace,
+  Q.GroupingSpec(..),
+  
+  -- * Default time zone
+  DT.DefaultTimeZone(DefaultTimeZone),
+  DT.utcDefault,
+  
+  -- * FileContents
+  FileContents(FileContents, unFileContents),
+  
+  -- * Errors
+  ErrorMsg (ErrorMsg, unErrorMsg),
+
+  -- * Items
+  I.Item(Transaction, Price, CommentItem, BlankLine),
+  I.Line(unLine),
+
+  -- * Parsing
+  Ledger(Ledger, unLedger),
+  parse,
+  
+  -- * Rendering
+  I.render
+  ) where
+
+
+import Control.Applicative ((<$>))
+import qualified Control.Monad.Exception.Synchronous as Ex
+import qualified Data.Text as X
+import Text.Parsec ( manyTill, eof )
+import qualified Text.Parsec as P
+
+import qualified Penny.Copper.Comments as C
+import qualified Penny.Copper.Qty as Q
+import qualified Penny.Copper.DateTime as DT
+import qualified Penny.Copper.Item as I
+import qualified Penny.Lincoln as L
+
+data Ledger =
+  Ledger { unLedger :: [(I.Line, I.Item)] }
+  deriving Show
+
+newtype FileContents = FileContents { unFileContents :: X.Text }
+                       deriving (Eq, Show)
+
+newtype ErrorMsg = ErrorMsg { unErrorMsg :: X.Text }
+                   deriving (Eq, Show)
+
+parseFile ::
+  DT.DefaultTimeZone
+  -> Q.RadGroup
+  -> (L.Filename, FileContents)
+  -> Ex.Exceptional ErrorMsg
+  [(I.Line, I.Item)]
+parseFile dtz rg (fn, (FileContents c)) =
+  let p = addFileMetadata fn
+          <$> manyTill (I.itemWithLineNumber dtz rg) eof
+      fnStr = X.unpack . L.unFilename $ fn
+  in case P.parse p fnStr c of
+    Left err -> Ex.throw (ErrorMsg . X.pack . show $ err)
+    Right g -> return g
+
+addFileMetadata ::
+  L.Filename
+  -> [(I.Line, I.Item)]
+  -> [(I.Line, I.Item)]
+addFileMetadata fn ls =
+  let (lns, is) = (map fst ls, map snd ls)
+      eis = map toEiItem is
+      procTop s m =
+        m { L.fileTransaction = Just (L.FileTransaction s)
+          , L.filename = Just fn }
+      procPstg s m =
+        m { L.filePosting = Just (L.FilePosting s) }
+      eis' = L.addSerialsToEithers procTop procPstg eis
+      is' = map fromEiItem eis'
+  in zip lns is'
+
+
+addGlobalMetadata ::
+  [[(I.Line, I.Item)]]
+  -> [(I.Line, I.Item)]
+addGlobalMetadata lss =
+  let ls = concat lss
+      procTop s m =
+        m { L.globalTransaction = Just (L.GlobalTransaction s) }
+      procPstg s m =
+        m { L.globalPosting = Just (L.GlobalPosting s) }
+      (lns, is) = (map fst ls, map snd ls)
+      eis = map toEiItem is
+      eis' = L.addSerialsToEithers procTop procPstg eis
+      is' = map fromEiItem eis'
+  in zip lns is'
+
+parse ::
+  DT.DefaultTimeZone
+  -> Q.RadGroup
+  -> [(L.Filename, FileContents)]
+  -> Ex.Exceptional ErrorMsg Ledger
+parse dtz rg ps =
+  mapM (parseFile dtz rg) ps
+  >>= (return . Ledger . addGlobalMetadata)
+
+data Other = OPrice L.PricePoint
+             | OCommentItem C.Comment
+             | OBlankLine
+             deriving Show
+
+type EiItem = Either Other L.Transaction
+
+toEiItem :: I.Item -> EiItem
+toEiItem i = case i of
+  I.Transaction t -> Right t
+  I.Price p -> Left (OPrice p)
+  I.CommentItem c -> Left (OCommentItem c)
+  I.BlankLine -> Left OBlankLine
+
+fromEiItem :: EiItem -> I.Item
+fromEiItem i = case i of
+  Left l -> case l of
+    OPrice p -> I.Price p
+    OCommentItem c -> I.CommentItem c
+    OBlankLine -> I.BlankLine
+  Right t -> I.Transaction t
+
diff --git a/Penny/Copper/Account.hs b/Penny/Copper/Account.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Account.hs
@@ -0,0 +1,131 @@
+-- | Account parsers. Account names fall into one of three groups:
+--
+-- * Level 1 account. Can have nearly any character, including
+-- spaces. However, when in a Ledger file they must be quoted.
+--
+-- * Level 2 account. The first sub-account begins with a letter. All
+-- other characters may be nearly any character, except for a space.
+module Penny.Copper.Account (
+  lvl1Account
+  , lvl1AccountQuoted
+  , lvl2Account
+  , render
+  , lvl1Char
+  , lvl2FirstChar
+  , lvl2RemainingChar
+  ) where
+
+import Control.Applicative((<$>), (<*>), (*>))
+import qualified Data.Foldable as F
+import Data.Text ( snoc, cons, pack, Text )
+import qualified Data.Traversable as T
+import Text.Parsec (
+  char, satisfy, many, (<?>),
+  many1, between, sepBy1, option )
+import Text.Parsec.Text ( Parser )
+
+import Data.List.NonEmpty (NonEmpty((:|)))
+import qualified Data.List.NonEmpty as NE
+import qualified Penny.Lincoln.Bits as B
+import Penny.Lincoln.TextNonEmpty ( TextNonEmpty ( TextNonEmpty ),
+                                    unsafeTextNonEmpty )
+import qualified Penny.Lincoln.HasText as HT
+import qualified Penny.Copper.Util as U
+
+-- | Characters allowed in a Level 1 account. (Check the source code
+-- to see what these are).
+lvl1Char :: Char -> Bool
+lvl1Char c = allowed && notBanned where
+  allowed = U.rangeLettersToSymbols c || c == ' '
+  notBanned = not $ c `elem` "}:"
+
+lvl1Sub :: Parser B.SubAccountName
+lvl1Sub = f <$> p <?> e where
+  f = B.SubAccountName . unsafeTextNonEmpty
+  p = many1 (satisfy lvl1Char)
+  e = "sub account name"
+
+lvl1Account :: Parser B.Account
+lvl1Account = B.Account . NE.fromList <$> p <?> e where
+  e = "account name"
+  p = sepBy1 lvl1Sub (char ':')
+
+lvl1AccountQuoted :: Parser B.Account
+lvl1AccountQuoted = between (char '{') (char '}') lvl1Account
+
+-- | Characters allowed for the first character of a Level 2 account.
+lvl2FirstChar :: Char -> Bool
+lvl2FirstChar = U.rangeLetters
+
+-- | Characters allowed for the remaining characters of a Level 2
+-- account.
+lvl2RemainingChar :: Char -> Bool
+lvl2RemainingChar c = allowed && notBanned where
+    allowed = U.rangeLettersToSymbols c
+    notBanned = not $ c `elem` "}:"
+
+lvl2SubAccountFirst :: Parser B.SubAccountName
+lvl2SubAccountFirst = f <$> c1 <*> cs <?> e where
+  c1 = satisfy lvl2FirstChar
+  cs = many (satisfy lvl2RemainingChar)
+  f l1 lr = B.SubAccountName (TextNonEmpty l1 (pack lr))
+  e = "sub account name beginning with a letter"
+  
+lvl2SubAccountRest :: Parser B.SubAccountName
+lvl2SubAccountRest = f <$> cs <?> e where
+  cs = many1 (satisfy p)
+  p c = allowed && notBanned where
+    allowed = U.rangeLettersToSymbols c
+    notBanned = not $ c `elem` "}:"
+  f = B.SubAccountName . unsafeTextNonEmpty
+  e = "sub account name"
+
+lvl2Account :: Parser B.Account
+lvl2Account = f <$> p1 <*> p2 <?> e where
+  f x y = B.Account (x :| y)
+  p1 = lvl2SubAccountFirst
+  p2 = option [] $
+       char ':' *> sepBy1 lvl2SubAccountRest (char ':')
+  e = "account name"
+
+data Level = L1 | L2
+           deriving (Eq, Ord, Show)
+
+-- | Checks an account to see what level to render it at.
+checkAccount :: B.Account -> Maybe Level
+checkAccount (B.Account subs) = let
+  checkFirst = checkFirstSubAccount (NE.head subs)
+  checkRest = map checkFirstSubAccount (NE.tail subs)
+  in F.minimum <$> T.sequenceA (checkFirst : checkRest)
+
+-- | Checks the first sub account to see if it qualifies as a Level 1
+-- or Level 2 sub account.
+checkFirstSubAccount ::
+  B.SubAccountName
+  -> Maybe Level
+checkFirstSubAccount s = do
+  l <- checkOtherSubAccount s
+  return $ case l of
+    L1 -> L1
+    L2 -> let (B.SubAccountName (TextNonEmpty c _)) = s
+          in if lvl2FirstChar c then L2 else L1
+
+-- | Checks a sub account other than the first one to see if it
+-- qualifies as a Level 1 or Level 2 sub account.
+checkOtherSubAccount ::
+  B.SubAccountName
+  -> Maybe Level
+checkOtherSubAccount = U.checkText ls where
+  ls = (lvl2RemainingChar, L2) :| [(lvl1Char, L1)]
+
+-- | Shows an account, with the minimum level of quoting
+-- possible. Fails with an error if any one of the characters in the
+-- account name does not satisfy the 'lvl1Char' predicate. Otherwise
+-- returns a rendered account, quoted if necessary.
+render :: B.Account -> Maybe Text
+render a = do
+  l <- checkAccount a
+  let t = HT.text . HT.Delimited (pack ":") . HT.textList $ a
+  return $ case l of
+    L1 -> cons '{' t `snoc` '}'
+    L2 -> t
diff --git a/Penny/Copper/Amount.hs b/Penny/Copper/Amount.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Amount.hs
@@ -0,0 +1,96 @@
+-- | Amount parsers. An amount is a commodity and a quantity. (An
+-- entry is an amount and a debit or credit).
+--
+-- Possible combinations:
+--
+-- * quoted Level 1 commodity, optional whitespace, quantity
+--
+-- * Level 3 commodity, optional whitespace, quantity
+--
+-- * Quantity, optional whitespace, quoted Level 1 commodity
+--
+-- * Quantity, optional whitespace, Level 2 commodity
+--
+-- Each quantity may be quoted or unquoted.
+module Penny.Copper.Amount (
+  amount
+  , render
+  ) where
+
+import Control.Applicative ((<$>), (<*>), (<|>))
+import qualified Data.Text as X
+import Text.Parsec ( char, many, (<?>) )
+import Text.Parsec.Text ( Parser )
+
+import qualified Penny.Copper.Commodity as C
+import qualified Penny.Copper.Qty as Q
+import qualified Penny.Lincoln as L
+
+-- | Parse optional spaces, returns appropriate metadata.
+spaces :: Parser L.SpaceBetween
+spaces = f <$> many (char ' ') where
+  f l = if null l then L.NoSpaceBetween else L.SpaceBetween
+
+cmdtyQty :: Parser L.Commodity
+            -> Q.RadGroup
+            -> Parser (L.Amount, L.Format)
+cmdtyQty p rg = let
+  f c s q = (a, fmt) where
+    a = L.Amount q c
+    fmt = L.Format L.CommodityOnLeft s
+  e = "amount, commodity on left"
+  in f <$> p <*> spaces <*> Q.qty rg <?> e
+
+lvl1CmdtyQty :: Q.RadGroup -> Parser (L.Amount, L.Format)
+lvl1CmdtyQty = cmdtyQty C.quotedLvl1Cmdty
+
+lvl3CmdtyQty :: Q.RadGroup -> Parser (L.Amount, L.Format)
+lvl3CmdtyQty = cmdtyQty C.lvl3Cmdty
+
+cmdtyOnRight :: Q.RadGroup -> Parser (L.Amount, L.Format)
+cmdtyOnRight rg = let  
+  f q s c = (a, fmt) where
+    a = L.Amount q c
+    fmt = L.Format L.CommodityOnRight s
+  e = "amount, commodity on right"
+  in f
+     <$> Q.qty rg
+     <*> spaces
+     <*> (C.quotedLvl1Cmdty <|> C.lvl2Cmdty)
+     <?> e
+
+-- | Parses an amount with its metadata. Handles all combinations of
+-- commodities and quantities.
+amount :: Q.RadGroup -> Parser (L.Amount, L.Format)
+amount rg = lvl1CmdtyQty rg
+            <|> lvl3CmdtyQty rg
+            <|> cmdtyOnRight rg
+            <?> "amount"
+
+-- | Render an Amount. The Format is required so that the commodity
+-- can be displayed in the right place.
+render ::
+  (Q.GroupingSpec, Q.GroupingSpec)
+  -- ^ Grouping
+  -> Q.RadGroup
+  -> L.Format
+  -> L.Amount
+  -> Maybe X.Text
+render gs rg f a = let
+  (q, c) = (L.qty a, L.commodity a)
+  qty = Q.quote $ Q.renderUnquoted rg gs q
+  ws = case L.between f of
+    L.SpaceBetween -> X.singleton ' '
+    L.NoSpaceBetween -> X.empty
+  mayLvl3 = C.renderLvl3 c
+  mayLvl2 = C.renderLvl2 c
+  in do
+    quotedLvl1 <- C.renderQuotedLvl1 c
+    let (l, r) = case L.side f of
+          L.CommodityOnLeft -> case mayLvl3 of
+            Nothing -> (quotedLvl1, qty)
+            Just l3 -> (l3, qty)
+          L.CommodityOnRight -> case mayLvl2 of
+            Nothing -> (qty, quotedLvl1)
+            Just l2 -> (qty, l2)
+    return $ X.concat [l, ws, r]
diff --git a/Penny/Copper/Comments.hs b/Penny/Copper/Comments.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Comments.hs
@@ -0,0 +1,35 @@
+module Penny.Copper.Comments (
+  Comment(Comment)
+  , comment
+  , isCommentChar
+  , render
+  ) where
+
+import Control.Applicative ((<*>), (<*), (<$))
+import Data.Text ( Text, pack, cons, snoc )
+import qualified Data.Text as X
+import Text.Parsec (many, char, satisfy, (<?>))
+import Text.Parsec.Text ( Parser )
+
+import Penny.Copper.Util (eol)
+import qualified Penny.Copper.Util as U
+
+data Comment = Comment Text
+             deriving (Eq, Show)
+
+isCommentChar :: Char -> Bool
+isCommentChar c = inCategory || allowed where
+  allowed = c `elem` " "
+  inCategory = U.rangeLettersToSymbols c
+
+comment :: Parser Comment
+comment = Comment . pack
+          <$ char '#'
+          <*> many (satisfy isCommentChar)
+          <* eol
+          <?> "single line comment"
+
+render :: Comment -> Maybe Text
+render (Comment t) = case X.find (not . isCommentChar) t of
+  Nothing -> return $ '#' `cons` t `snoc` '\n'
+  Just _ -> Nothing
diff --git a/Penny/Copper/Commodity.hs b/Penny/Copper/Commodity.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Commodity.hs
@@ -0,0 +1,187 @@
+-- | Commodity parsers. Commodity names fall into three groups:
+--
+-- * Level 1 commodities. These can have a broad selection of
+-- characters, including spaces. The downside is that they need to be
+-- quoted when they appear in a file. (They don't have to be quoted
+-- from the command line, presuming that a single command line
+-- argument captures the entire commodity name and nothing else.)
+--
+-- * Level 2 commodities. The first sub-commodity begins with a letter
+-- or a symbol. All other characters may be nearly any other
+-- character. Spaces however are not permitted.
+--
+-- * Level 3 commodities. All charcters must be letters or symbols. In
+-- addition, the first character cannot be a @+@ or a @-@.
+module Penny.Copper.Commodity (
+  -- * Level 1 commodities
+  lvl1Char,
+  lvl1Cmdty,
+  quotedLvl1Cmdty,
+  commandLineCmdty,
+  
+  -- * Level 2 commodities
+  lvl2FirstChar,
+  lvl2OtherChars,
+  lvl2Cmdty,
+  
+  -- * Level 3 commodities
+  lvl3FirstChar,
+  lvl3OtherChars,
+  lvl3Cmdty,
+  
+  -- * Helpers when parsing from a file
+  leftSideCmdty,
+  rightSideCmdty,
+  
+  -- * Rendering
+  renderQuotedLvl1,
+  renderLvl2,
+  renderLvl3
+  ) where
+
+import Control.Applicative ((<*>), (<$>), (*>), (<|>))
+import Control.Monad (guard)
+import Data.Text ( pack, Text, cons, snoc, singleton )
+import Text.Parsec ( satisfy, many, char, sepBy1, many1, (<?>),
+                     between, option, sepBy )
+import Text.Parsec.Text ( Parser )
+
+import qualified Penny.Lincoln.Bits as B
+import Data.List.NonEmpty (NonEmpty((:|)), fromList)
+import Penny.Copper.Util (listIsOK, firstCharOfListIsOK)
+import qualified Penny.Copper.Util as U
+import qualified Penny.Lincoln.HasText as HT
+import Penny.Lincoln.TextNonEmpty ( TextNonEmpty ( TextNonEmpty ),
+                                    unsafeTextNonEmpty )
+
+-- | Most liberal set of letters allowed in a commodity. 
+lvl1Char :: Char -> Bool
+lvl1Char c = (category || specific) && notBanned where
+  category = U.rangeLettersToSymbols c
+  specific = c == ' '
+  notBanned = not $ c `elem` ['"', ':']
+
+-- | A sub commodity comprised of the most liberal characters.
+lvl1SubCmdty :: Parser B.SubCommodity
+lvl1SubCmdty = f <$> m <?> "sub commodity" where
+  m = many1 (satisfy lvl1Char)
+  f cs = B.SubCommodity (TextNonEmpty (head cs) (pack $ tail cs))
+
+-- | A commodity that might have spaces inside of the name. To parse
+-- this when it is in a ledger file, it must be quoted; use
+-- quotedLvl1Cmdty for that. This parser can be used directly for values
+-- entered from the command line.
+lvl1Cmdty :: Parser B.Commodity
+lvl1Cmdty = (B.Commodity . fromList)
+                  <$> sepBy1 lvl1SubCmdty (char ':')
+                  <?> "commodity with spaces"
+
+
+-- | A commodity that may have spaces in the name; is wrapped inside
+-- of double quotes.
+quotedLvl1Cmdty :: Parser B.Commodity
+quotedLvl1Cmdty = between q q lvl1Cmdty
+              <?> "quoted commodity" where
+                q = char '"'
+
+-- | Allows only letters and symbols.
+lvl2FirstChar :: Char -> Bool
+lvl2FirstChar c = U.rangeLetters c || U.rangeMathCurrency c
+
+lvl2OtherChars :: Char -> Bool
+lvl2OtherChars c = category && notBanned where
+  category = U.rangeLettersToSymbols c
+  notBanned = not $ c `elem` ['"', ':']
+
+lvl2FirstSubCmdty :: Parser B.SubCommodity
+lvl2FirstSubCmdty = f <$> firstLet <*> restLet <?> e where
+  e = "sub commodity, first character is letter or symbol"
+  firstLet = satisfy lvl2FirstChar
+  restLet = many (satisfy lvl2OtherChars)
+  f l1 lr = B.SubCommodity (TextNonEmpty l1 (pack lr))
+
+lvl2OtherSubCmdty :: Parser B.SubCommodity
+lvl2OtherSubCmdty = f <$> ls <?> e where
+  e = "sub commodity"
+  ls = many1 (satisfy lvl2OtherChars)
+  f = B.SubCommodity . unsafeTextNonEmpty
+
+lvl2Cmdty :: Parser B.Commodity
+lvl2Cmdty = f <$> firstSub <*> restSubs <?> e where
+  e = "commodity, first character is letter or symbol"
+  firstSub = lvl2FirstSubCmdty
+  restSubs = option []
+             $ char ':'
+             *> sepBy1 lvl2OtherSubCmdty (char ':')
+  f s1 sr = B.Commodity (s1 :| sr)
+
+lvl3OtherChars :: Char -> Bool
+lvl3OtherChars c = U.rangeLetters c || U.rangeMathCurrency c
+
+lvl3FirstChar :: Char -> Bool
+lvl3FirstChar c = lvl3OtherChars c && (not $ c `elem` "+-")
+
+lvl3FirstSubCmdty :: Parser B.SubCommodity
+lvl3FirstSubCmdty = f <$> c <*> cs <?> e where
+  e = "first sub commodity, letters and symbols only, "
+      ++ "first character not a + or -"
+  f c1 cr = B.SubCommodity (TextNonEmpty c1 (pack cr))
+  c = satisfy lvl3FirstChar
+  cs = many (satisfy lvl3OtherChars)
+
+lvl3OtherSubCmdty :: Parser B.SubCommodity
+lvl3OtherSubCmdty = f <$> ls <?> e where 
+  e = "sub commodity, letters and symbols only"
+  f = B.SubCommodity . unsafeTextNonEmpty
+  ls = many1 (satisfy lvl3OtherChars)
+
+lvl3Cmdty :: Parser B.Commodity
+lvl3Cmdty = f <$> p1 <*> pr <?> e where
+  f cf cs = B.Commodity (cf :| cs)
+  p1 = lvl3FirstSubCmdty
+  pr = option [] $ char ':' *> sepBy lvl3OtherSubCmdty (char ':')
+  e = "commodity, letters and symbols only"
+
+-- | A commodity being read in from the command line, where the
+-- commodity is guaranteed to be the only thing to parse.
+commandLineCmdty :: Parser B.Commodity
+commandLineCmdty = lvl1Cmdty
+
+-- | A commodity on the left side of a quantity in a ledger file.
+leftSideCmdty :: Parser B.Commodity
+leftSideCmdty =
+  quotedLvl1Cmdty
+  <|> lvl3Cmdty
+  <?> "commodity to the left of the quantity"
+
+-- | A commodity on the right side of a quantity in a ledger file.
+rightSideCmdty :: Parser B.Commodity
+rightSideCmdty =
+  quotedLvl1Cmdty
+  <|> lvl2Cmdty
+  <?> "commodity to the right of the quantity"
+
+-- | Render a quoted Level 1 commodity. Fails if any character does
+-- not satisfy lvl1Char.
+renderQuotedLvl1 :: B.Commodity -> Maybe Text
+renderQuotedLvl1 ca@(B.Commodity c) = do
+  guard $ listIsOK lvl1Char ca
+  return $ '"'
+    `cons` HT.text (HT.Delimited (singleton ':') (HT.textList c))
+    `snoc` '"'
+
+-- | Render a Level 2 commodity. Fails if the first character is not a
+-- letter or a symbol, or if any other character is a space.
+renderLvl2 :: B.Commodity -> Maybe Text
+renderLvl2 (B.Commodity c) = do
+  guard $ firstCharOfListIsOK lvl2FirstChar c
+  guard $ listIsOK lvl2OtherChars c
+  return $ HT.text (HT.Delimited (singleton ':') (HT.textList c))
+
+-- | Render a Level 3 commodity. Fails if any character is not a
+-- letter or a symbol.
+renderLvl3 :: B.Commodity -> Maybe Text
+renderLvl3 (B.Commodity c) = do
+  guard $ listIsOK lvl3OtherChars c
+  guard $ firstCharOfListIsOK lvl3FirstChar c
+  return $ HT.text (HT.Delimited (singleton ':') (HT.textList c))
diff --git a/Penny/Copper/DateTime.hs b/Penny/Copper/DateTime.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/DateTime.hs
@@ -0,0 +1,138 @@
+module Penny.Copper.DateTime (
+  DefaultTimeZone(DefaultTimeZone, unDefaultTimeZone)
+  , dateTime
+  , utcDefault
+  , render
+  ) where
+
+import Control.Applicative ((<$>), optional, (<*>))
+import qualified Data.Text as X
+import Data.Time (fromGregorianValid)
+import Data.Maybe (fromMaybe)
+import qualified Data.Time as T
+import Text.Parsec (char, digit, (<|>), (<?>))
+import Text.Parsec.Text ( Parser )
+import Control.Monad ( void, when )
+import Data.Fixed ( Pico )
+import System.Locale (defaultTimeLocale)
+
+import Penny.Copper.Util (spaces)
+import qualified Penny.Lincoln.Bits as B
+
+newtype DefaultTimeZone =
+  DefaultTimeZone { unDefaultTimeZone :: B.TimeZoneOffset }
+  deriving (Eq, Show)
+
+utcDefault :: DefaultTimeZone
+utcDefault = DefaultTimeZone B.noOffset
+
+charToDigit :: Char -> Int
+charToDigit c = case c of
+  '0' -> 0
+  '1' -> 1
+  '2' -> 2
+  '3' -> 3
+  '4' -> 4
+  '5' -> 5
+  '6' -> 6
+  '7' -> 7
+  '8' -> 8
+  '9' -> 9
+  _ -> error "unrecognized digit"
+
+read2digits :: Parser Int
+read2digits = f <$> digit <*> digit where
+  f d1 d2 = charToDigit d1 * 10 + charToDigit d2
+
+read4digits :: Parser Integer
+read4digits = f <$> digit <*> digit <*> digit <*> digit where
+  f d1 d2 d3 d4 = fromIntegral $
+    charToDigit d1 * 1000
+    + charToDigit d2 * 100
+    + charToDigit d3 * 10
+    + charToDigit d4
+
+date :: Parser T.Day
+date = do
+  let slash = void $ char '/' <|> char '-'
+  y <- read4digits
+  slash
+  m <- read2digits
+  slash
+  d <- read2digits
+  case fromGregorianValid y m d of
+    Nothing -> fail "invalid date"
+    Just dt -> return dt
+
+colon :: Parser ()
+colon = void $ char ':'
+
+hrs :: Parser Int
+hrs = do
+  h <- read2digits
+  when (h > 23) $ fail "invalid hour"
+  return h
+
+mins :: Parser Int
+mins = do
+  m <- read2digits
+  when (m > 59) $ fail "invalid minute"
+  return m
+
+secs :: Parser Pico
+secs = do
+  s <- fromIntegral <$> read2digits
+  when (s > 59) $ fail "invalid seconds"
+  return s
+
+timeOfDay :: Parser T.TimeOfDay
+timeOfDay = do
+  h <- hrs
+  colon
+  m <- mins
+  maybeS <- optional (colon >> secs)
+  let s = fromMaybe (fromIntegral (0 :: Int)) maybeS
+  return $ T.TimeOfDay h m s
+
+timeZoneOffset :: Parser B.TimeZoneOffset
+timeZoneOffset = do
+  changeSign <-
+    (char '+' >> return id)
+    <|> (char '-' >> return (negate :: Int -> Int))
+    <?> "time zone sign"
+  h <- read2digits
+  m <- read2digits
+  let mi = h * 60 + m
+  maybe (fail "invalid time zone offset") return
+    $ B.minsToOffset (changeSign mi)
+
+dateTime :: DefaultTimeZone -> Parser B.DateTime
+dateTime (DefaultTimeZone dtz) = do
+  d <- date
+  spaces
+  mayTod <- optional timeOfDay
+  spaces
+  mayTz <- optional timeZoneOffset
+  let tod = fromMaybe T.midnight mayTod
+      tz = fromMaybe dtz mayTz
+  return (B.dateTime (T.LocalTime d tod) tz)
+
+-- | Render a DateTime. If the DateTime is in the given
+-- DefaultTimeZone, and the DateTime is midnight, then the time and
+-- time zone will not be printed. Otherwise, the time and time zone
+-- will both be printed. The test for time zone equality depends only
+-- upon the time zone's offset from UTC.
+render :: DefaultTimeZone -> B.DateTime -> X.Text
+render (DefaultTimeZone dtz) dt = let
+  lt = B.localTime dt
+  off = B.timeZone dt
+  fmtLong = "%F %T %z"
+  fmtShort = "%F"
+  sameZone = dtz == off
+  local = T.localTimeOfDay lt
+  isMidnight = local == T.midnight
+  fmt = if sameZone && isMidnight
+        then fmtShort
+        else fmtLong
+  zt = T.ZonedTime lt (T.minutesToTimeZone (B.offsetToMins off))
+  in X.pack $ T.formatTime defaultTimeLocale fmt zt
diff --git a/Penny/Copper/Entry.hs b/Penny/Copper/Entry.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Entry.hs
@@ -0,0 +1,43 @@
+module Penny.Copper.Entry (entry, render) where
+
+import Control.Applicative ((<$>), (<*>))
+import Control.Monad ( void )
+import qualified Data.Text as X
+import Text.Parsec ( char, string, optional, (<|>), (<?>) )
+import Text.Parsec.Text ( Parser )
+
+import Penny.Copper.Amount (amount)
+import qualified Penny.Copper.Amount as A
+import qualified Penny.Copper.Qty as Q
+import Penny.Copper.Util (lexeme)
+import qualified Penny.Lincoln as L
+
+drCr :: Parser L.DrCr
+drCr = let
+  dr = do
+    void (char 'D' <|> char 'd')
+    void (char 'r') <|> void (string "ebit")
+    return L.Debit
+  cr = do
+    void (char 'C' <|> char 'c')
+    void (optional (char 'r' >> optional (string "edit")))
+    return L.Credit
+  in dr <|> cr
+
+entry :: Q.RadGroup -> Parser (L.Entry, L.Format)
+entry rg = f <$> lexeme drCr <*> amount rg <?> e where
+  f dc (am, fmt) = (L.Entry dc am, fmt)
+  e = "entry"
+
+render ::
+  (Q.GroupingSpec, Q.GroupingSpec)
+  -> Q.RadGroup
+  -> L.Format
+  -> L.Entry
+  -> Maybe X.Text
+render gs rg f (L.Entry dc a) = do
+  amt <- A.render gs rg f a
+  let dcTxt = X.pack $ case dc of
+        L.Debit -> "Dr"
+        L.Credit -> "Cr"
+  return $ X.append (X.snoc dcTxt ' ') amt
diff --git a/Penny/Copper/Flag.hs b/Penny/Copper/Flag.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Flag.hs
@@ -0,0 +1,30 @@
+module Penny.Copper.Flag (flag, isFlagChar, render) where
+
+import Control.Applicative ((<$>), (<*>))
+import Data.Text ( pack, cons, snoc )
+import qualified Data.Text as X
+import Text.Parsec ( char, satisfy, many, between, (<?>))
+import Text.Parsec.Text ( Parser )
+
+import qualified Penny.Copper.Util as U
+import qualified Penny.Lincoln.Bits as B
+import Penny.Lincoln.TextNonEmpty ( TextNonEmpty ( TextNonEmpty ) )
+import qualified Penny.Lincoln.TextNonEmpty as TNE
+
+isFlagChar :: Char -> Bool
+isFlagChar c = allowed && not banned where
+  allowed = U.rangeLettersToSymbols c ||
+            c == ' '
+  banned = c == ']'
+
+flag :: Parser B.Flag
+flag = between (char '[') (char ']') p <?> "flag" where
+  p = (\c cs -> B.Flag (TextNonEmpty c (pack cs)))
+       <$> satisfy isFlagChar
+       <*> many (satisfy isFlagChar)
+
+render :: B.Flag -> Maybe X.Text
+render (B.Flag fl) =
+  if TNE.all isFlagChar fl
+  then Just $ '[' `cons` (TNE.toText fl) `snoc` ']'
+  else Nothing
diff --git a/Penny/Copper/Item.hs b/Penny/Copper/Item.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Item.hs
@@ -0,0 +1,68 @@
+module Penny.Copper.Item (
+  itemWithLineNumber,
+  Item(Transaction, Price, CommentItem, BlankLine),
+  Line(unLine),
+  render
+  ) where
+
+import Control.Applicative ((<$>), (<*>), (<$))
+import qualified Data.Text as X
+import Text.Parsec (getPosition, sourceLine, (<|>),
+                    (<?>))
+import Text.Parsec.Text ( Parser )
+
+import qualified Penny.Copper.Comments as C
+import qualified Penny.Copper.DateTime as DT
+import qualified Penny.Lincoln as L
+import qualified Penny.Copper.Qty as Q
+import Penny.Copper.Price ( price )
+import qualified Penny.Copper.Price as P
+import qualified Penny.Copper.Transaction as T
+import Penny.Copper.Transaction ( transaction )
+import Penny.Copper.Util (eol)
+
+
+-- | An Item is used both to hold the result of parsing an item from a
+-- file and for rendering. It is parameterized on two types: the
+-- metadata type for the TopLine, and the metadata type for the
+-- Posting.
+data Item = Transaction L.Transaction
+          | Price L.PricePoint
+          | CommentItem C.Comment
+          | BlankLine
+          deriving Show
+
+newtype Line = Line { unLine :: Int }
+               deriving (Eq, Show)
+
+itemWithLineNumber ::
+  DT.DefaultTimeZone
+  -> Q.RadGroup
+  -> Parser (Line, Item)
+itemWithLineNumber dtz rg = (,)
+  <$> ((Line . sourceLine) <$> getPosition)
+  <*> parseItem dtz rg
+
+parseItem ::
+  DT.DefaultTimeZone
+  -> Q.RadGroup
+  -> Parser Item
+parseItem dtz rg = let
+   bl = BlankLine <$ eol <?> "blank line"
+   t = Transaction <$> transaction dtz rg
+   p = Price <$> price dtz rg
+   c = CommentItem <$> C.comment
+   in (bl <|> t <|> p <|> c)
+
+render ::
+  DT.DefaultTimeZone
+  -> (Q.GroupingSpec, Q.GroupingSpec)
+  -> Q.RadGroup
+  -> Item
+  -> Maybe X.Text
+render dtz gs rg i = case i of
+  Transaction t -> T.render dtz gs rg t
+  Price pp -> P.render dtz gs rg pp
+  CommentItem c -> C.render c
+  BlankLine -> Just $ X.singleton '\n'
+    
diff --git a/Penny/Copper/Memos/Posting.hs b/Penny/Copper/Memos/Posting.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Memos/Posting.hs
@@ -0,0 +1,39 @@
+module Penny.Copper.Memos.Posting (
+  memo, render, isCommentChar) where
+
+import Control.Applicative ((<*>), (<*), (<$), (<$>))
+import qualified Data.Text as X
+import Text.Parsec (char, satisfy, many, (<?>))
+import Text.Parsec.Text ( Parser )
+
+import Penny.Copper.Util (eol, rangeLettersToSymbols)
+import qualified Penny.Lincoln.Bits as B
+import qualified Penny.Lincoln.TextNonEmpty as TNE
+
+isCommentChar :: Char -> Bool
+isCommentChar c = rangeLettersToSymbols c
+                  || c == ' '
+
+memo :: Parser B.Memo
+memo = B.Memo <$> many memoLine
+
+memoLine :: Parser B.MemoLine
+memoLine = (\c cs -> B.MemoLine $ TNE.TextNonEmpty c (X.pack cs))
+           <$ char '\''
+           <*> satisfy isCommentChar
+           <*> many (satisfy isCommentChar)
+           <* eol
+           <?> "posting memo"
+
+render :: B.Memo -> Maybe X.Text
+render (B.Memo ls) = X.concat <$> mapM renderLine ls 
+
+renderLine :: B.MemoLine -> Maybe X.Text
+renderLine (B.MemoLine l) =
+  if TNE.all isCommentChar l
+  then Just $
+       X.pack (replicate 8 ' ')
+       `X.snoc` '\''
+       `X.append` TNE.toText l
+       `X.snoc` '\n'
+  else Nothing
diff --git a/Penny/Copper/Memos/Transaction.hs b/Penny/Copper/Memos/Transaction.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Memos/Transaction.hs
@@ -0,0 +1,45 @@
+module Penny.Copper.Memos.Transaction (
+  memo, render, isCommentChar
+  ) where
+
+import Control.Applicative ((<*>), (<$>), (<*), (<$))
+import qualified Data.Text as X
+import Text.Parsec (
+  char, satisfy, sourceLine, getPosition, (<?>),
+  many)
+import Text.Parsec.Text ( Parser )
+
+import Penny.Copper.Util (rangeLettersToSymbols, eol)
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.TextNonEmpty as TNE
+
+isCommentChar :: Char -> Bool
+isCommentChar c = rangeLettersToSymbols c
+                  || c == ' '
+
+memoLine :: Parser L.MemoLine
+memoLine = L.MemoLine <$> (
+  TNE.TextNonEmpty
+  <$ char ';'
+  <*> satisfy isCommentChar
+  <*> (X.pack <$> many (satisfy isCommentChar))
+  <* eol )
+  <?> "posting memo line"
+
+-- | Parses a transaction memo and associated whitespace afterward.
+memo :: Parser (L.Memo, L.TopMemoLine)
+memo =
+  flip (,)
+  <$> ((L.TopMemoLine . sourceLine) <$> getPosition)
+  <*> (L.Memo <$> many memoLine)
+  <?> "transaction memo"
+  
+-- | Renders a transaction memo. Fails if the memo is not renderable.
+render :: L.Memo -> Maybe X.Text
+render (L.Memo m) = X.concat <$> mapM renderLine m
+
+renderLine :: L.MemoLine -> Maybe X.Text
+renderLine (L.MemoLine l) =
+  if TNE.all isCommentChar l
+  then Just $ X.singleton ';' `X.append` TNE.toText l `X.snoc` '\n'
+  else Nothing
diff --git a/Penny/Copper/Number.hs b/Penny/Copper/Number.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Number.hs
@@ -0,0 +1,29 @@
+module Penny.Copper.Number (isNumChar, number, render) where
+
+import Control.Applicative ((<$>), (<*>))
+import Data.Text ( pack, cons, snoc, Text )
+import Text.Parsec ( char, satisfy, many, between, (<?>))
+import Text.Parsec.Text ( Parser )
+
+import Penny.Copper.Util (rangeLettersToSymbols)
+import qualified Penny.Lincoln.Bits as B
+import Penny.Lincoln.TextNonEmpty ( TextNonEmpty ( TextNonEmpty ) )
+import qualified Penny.Lincoln.TextNonEmpty as TNE
+
+isNumChar :: Char -> Bool
+isNumChar c = allowed && not banned where
+  allowed = rangeLettersToSymbols c ||
+            c == ' '
+  banned = c == ')'
+
+number :: Parser B.Number
+number = between (char '(') (char ')') p <?> "number" where
+  p = (\c cs -> B.Number (TextNonEmpty c (pack cs)))
+      <$> satisfy isNumChar
+      <*> many (satisfy isNumChar)
+
+render :: B.Number -> Maybe Text
+render (B.Number tne) =
+  if TNE.all isNumChar tne
+  then Just $ '(' `cons` TNE.toText tne `snoc` ')'
+  else Nothing
diff --git a/Penny/Copper/Payees.hs b/Penny/Copper/Payees.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Payees.hs
@@ -0,0 +1,87 @@
+-- | Payee parsers. There are two types of payee parsers:
+--
+-- Quoted payees. These allow the most latitude in the characters
+-- allowed. They are surrounded by @<@ and @>@.
+--
+-- Unquoted payees. These are not surrounded by @<@ and @>@. Their
+-- first character must be a letter or number.
+
+module Penny.Copper.Payees (
+  -- * Parse any payee
+  payee
+  
+  -- * Quoted payees
+  , quotedChar
+  , quotedPayee
+    
+    -- * Unquoted payees
+  , unquotedFirstChar
+  , unquotedRestChars
+  , unquotedPayee
+    
+    -- * Rendering
+  , smartRender
+  , quoteRender
+  ) where
+
+import Control.Applicative ((<$>), (<*>), (<|>))
+import Data.Text (pack, Text, snoc, cons)
+import qualified Data.Text as X
+import Text.Parsec (char, satisfy, many, between, (<?>))
+import Text.Parsec.Text ( Parser )
+
+import Penny.Copper.Util (rangeLettersToSymbols, rangeLetters)
+import qualified Penny.Lincoln.Bits as B
+import Penny.Lincoln.TextNonEmpty as TNE
+
+quotedChar :: Char -> Bool
+quotedChar c = allowed && not banned where
+  allowed = rangeLettersToSymbols c || c == ' '
+  banned = c == '>'
+
+payee :: Parser B.Payee
+payee = quotedPayee <|> unquotedPayee
+
+quotedPayee :: Parser B.Payee
+quotedPayee = between (char '<') (char '>') p <?> "quoted payee" where
+  p = (\c cs -> B.Payee (TextNonEmpty c (pack cs)))
+      <$> satisfy quotedChar
+      <*> many (satisfy quotedChar)
+
+unquotedFirstChar :: Char -> Bool
+unquotedFirstChar = rangeLetters
+
+unquotedRestChars :: Char -> Bool
+unquotedRestChars = quotedChar
+
+unquotedPayee :: Parser B.Payee
+unquotedPayee = let
+  p c cs = B.Payee (TextNonEmpty c (pack cs))
+  in p
+     <$> satisfy unquotedFirstChar
+     <*> many (satisfy unquotedRestChars)
+     <?> "unquoted payee"
+
+-- | Render a payee with a minimum of quoting. Fails if cannot be
+-- rendered at all.
+smartRender :: B.Payee -> Maybe Text
+smartRender (B.Payee p) = let
+  TextNonEmpty f r = p
+  noQuoteNeeded = unquotedFirstChar f
+                  && X.all unquotedRestChars r
+  renderable = TNE.all quotedChar p
+  quoted = '<' `cons` TNE.toText p `snoc` '>'
+  makeText
+    | noQuoteNeeded = Just $ TNE.toText p
+    | renderable = Just quoted
+    | otherwise = Nothing
+  in makeText
+
+-- | Renders with quotes, whether the payee needs it or not.
+quoteRender :: B.Payee -> Maybe Text
+quoteRender (B.Payee p) = let
+  renderable = TNE.all quotedChar p
+  quoted = '<' `cons` TNE.toText p `snoc` '>'
+  in if renderable
+     then Just quoted
+     else Nothing
diff --git a/Penny/Copper/Posting.hs b/Penny/Copper/Posting.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Posting.hs
@@ -0,0 +1,126 @@
+module Penny.Copper.Posting (
+  posting, render
+  ) where
+
+import Control.Applicative ((<$>), (<*>), (<*), (<|>))
+import qualified Data.Text as X
+import Text.Parsec (
+  getParserState,
+  statePos, optionMaybe, sourceLine, (<?>),
+  State)
+import Text.Parsec.Text ( Parser )
+
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.Bits as B
+import qualified Penny.Copper.Account as Ac
+import qualified Penny.Copper.Entry as En
+import qualified Penny.Copper.Flag as Fl
+import qualified Penny.Copper.Memos.Posting as Me
+import qualified Penny.Copper.Number as Nu
+import qualified Penny.Copper.Payees as Pa
+import qualified Penny.Copper.Qty as Qt
+import qualified Penny.Copper.Tags as Ta
+import Penny.Copper.Util (lexeme, eol, renMaybe, txtWords)
+import qualified Penny.Lincoln.Transaction as T
+import qualified Penny.Lincoln.Transaction.Unverified as U
+
+posting :: Qt.RadGroup
+           -> Parser U.Posting
+posting rg =
+  makeUnverified
+  <$> (L.PostingLine . sourceLine . statePos
+       <$> getParserState)
+  <*> (UnverifiedWithMeta
+       <$> optionMaybe (lexeme Fl.flag)
+       <*> optionMaybe (lexeme Nu.number)
+       <*> optionMaybe (lexeme Pa.quotedPayee)
+       <*> lexeme (Ac.lvl1AccountQuoted <|> Ac.lvl2Account)
+       <*> lexeme Ta.tags
+       <*> optionMaybe (lexeme (En.entry rg))
+       <* eol
+       <*> Me.memo)
+  <?> "posting"
+
+data UnverifiedWithMeta = UnverifiedWithMeta {
+  flag :: Maybe B.Flag
+  , number :: Maybe B.Number
+  , payee :: Maybe B.Payee
+  , account :: B.Account
+  , tags :: B.Tags
+  , entry :: Maybe (B.Entry, L.Format)
+  , memo :: B.Memo
+  } deriving (Eq, Show)
+    
+makeUnverified ::
+  L.PostingLine
+  -> UnverifiedWithMeta
+  -> U.Posting
+makeUnverified pl u = upo where
+  upo = U.Posting (payee u) (number u) (flag u) (account u)
+        (tags u) en (memo u) meta
+  (en, fmt) = case entry u of
+    Nothing -> (Nothing, Nothing)
+    Just (e, f) -> (Just e, Just f)
+  meta = L.PostingMeta (Just pl) fmt Nothing Nothing
+
+
+-- | Renders a Posting. Fails if any of the components
+-- fail to render. In addition, if the unverified Posting has an
+-- Entry, a Format must be provided, otherwise render fails.
+--
+-- The columns look like this. Column numbers begin with 0 (like they
+-- do in Emacs) rather than with column 1 (like they do in
+-- Vim). (Really Emacs is the strange one; most CLI utilities seem to
+-- start with column 1 too...)
+--
+-- > ID COLUMN WIDTH WHAT
+-- > ---------------------------------------------------
+-- > A    0      4     Blank spaces for indentation
+-- > B    4      50    Flag, Number, Payee, Account, Tags
+-- > C    54     2     Blank spaces for padding
+-- > D    56     NA    Entry
+--
+-- Omit the padding after column B if there is no entry; also omit
+-- columns C and D entirely if there is no Entry. (It is annoying to
+-- have extraneous blank space in a file).
+render ::
+  (Qt.GroupingSpec, Qt.GroupingSpec)
+  -> Qt.RadGroup
+  -> T.Posting
+  -> Maybe X.Text
+render gs rg p = do
+  fl <- renMaybe (T.pFlag p) Fl.render
+  nu <- renMaybe (T.pNumber p) Nu.render
+  pa <- renMaybe (T.pPayee p) Pa.quoteRender
+  ac <- Ac.render (T.pAccount p)
+  ta <- Ta.render (T.pTags p)
+  me <- Me.render (T.pMemo p)
+  maybePair <- case (T.pInferred p, L.postingFormat . T.pMeta $ p) of
+    (T.Inferred, Nothing) -> return Nothing
+    (T.NotInferred, Just f) -> return (Just (T.pEntry p, f))
+    _ -> Nothing
+  let renderEn (e, f) = En.render gs rg f e
+  en <- renMaybe maybePair renderEn
+  return $ formatter fl nu pa ac ta en me
+
+formatter ::
+  X.Text    -- ^ Flag
+  -> X.Text -- ^ Number
+  -> X.Text -- ^ Payee
+  -> X.Text -- ^ Account
+  -> X.Text -- ^ Tags
+  -> X.Text -- ^ Entry
+  -> X.Text -- ^ Memo
+  -> X.Text
+formatter fl nu pa ac ta en me = let
+  colA = X.pack (replicate 4 ' ')
+  colBnoPad = txtWords [fl, nu, pa, ac, ta]
+  colD = en
+  colB = if X.null en
+         then colBnoPad
+         else X.justifyLeft 50 ' ' colBnoPad
+  colC = if X.null en
+         then X.empty
+         else X.pack (replicate 2 ' ')
+  rtn = X.singleton '\n'
+  in X.concat [colA, colB, colC, colD, rtn, me]
diff --git a/Penny/Copper/Price.hs b/Penny/Copper/Price.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Price.hs
@@ -0,0 +1,97 @@
+module Penny.Copper.Price (price, render) where
+
+import Text.Parsec ( char, getPosition, sourceLine, (<?>),
+                     SourcePos )
+import Text.Parsec.Text ( Parser )
+
+import Control.Applicative ((<*>), (<*), (<|>), (<$))
+import Data.Text (singleton, snoc, intercalate)
+import qualified Data.Text as X
+
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.Bits.PricePoint as PP
+import qualified Penny.Copper.Amount as A
+import qualified Penny.Copper.Commodity as C
+import qualified Penny.Copper.DateTime as DT
+import qualified Penny.Copper.Qty as Q
+import Penny.Copper.Util (lexeme, eol)
+
+{-
+BNF-style specification for prices:
+
+<price> ::= "dateTime" <fromCmdty> <toAmount>
+<fromCmdty> ::= "quotedLvl1Cmdty" | "lvl2Cmdty"
+<toAmount> ::= "amount"
+-}
+
+mkPrice :: SourcePos
+         -> L.DateTime
+         -> L.Commodity
+         -> (L.Amount, L.Format)
+         -> Maybe L.PricePoint
+mkPrice pos dt from (am, fmt) = let
+  to = L.commodity am
+  q = L.qty am
+  pm = L.PriceMeta (Just pl) (Just fmt)
+  pl = L.PriceLine . sourceLine $ pos
+  in do
+    p <- L.newPrice (L.From from) (L.To to) (L.CountPerUnit q)
+    return $ L.PricePoint dt p pm
+
+maybePrice ::
+  DT.DefaultTimeZone
+  -> Q.RadGroup
+  -> Parser (Maybe L.PricePoint)
+maybePrice dtz rg =
+  mkPrice
+  <$ lexeme (char '@')
+  <*> getPosition
+  <*> lexeme (DT.dateTime dtz)
+  <*> lexeme (C.quotedLvl1Cmdty <|> C.lvl2Cmdty)
+  <*> A.amount rg
+  <* eol
+  <?> "price"
+  
+-- | A price with an EOL and whitespace after the EOL. @price d r@
+-- will parse a PriceBox, where @d@ is the DefaultTimeZone for the
+-- DateTime in the PricePoint, and @r@ is the radix point and grouping
+-- character to parse the Amount. Fails if the price is not valid
+-- (e.g. the from and to commodities are the same).
+price ::
+  DT.DefaultTimeZone
+  -> Q.RadGroup
+  -> Parser L.PricePoint
+price dtz rg = do
+  b <- maybePrice dtz rg
+  case b of
+    Nothing -> fail "invalid price given"
+    Just p -> return p
+
+-- | @render dtz rg f pp@ renders a price point @pp@. @dtz@ is the
+-- DefaultTimeZone for rendering of the DateTime in the Price
+-- Point. @rg@ is the radix point and grouping character for the
+-- amount. @f@ is the Format for how the price will be
+-- formatted. Fails if either the From or the To commodity cannot be
+-- rendered.
+render ::
+  DT.DefaultTimeZone
+  -> (Q.GroupingSpec, Q.GroupingSpec)
+  -> Q.RadGroup
+  -> L.PricePoint
+  -> Maybe X.Text
+render dtz gs rg pp = let
+  dateTxt = DT.render dtz (PP.dateTime pp)
+  (L.From from) = L.from . L.price $ pp
+  (L.To to) = L.to . L.price $ pp
+  (L.CountPerUnit q) = L.countPerUnit . L.price $ pp
+  mayFromTxt = C.renderLvl3 from <|> C.renderQuotedLvl1 from
+  amt = L.Amount q to
+  in do
+    fmt <- L.priceFormat . L.ppMeta $ pp
+    let mayAmtTxt = A.render gs rg fmt amt
+    amtTxt <- mayAmtTxt
+    fromTxt <- mayFromTxt
+    return $
+       (intercalate (singleton ' ')
+       [singleton '@', dateTxt, fromTxt, amtTxt])
+       `snoc` '\n'
diff --git a/Penny/Copper/Qty.hs b/Penny/Copper/Qty.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Qty.hs
@@ -0,0 +1,283 @@
+module Penny.Copper.Qty (
+  -- * Setting the radix and separator characters
+  RadGroup, periodComma, periodSpace, commaPeriod,
+  commaSpace,
+  
+  -- * Rendering
+  GroupingSpec(NoGrouping, GroupLarge, GroupAll),
+  renderUnquoted,
+  quote,
+
+  -- * Parsing quantities
+  qtyUnquoted,
+  qtyQuoted,
+  qty) where
+
+import Control.Applicative ((<$>), (<*>), (<$), (*>), optional)
+import qualified Data.Decimal as D
+import Data.List (intercalate)
+import Data.List.Split (splitEvery, splitOn)
+import qualified Data.Text as X
+import Data.Text (snoc, cons)
+import Text.Parsec ( char, (<|>), many1, (<?>), 
+                     sepBy1, digit, between)
+import qualified Text.Parsec as P
+import Text.Parsec.Text ( Parser )
+
+import Penny.Lincoln.Bits.Qty ( Qty, partialNewQty, unQty )
+
+data Radix = RComma | RPeriod deriving (Eq, Show)
+data Grouper = GComma | GPeriod | GSpace deriving (Eq, Show)
+
+data RadGroup = RadGroup Radix Grouper deriving (Eq, Show)
+
+-- | Radix is period, grouping is comma
+periodComma :: RadGroup
+periodComma = RadGroup RPeriod GComma
+
+-- | Radix is period, grouping is space
+periodSpace :: RadGroup
+periodSpace = RadGroup RPeriod GSpace
+
+-- | Radix is comma, grouping is period
+commaPeriod :: RadGroup
+commaPeriod = RadGroup RComma GPeriod
+
+-- | Radix is comma, grouping is space
+commaSpace :: RadGroup
+commaSpace = RadGroup RComma GSpace
+
+parseRadix :: Radix -> Parser ()
+parseRadix r = () <$ char c <?> "radix point" where
+  c = case r of RComma -> ','; RPeriod -> '.'
+
+parseGrouper :: Grouper -> Parser ()
+parseGrouper g = () <$ char c <?> "grouping character" where
+  c = case g of
+    GComma -> ','
+    GPeriod -> '.'
+    GSpace -> ' '
+
+{- a BNF style specification for numbers.
+
+<radix> ::= (as specified)
+<grouper> ::= (as specified)
+<digit> ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0"
+<digits> ::= <digit> | <digit> <digits>
+<firstGroups> ::= <digits> <grouper> <digits>
+<nextGroup> ::= <grouper> <digits>
+<nextGroups> ::= <nextGroup> | <nextGroup> <nextGroups>
+<allGroups> ::= <firstGroups> | <firstGroups> <nextGroups>
+<whole> ::= <allGroups> | <digits>
+<fractional> ::= <digits>
+<number> ::= <whole>
+             | <whole> <radix>
+             | <whole> <radix> <fractional>
+             | <radix> <fractional>
+-}
+
+wholeGrouped :: Grouper -> Parser String
+wholeGrouped g = p <$> group1 <*> optional groupRest <?> e where 
+  e = "whole number"
+  group1 = many1 digit
+  groupRest = parseGrouper g *> sepBy1 (many1 digit) (parseGrouper g)
+  p g1 gr = case gr of
+    Nothing -> g1
+    Just groups -> g1 ++ concat groups
+
+fractionalGrouped :: Grouper -> Parser String
+fractionalGrouped g =
+  p <$> group1 <*> optional groupRest <?> e where
+    e = "fractional number"
+    group1 = many1 digit
+    groupRest = parseGrouper g *> sepBy1 (many1 digit) (parseGrouper g)
+    p g1 gr = case gr of
+      Nothing -> g1
+      Just groups -> g1 ++ concat groups
+
+wholeNonGrouped :: Parser String
+wholeNonGrouped = many1 digit
+
+fractionalOnly :: Radix -> Parser String
+fractionalOnly r = parseRadix r *> many1 P.digit
+
+numberStrGrouped :: Radix -> Grouper -> Parser NumberStr
+numberStrGrouped r g = startsWhole <|> fracOnly <?> e where
+  e = "quantity, with optional grouping"
+  startsWhole = p <?> "whole number" where
+    p = do
+      wholeStr <- wholeGrouped g
+      mayRad <- optional (parseRadix r)
+      case mayRad of
+        Nothing -> return $ Whole wholeStr
+        Just _ -> do
+          mayFrac <- optional $ fractionalGrouped g
+          case mayFrac of
+            Nothing -> return $ WholeRad wholeStr
+            Just frac -> return $ WholeRadFrac wholeStr frac
+  fracOnly = RadFrac <$> fractionalOnly r
+
+numberStrNonGrouped :: Radix -> Parser NumberStr
+numberStrNonGrouped r = startsWhole <|> fracOnly <?> e where
+  e = "quantity, no grouping"
+  startsWhole = p <?> "whole number" where
+    p = do
+      wholeStr <- wholeNonGrouped
+      mayRad <- optional (parseRadix r)
+      case mayRad of
+        Nothing -> return $ Whole wholeStr
+        Just _ -> do
+          mayFrac <- optional $ many1 P.digit
+          case mayFrac of
+            Nothing -> return $ WholeRad wholeStr
+            Just frac -> return $ WholeRadFrac wholeStr frac
+  fracOnly = RadFrac <$> fractionalOnly r
+
+
+-- | A number string after radix and grouping characters have been
+-- stripped out.
+data NumberStr =
+  Whole String
+  -- ^ A whole number only. No radix point.
+  | WholeRad String
+    -- ^ A whole number and a radix point, but nothing after the radix
+    -- point.
+  | WholeRadFrac String String
+    -- ^ A whole number and something after the radix point.
+  | RadFrac String
+    -- ^ A radix point and a fractional value after it, but nothing
+    -- before the radix point.
+  deriving Show
+
+-- | Do not use Prelude.read or Prelude.reads on whole decimal strings
+-- like @232.72@. Sometimes it will fail, though sometimes it will
+-- succeed; why is not clear to me. Hopefully reading integers won't
+-- fail! However, in case it does, use read', whose error message will
+-- at least tell you what number was being read.
+--
+-- Data.Decimal cannot handle decimals whose exponent would exceed
+-- 255, which is the maximum that a Word8 can hold. A Word8 is used to
+-- hold the exponent. If the exponent would exceed 255, this function
+-- fails.
+toDecimal :: NumberStr -> Maybe D.Decimal
+toDecimal ns = case ns of
+  Whole s -> Just $ D.Decimal 0 (readWithErr s)
+  WholeRad s -> Just $ D.Decimal 0 (readWithErr s)
+  WholeRadFrac w f -> fromWholeRadFrac w f
+  RadFrac f -> fromWholeRadFrac "0" f
+  where
+    fromWholeRadFrac w f = let
+      len = length f
+      in if len > 255
+         then Nothing
+         else Just $ D.Decimal (fromIntegral len) (readWithErr (w ++ f))
+
+readWithErr :: String -> Integer
+readWithErr s = let
+  readSresult = reads s
+  in case readSresult of
+    (i, ""):[] -> i
+    _ -> error $ "readWithErr failed. String being read: " ++ s
+         ++ " Result of reads: " ++ show readSresult
+  
+-- | Unquoted quantity. These include no spaces, regardless of what
+-- the grouping character is.
+qtyUnquoted :: RadGroup -> Parser Qty
+qtyUnquoted (RadGroup r g) = do
+  nStr <- case g of
+    GSpace -> numberStrNonGrouped r
+    _ -> numberStrGrouped r g
+  d <- case toDecimal nStr of
+    Nothing -> fail $ "fractional part too big: " ++ show nStr
+    Just dec -> return dec
+  return $ partialNewQty d
+  
+-- | Parse quoted quantity. It can include spaces, if the grouping
+-- character is a space. However these must be quoted when in a Ledger
+-- file (from the command line they need not be quoted). The quote
+-- character is a caret, @^@.
+qtyQuoted :: RadGroup -> Parser Qty
+qtyQuoted (RadGroup r g) = between (char '^') (char '^') p where
+  p = do
+    nStr <- numberStrGrouped r g
+    d <- case toDecimal nStr of
+      Nothing -> fail $ "fractional part too big: " ++ show nStr
+      Just dec -> return dec
+    return $ partialNewQty d
+
+-- | Parse a quoted quantity or, if that fails, an unquoted
+-- quantity.
+qty :: RadGroup -> Parser Qty
+qty r = qtyQuoted r <|> qtyUnquoted r <?> "quantity"
+
+-- | Specifies how to perform digit grouping when rendering a
+-- quantity. All grouping groups into groups of 3 digits.
+data GroupingSpec = 
+  NoGrouping
+  -- ^ Do not perform any digit grouping
+  | GroupLarge
+    -- ^ Group digits, but only if the number to be grouped is greater
+    -- than 9,999 (if grouping the whole part) or if there are more
+    -- than 4 decimal places (if grouping the fractional part).
+  | GroupAll
+    -- ^ Group digits whenever there are at least four decimal places.
+  deriving (Eq, Show)
+
+-- | Quotes a rendered Qty, but only if necessary; otherwise, simply
+-- leaves it unquoted.
+quote :: X.Text -> X.Text
+quote t = case X.find (== ' ') t of
+  Nothing -> t
+  Just _ -> '^' `cons` t `snoc` '^'
+
+-- | Renders an unquoted Qty. Performs digit grouping as requested. 
+renderUnquoted ::
+  RadGroup
+  -> (GroupingSpec, GroupingSpec)
+  -- ^ Group for the portion to the left and right of the radix point?
+
+  -> Qty
+  -> X.Text
+renderUnquoted (RadGroup r g) (gl, gr) q = let
+  qs = show . unQty $ q
+  in X.pack $ case splitOn "." qs of
+    w:[] -> groupWhole g gl w
+    w:d:[] ->
+      groupWhole g gl w ++ renderRadix r ++ groupDecimal g gr d
+    _ -> error "Qty.hs: rendering error"
+
+renderGrouper :: Grouper -> String
+renderGrouper g = case g of
+  GComma -> ","
+  GPeriod -> "."
+  GSpace -> " "
+
+renderRadix :: Radix -> String
+renderRadix r = case r of
+  RComma -> ","
+  RPeriod -> "."
+
+-- | Performs grouping for amounts to the left of the radix point.
+groupWhole :: Grouper -> GroupingSpec -> String -> String
+groupWhole g gs o = let
+  grouped = intercalate (renderGrouper g)
+            . reverse
+            . map reverse
+            . splitEvery 3
+            . reverse
+            $ o
+  in case gs of
+  NoGrouping -> o
+  GroupLarge -> if length o > 4 then grouped else o
+  GroupAll -> grouped
+
+-- | Performs grouping for amounts to the right of the radix point.
+groupDecimal :: Grouper -> GroupingSpec -> String -> String
+groupDecimal g gs o = let
+  grouped = intercalate (renderGrouper g)
+            . splitEvery 3
+            $ o
+  in case gs of
+    NoGrouping -> o
+    GroupLarge -> if length o > 4 then grouped else o
+    GroupAll -> grouped
diff --git a/Penny/Copper/Tags.hs b/Penny/Copper/Tags.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Tags.hs
@@ -0,0 +1,37 @@
+module Penny.Copper.Tags (
+  isTagChar, tags, render
+  )where
+
+import Control.Applicative ((<$>), (*>), (<*>))
+import qualified Data.Text as X
+import Text.Parsec (char, satisfy, many, (<?>))
+import Text.Parsec.Text ( Parser )
+
+import Penny.Copper.Util (lexeme, rangeLettersNumbers)
+import qualified Penny.Lincoln.Bits as B
+import qualified Penny.Lincoln.TextNonEmpty as TNE
+
+isTagChar :: Char -> Bool
+isTagChar = rangeLettersNumbers
+
+tagChar :: Parser Char
+tagChar = satisfy isTagChar
+
+tag :: Parser B.Tag
+tag = (char '*' *> (f <$> tagChar <*> many tagChar)) <?> e where
+  f t ts = B.Tag $ TNE.TextNonEmpty t (X.pack ts)
+  e = "tag"
+
+tags :: Parser B.Tags
+tags = B.Tags <$> many (lexeme tag)
+
+renderTag :: B.Tag -> Maybe X.Text
+renderTag (B.Tag t) =
+  if TNE.all isTagChar t
+  then Just $ X.cons '*' (TNE.toText t)
+  else Nothing
+
+render :: B.Tags -> Maybe X.Text
+render (B.Tags ts) =
+  X.intercalate (X.singleton ' ')
+  <$> mapM renderTag ts
diff --git a/Penny/Copper/TopLine.hs b/Penny/Copper/TopLine.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/TopLine.hs
@@ -0,0 +1,59 @@
+module Penny.Copper.TopLine (
+  topLine
+  , render
+  ) where
+
+import Control.Applicative ((<$>), (<*>), (<*), (<|>), pure)
+import qualified Data.Text as X
+import Text.Parsec ( optionMaybe, getPosition,
+                     sourceLine, optionMaybe )
+import Text.Parsec.Text ( Parser )
+
+import qualified Penny.Copper.DateTime as DT
+import qualified Penny.Copper.Memos.Transaction as M
+import qualified Penny.Copper.Flag as F
+import qualified Penny.Copper.Number as N
+import qualified Penny.Copper.Payees as P
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.Transaction as T
+import Penny.Copper.Util (lexeme, eol, renMaybe, txtWords)
+import qualified Penny.Lincoln.Transaction.Unverified as U
+
+topLine ::
+  DT.DefaultTimeZone
+  -- -> Parser (U.TopLine (Meta.TopMemoLine, Meta.TopLineLine))
+  -> Parser U.TopLine
+topLine dtz =
+  f
+  <$> M.memo
+  <*> getPosition
+  <*> lexeme (DT.dateTime dtz)
+  <*> optionMaybe (lexeme F.flag)
+  <*> optionMaybe (lexeme N.number)
+  <*> optionMaybe (P.quotedPayee <|> P.unquotedPayee)
+  <*  eol
+  where
+    f (me, tml) pos dt fl nu pa = tl where
+      tl = U.TopLine dt fl nu pa me meta
+      meta = L.emptyTopLineMeta { L.topMemoLine = Just tml
+                                , L.topLineLine = Just tll }
+      tll = L.TopLineLine . sourceLine $ pos
+
+
+
+render ::
+  DT.DefaultTimeZone
+  -> T.TopLine
+  -> Maybe X.Text
+render dtz tl =
+  f
+  <$> M.render (T.tMemo tl)
+  <*> pure (DT.render dtz (T.tDateTime tl))
+  <*> renMaybe (T.tFlag tl) F.render
+  <*> renMaybe (T.tNumber tl) N.render
+  <*> renMaybe (T.tPayee tl) P.smartRender
+  where
+    f meX dtX flX nuX paX =
+      meX
+      `X.append` (txtWords [dtX, flX, nuX, paX])
+      `X.snoc` '\n'
diff --git a/Penny/Copper/Transaction.hs b/Penny/Copper/Transaction.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Transaction.hs
@@ -0,0 +1,75 @@
+module Penny.Copper.Transaction (transaction, render) where
+
+import Control.Applicative ((<$>), (<*>))
+import qualified Control.Monad.Exception.Synchronous as Ex
+import Data.Foldable (toList)
+import qualified Data.Traversable as Tr
+import qualified Data.Text as X
+import Text.Parsec (many)
+import Text.Parsec.Text ( Parser )
+
+import qualified Penny.Copper.DateTime as DT
+import qualified Penny.Copper.TopLine as TL
+import Penny.Copper.TopLine ( topLine )
+import qualified Penny.Copper.Posting as Po
+import qualified Penny.Copper.Qty as Qt
+import qualified Penny.Lincoln as L
+import Penny.Lincoln.Family (orphans)
+import qualified Penny.Lincoln.Family.Family as F
+import Penny.Lincoln.Family.Family ( Family ( Family ) )
+import qualified Penny.Lincoln.Transaction as T
+import qualified Penny.Lincoln.Transaction.Unverified as U
+
+errorStr :: T.Error -> String
+errorStr e = case e of
+  T.UnbalancedError -> "postings are not balanced"
+  T.CouldNotInferError -> "could not infer entry for posting"
+
+mkTransaction ::
+  U.TopLine
+  -> U.Posting
+  -> U.Posting
+  -> [U.Posting]
+  -> Ex.Exceptional String L.Transaction
+mkTransaction top p1 p2 ps = let
+  famTrans = Family top p1 p2 ps
+  errXact = T.transaction famTrans
+  in case errXact of
+    Ex.Exception err -> Ex.Exception . errorStr $ err
+    Ex.Success x -> return x
+
+maybeTransaction ::
+  DT.DefaultTimeZone
+  -> Qt.RadGroup
+  -> Parser (Ex.Exceptional String L.Transaction)
+maybeTransaction dtz rg =
+  mkTransaction
+  <$> topLine dtz
+  <*> Po.posting rg
+  <*> Po.posting rg
+  <*> many (Po.posting rg)
+
+transaction ::
+  DT.DefaultTimeZone
+  -> Qt.RadGroup
+  -> Parser L.Transaction
+transaction dtz rg = do
+  ex <- maybeTransaction dtz rg
+  case ex of
+    Ex.Exception s -> fail s
+    Ex.Success b -> return b
+
+render ::
+  DT.DefaultTimeZone
+  -> (Qt.GroupingSpec, Qt.GroupingSpec)
+  -> Qt.RadGroup
+  -> T.Transaction
+  -> Maybe X.Text
+render dtz gs rg txn = do
+  let txnFam = T.unTransaction txn
+  tlX <- TL.render dtz (F.parent txnFam)
+  pstgsX <- Tr.traverse (Po.render gs rg) (orphans txnFam)
+  return $ tlX `X.append` (X.concat (toList pstgsX))
+  
+
+
diff --git a/Penny/Copper/Util.hs b/Penny/Copper/Util.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Copper/Util.hs
@@ -0,0 +1,150 @@
+module Penny.Copper.Util where
+
+import Control.Applicative ((<*), pure, (<$))
+import qualified Data.Char as C
+import qualified Data.Foldable as F
+import qualified Data.List.NonEmpty as NE
+import qualified Data.Text as X
+import qualified Penny.Lincoln.HasText as HT
+import qualified Penny.Lincoln.TextNonEmpty as TNE
+import Text.Parsec (char, many, skipMany)
+import Text.Parsec.Text (Parser)
+
+rangeLettersToSymbols :: Char -> Bool
+rangeLettersToSymbols c = case C.generalCategory c of
+  C.UppercaseLetter -> True
+  C.LowercaseLetter -> True
+  C.TitlecaseLetter -> True
+  C.ModifierLetter -> True
+  C.OtherLetter -> True
+  C.DecimalNumber -> True
+  C.LetterNumber -> True
+  C.OtherNumber -> True
+  C.ConnectorPunctuation -> True
+  C.DashPunctuation -> True
+  C.OpenPunctuation -> True
+  C.ClosePunctuation -> True
+  C.InitialQuote -> True
+  C.FinalQuote -> True
+  C.OtherPunctuation -> True
+  C.MathSymbol -> True
+  C.CurrencySymbol -> True
+  C.ModifierSymbol -> True
+  C.OtherSymbol -> True
+  _ -> False
+
+rangeLetters :: Char -> Bool
+rangeLetters c = case C.generalCategory c of
+  C.UppercaseLetter -> True
+  C.LowercaseLetter -> True
+  C.TitlecaseLetter -> True
+  C.ModifierLetter -> True
+  C.OtherLetter -> True
+  _ -> False
+
+rangeMathCurrency :: Char -> Bool
+rangeMathCurrency c = case C.generalCategory c of
+  C.MathSymbol -> True
+  C.CurrencySymbol -> True
+  _ -> False
+
+rangeSymbols :: Char -> Bool
+rangeSymbols c = case C.generalCategory c of
+  C.MathSymbol -> True
+  C.CurrencySymbol -> True
+  C.ModifierSymbol -> True
+  C.OtherSymbol -> True
+  _ -> False
+
+rangeLettersNumbers :: Char -> Bool
+rangeLettersNumbers c = case C.generalCategory c of
+  C.UppercaseLetter -> True
+  C.LowercaseLetter -> True
+  C.TitlecaseLetter -> True
+  C.ModifierLetter -> True
+  C.OtherLetter -> True
+  C.DecimalNumber -> True
+  C.LetterNumber -> True
+  C.OtherNumber -> True
+  _ -> False
+
+-- | Creates a new parser that behaves like the old one, but also
+-- parses any whitespace remaining afterward.
+lexeme :: Parser a -> Parser a
+lexeme p = p <* skipMany (char ' ')
+
+-- | Parses any trailing whitespace followed by a newline followed by
+-- additional whitespace.
+eol :: Parser ()
+eol = pure ()
+      <* skipMany (char ' ')
+      <* char '\n'
+      <* skipMany (char ' ')
+
+-- | Parses a run of spaces.
+spaces :: Parser ()
+spaces = () <$ many (char ' ')
+
+-- | Applied to a non-empty list of pairs, with the first element of
+-- the pair being a predicate that returns True if a character is OK
+-- and the second element being something of an arbitrary type, and to
+-- something that has a Text. The pairs must be ordered from most
+-- restrictive to least restrictive predicates. If at least one of the
+-- predicates indicates that the Text is valid, returns the leftmost b
+-- associated with that predicate. If none of the predicates indicates
+-- that the Text is valid, returns the rightmost error.
+--
+-- Here, most restrictive means the predicate that indicates True for
+-- the narrowest range of characters, while least restrictive means
+-- the predicate that indicates True for the widest range of
+-- characters.
+checkText ::
+  HT.HasText a
+  => NE.NonEmpty ((Char -> Bool), b)
+  -> a
+  -> Maybe b
+checkText ps a = let
+  t = HT.text a
+  results = fmap (g . f) ps where
+    f (p, b) = (X.find (not . p) t, b)
+    g (p, b) = case p of
+      Nothing -> Right b
+      Just c -> Left c
+  folder x y = case x of
+    Right b -> Right b
+    Left _ -> y
+  in case F.foldr1 folder results of
+    Left _ -> Nothing
+    Right b -> return b
+
+listIsOK ::
+  HT.HasTextNonEmptyList a
+  => (Char -> Bool) -- ^ Returns True for characters that are allowed
+  -> a
+  -> Bool
+listIsOK p = F.all (TNE.all p) . HT.textNonEmptyList
+
+firstCharOfListIsOK ::
+  HT.HasTextNonEmptyList a
+  => (Char -> Bool) -- ^ Returns True if the first character is allowed
+  -> a
+  -> Bool
+firstCharOfListIsOK p ls = let
+  firstText = NE.head . HT.textNonEmptyList $ ls
+  in p (TNE.first firstText)
+
+-- | Takes a field that may or may not be present and a function that
+-- renders it. If the field is not present at all, returns an empty
+-- Text. Otherwise will succeed or fail depending upon whether the
+-- rendering function succeeds or fails.
+renMaybe :: Maybe a -> (a -> Maybe X.Text) -> Maybe X.Text
+renMaybe mx f = case mx of
+  Nothing -> Just X.empty
+  Just a -> f a
+
+-- | Merges a list of words into one Text; however, if any given Text
+-- is empty, that Text is first dropped from the list.
+txtWords :: [X.Text] -> X.Text
+txtWords xs = case filter (not . X.null) xs of
+  [] -> X.empty
+  rs -> X.unwords rs
diff --git a/Penny/Liberty.hs b/Penny/Liberty.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Liberty.hs
@@ -0,0 +1,692 @@
+-- | Liberty - Penny command line parsing utilities
+--
+-- Both Cabin and Zinc share various functions that aid in parsing
+-- command lines. For instance both the Postings report and the Zinc
+-- postings filter use common command-line options. However, Zinc
+-- already depends on Cabin. To avoid a cyclic dependency whereby
+-- Cabin would also depend on Zinc, functions formerly in Zinc that
+-- Cabin will also find useful are relocated here, to Liberty.
+
+module Penny.Liberty (
+  Error(..),
+  MatcherFactory,
+  X.evaluate,
+  X.Token,
+  FilteredNum(FilteredNum, unFilteredNum),
+  SortedNum(SortedNum, unSortedNum),
+  LibertyMeta(filteredNum, sortedNum),
+  xactionsToFiltered,
+  ListLength(ListLength, unListLength),
+  ItemIndex(ItemIndex, unItemIndex),
+  PostFilterFn,
+  parseComparer,
+  processPostFilters,
+  parseTokenList,
+  parsePredicate,
+  
+  -- * Parsers
+  Operand,
+  parseOperand,
+  parsePostFilter,
+  parseMatcherSelect,
+  parseCaseSelect,
+  parseOperator,
+  Orderer,
+  parseSort
+  ) where
+
+import Control.Applicative ((<|>))
+import qualified Control.Monad.Exception.Synchronous as Ex
+import Control.Monad.Exception.Synchronous (
+  Exceptional(Exception))
+import Data.Char (toUpper)
+import Data.List (isPrefixOf, sortBy)
+import Data.Text (Text, pack)
+import qualified Data.Text as Text
+import qualified System.Console.MultiArg.Combinator as C
+import System.Console.MultiArg.Prim (Parser)
+import Text.Parsec (parse)
+  
+import Penny.Copper.DateTime (DefaultTimeZone, dateTime)
+import Penny.Copper.Qty (RadGroup, qty)
+
+import Penny.Lincoln.Family.Child (child, parent)
+import qualified Penny.Lincoln.Predicates as P
+import qualified Penny.Lincoln as L
+import qualified Penny.Lincoln.Queries as Q
+import qualified Penny.Liberty.Expressions as X
+
+import Text.Matchers.CaseSensitive (
+  CaseSensitive(Sensitive, Insensitive))
+import qualified Text.Matchers.Text as TM
+
+-- | A serial indicating how a post relates to all other postings that
+-- made it through the filtering phase.
+newtype FilteredNum = FilteredNum { unFilteredNum :: L.Serial }
+                      deriving Show
+
+-- | A serial indicating how a posting relates to all other postings
+-- that have been sorted.
+newtype SortedNum = SortedNum { unSortedNum :: L.Serial }
+                    deriving Show
+
+-- | All metadata from Liberty.
+data LibertyMeta =
+  LibertyMeta { filteredNum :: FilteredNum
+              , sortedNum :: SortedNum }
+  deriving Show
+
+
+-- | Parses a list of tokens. Returns Nothing if the token sequence is
+-- invalid (e.g. if two operators are next to each other) or if the
+-- resulting RPN expression is bad (there are tokens left on the stack
+-- after parsing). Otherwise, returns the expression.
+--
+-- An empty list will fail to parse. The function calling this one
+-- must deal with empty lists if those are a possibility.
+parseTokenList :: [X.Token a] -> Maybe a
+parseTokenList = X.evaluate
+
+-- | Parses a list of tokens to obtain a predicate. Deals with an
+-- empty list of tokens by returning a predicate that is always
+-- True. Fails if the list of tokens is not empty and the parse fails.
+parsePredicate :: [X.Token (a -> Bool)] -> Maybe (a -> Bool)
+parsePredicate ls = case ls of
+  [] -> return (const True)
+  ts -> parseTokenList ts
+
+-- | Takes a list of transactions, splits them into PostingChild
+-- instances, filters them, post-filters them, sorts them, and places
+-- them in Box instances with Filtered serials.
+xactionsToFiltered ::
+  
+  (L.PostFam -> Bool)
+  -- ^ The predicate to filter the transactions
+
+  -> [PostFilterFn]
+  -- ^ Post filter specs
+
+  -> (L.PostFam -> L.PostFam -> Ordering)
+  -- ^ The sorter
+
+  -> [L.Transaction]
+  -- ^ The transactions to work on (probably parsed in from Copper)
+  
+  -> [L.Box LibertyMeta]
+  -- ^ Sorted, filtered postings
+
+xactionsToFiltered pdct pfs s =
+  addSortedNum
+  . processPostFilters pfs
+  . sortBy (sorter s)
+  . addFilteredNum
+  . map toBox
+  . filter pdct
+  . concatMap L.postFam
+
+
+-- | Transforms a PostingChild into a Box.
+toBox :: L.PostFam -> L.Box ()
+toBox = L.Box ()
+
+-- | Takes a list of filtered boxes and adds the Filtered serials.
+
+addFilteredNum :: [L.Box a] -> [L.Box FilteredNum]
+addFilteredNum = L.serialItems f where
+  f ser = fmap (const (FilteredNum ser))
+
+-- | Wraps a PostingChild sorter to change it to a Box sorter.
+sorter :: (L.PostFam -> L.PostFam -> Ordering)
+          -> L.Box a
+          -> L.Box b
+          -> Ordering
+sorter f b1 b2 = f (L.boxPostFam b1) (L.boxPostFam b2)
+
+-- | Takes a list of Boxes with metadata and adds a Serial for the
+-- Sorted.
+addSortedNum ::
+  [L.Box FilteredNum]
+  -> [L.Box LibertyMeta]
+addSortedNum = L.serialItems f where
+  f ser = fmap g where
+    g filtNum = LibertyMeta filtNum (SortedNum ser)
+
+type MatcherFactory =
+  CaseSensitive
+  -> Text
+  -> Ex.Exceptional Text (Text -> Bool)
+type Operand = X.Operand (L.PostFam -> Bool)
+
+newtype ListLength = ListLength { unListLength :: Int }
+                     deriving (Eq, Ord, Show)
+newtype ItemIndex = ItemIndex { unItemIndex :: Int }
+                    deriving (Eq, Ord, Show)
+
+-- | Specifies options for the post-filter stage.
+type PostFilterFn = ListLength -> ItemIndex -> Bool
+
+
+processPostFilters :: [PostFilterFn] -> [a] -> [a]
+processPostFilters pfs ls = foldl processPostFilter ls pfs
+
+
+processPostFilter :: [a] -> PostFilterFn -> [a]
+processPostFilter as fn = map fst . filter fn' $ zipped where
+  len = ListLength $ length as
+  fn' (_, idx) = fn len (ItemIndex idx)
+  zipped = zip as [0..]
+  
+
+data Error = MakeMatcherFactoryError Text
+             | DateParseError
+             | BadPatternError Text
+             | BadNumberError Text
+             | BadQtyError Text
+             | BadSortKeyError Text
+             | BadComparator Text
+             | BadExpression
+             | BadColorName Text
+             | BadFieldName Text
+             | BadBackgroundArg Text
+             | UnexpectedWord Text Text
+             | BadCommodityError Text
+             deriving Show
+
+
+------------------------------------------------------------
+-- Operands
+------------------------------------------------------------
+
+-- | Given a String from the command line which represents a pattern,
+-- and a MatcherFactor, return a Matcher. Fails if the pattern is bad
+-- (e.g. it is not a valid regular expression).
+getMatcher ::
+  String
+  -> CaseSensitive
+  -> MatcherFactory
+  -> Ex.Exceptional Error (Text -> Bool)
+
+getMatcher s cs f = case f cs (pack s) of
+  Ex.Exception e -> Ex.Exception $ BadPatternError e
+  Ex.Success m -> return m
+
+-- | Parses comparers given on command line to a function.
+parseComparer ::
+  (Eq a, Ord a)
+  => String
+  -> Maybe (a -> a -> Bool)
+parseComparer t
+  | t == "<" = Just (<)
+  | t == "<=" = Just (<=)
+  | t == "==" = Just (==)
+  | t == "=" = Just (==)
+  | t == ">" = Just (>)
+  | t == ">=" = Just (>=)
+  | t == "/=" = Just (/=)
+  | t == "!=" = Just (/=)
+  | otherwise = Nothing
+
+parseDate :: DefaultTimeZone -> Text -> Exceptional Error L.DateTime
+parseDate dtz t = case parse (dateTime dtz) "" t of
+  Left _ -> Exception DateParseError
+  Right d -> return d
+
+date :: Parser (DefaultTimeZone -> Ex.Exceptional Error Operand)
+date =
+  let os = C.OptSpec ["date"] ['d'] (C.TwoArg f)
+      f a1 a2 dtz = do
+        cmp <- Ex.fromMaybe (BadComparator (pack a1))
+               (parseComparer a1)
+        dt <- parseDate dtz (pack a2)
+        return $ X.Operand (P.date (`cmp` dt))
+  in C.parseOption [os]
+
+current :: Parser (L.DateTime -> Operand)
+current =
+  let os = C.OptSpec ["current"] [] (C.NoArg f)
+      f dt = X.Operand (P.date (<= dt))
+  in C.parseOption [os]
+
+-- | Creates options that match against fields that are
+-- colon-separated. The operand added will return True if the
+-- posting's colon-separated option matches the pattern given, or
+-- False if it does not.
+sepOption ::
+  String
+  -- ^ Long option name
+  
+  -> Maybe Char
+  -- ^ Short option name, if there is one
+
+  -> (Text -> (Text -> Bool) -> L.PostFam -> Bool)
+  -- ^ When applied to a text that separates the different segments of
+  -- the field, a matcher, and a posting, this function returns True
+  -- if the posting matches the matcher or False if it does not.
+  
+  -> Parser (CaseSensitive
+             -> MatcherFactory
+             -> Ex.Exceptional Error Operand)
+sepOption str mc f =
+  let so = case mc of
+        Nothing -> []
+        Just c -> [c]
+      os = C.OptSpec [str] so (C.OneArg g)
+      g a cs fty = do
+        m <- getMatcher a cs fty
+        return $ X.Operand (f sep m)
+  in C.parseOption [os]
+
+sep :: Text
+sep = Text.singleton ':'
+
+-- | Parses exactly one integer; fails if it cannot read exactly one.
+parseInt :: String -> Exceptional Error Int
+parseInt t = let ps = reads t in
+  case ps of
+    [] -> Exception $ BadNumberError (pack t)
+    ((i, s):[]) -> if length s /= 0
+                   then Exception $ BadNumberError (pack t)
+                   else return i
+    _ -> Exception $ BadNumberError (pack t)
+
+-- | Creates options that take two arguments, with the first argument
+-- being the level to match, and the second being the pattern that
+-- should match against that level.  Adds an operand that returns True
+-- if the pattern matches.
+levelOption ::
+  String
+  -- ^ Long option name
+  
+  -> (Int -> (Text -> Bool) -> L.PostFam -> Bool)
+  -- ^ Applied to an integer, a matcher, and a PostingBox, this
+  -- function returns True if a particular field in the posting
+  -- matches the matcher given at the given level, or False otherwise.
+  
+  -> Parser (CaseSensitive
+             -> MatcherFactory -> Exceptional Error Operand)
+  
+levelOption str f =
+  let os = C.OptSpec [str] [] (C.TwoArg g)
+      g a1 a2 cs fty = do
+        n <- parseInt a1
+        m <- getMatcher a2 cs fty
+        return $ X.Operand (f n m)
+  in C.parseOption [os]
+
+-- | Creates options that add an operand that matches the posting if a
+-- particluar field matches the pattern given.
+patternOption ::
+  String
+  -- ^ Long option
+  
+  -> Maybe Char
+  -- ^ Short option, if included
+
+  -> ((Text -> Bool) -> L.PostFam -> Bool)
+  -- ^ When applied to a matcher and a PostingBox, this function
+  -- returns True if the posting matches, or False if it does not.
+  
+  -> Parser (CaseSensitive
+             -> MatcherFactory -> Exceptional Error Operand)
+patternOption str mc f =
+  let so = maybe [] (\c -> [c]) mc
+      os = C.OptSpec [str] so (C.OneArg g)
+      g a1 cs fty = do
+        m <- getMatcher a1 cs fty
+        return $ X.Operand (f m)
+  in C.parseOption [os]
+
+-- | The account option; matches if the pattern given matches the
+-- colon-separated account name.
+account :: Parser (CaseSensitive
+                   -> MatcherFactory -> Exceptional Error Operand)
+account = sepOption "account" (Just 'a') P.account
+
+-- | The account-level option; matches if the account at the given
+-- level matches.
+accountLevel :: Parser (CaseSensitive
+                        -> MatcherFactory -> Exceptional Error Operand)
+accountLevel = levelOption "account-level" P.accountLevel
+
+-- | The accountAny option; returns True if the matcher given matches
+-- a single sub-account name at any level.
+accountAny :: Parser (CaseSensitive
+                      -> MatcherFactory -> Exceptional Error Operand)
+accountAny = patternOption "account-any" Nothing P.accountAny
+
+-- | The payee option; returns True if the matcher matches the payee
+-- name.
+payee :: Parser (CaseSensitive
+                 -> MatcherFactory -> Exceptional Error Operand)
+payee = patternOption "payee" (Just 'p') P.payee
+
+tag :: Parser (CaseSensitive
+               -> MatcherFactory -> Exceptional Error Operand)
+tag = patternOption "tag" (Just 't') P.tag
+
+number :: Parser (CaseSensitive
+                  -> MatcherFactory -> Exceptional Error Operand)
+number = patternOption "number" Nothing P.number
+
+flag :: Parser (CaseSensitive
+                -> MatcherFactory -> Exceptional Error Operand)
+flag = patternOption "flag" Nothing P.flag
+
+commodity :: Parser (CaseSensitive
+                     -> MatcherFactory -> Exceptional Error Operand)
+commodity = sepOption "commodity" Nothing P.commodity
+
+commodityLevel :: Parser (CaseSensitive
+                          -> MatcherFactory -> Exceptional Error Operand)
+commodityLevel = levelOption "commodity-level" P.commodityLevel
+
+commodityAny :: Parser (CaseSensitive
+                        -> MatcherFactory -> Exceptional Error Operand)
+commodityAny = patternOption "commodity" Nothing P.commodityAny
+
+postingMemo :: Parser (CaseSensitive
+                       -> MatcherFactory -> Exceptional Error Operand)
+postingMemo = patternOption "posting-memo" Nothing P.postingMemo
+
+transactionMemo :: Parser (CaseSensitive
+                           -> MatcherFactory -> Exceptional Error Operand)
+transactionMemo = patternOption "transaction-memo"
+                  Nothing P.transactionMemo
+
+debit :: Parser Operand
+debit =
+  let os = C.OptSpec ["debit"] [] (C.NoArg (X.Operand P.debit))
+  in C.parseOption [os]
+
+credit :: Parser Operand
+credit =
+  let os = C.OptSpec ["credit"] [] (C.NoArg (X.Operand P.credit))
+  in C.parseOption [os]
+
+qtyOption :: Parser (RadGroup -> Exceptional Error Operand)
+qtyOption =
+  let os = C.OptSpec ["qty"] [] (C.TwoArg f)
+      f a1 a2 rg = do
+        q <- case parse (qty rg) "" (pack a2) of
+          Left _ -> Ex.throw $ BadQtyError (pack a2)
+          Right g -> return g
+        cmp <- Ex.fromMaybe (BadComparator (pack a1))
+               (parseComparer a1)
+        return $ X.Operand (P.qty (`cmp` q))
+  in C.parseOption [os]
+
+-- | Creates two options suitable for comparison of serial numbers,
+-- one for ascending, one for descending.
+serialOption ::
+
+  (L.PostFam -> Maybe L.Serial)
+  -- ^ Function that, when applied to a PostingChild, returns the serial
+  -- you are interested in.
+
+  -> String
+  -- ^ Name of the command line option, such as @global-transaction@
+
+  -> Parser (Exceptional Error Operand)
+  -- ^ Parses both descending and ascending serial options.
+
+serialOption getSerial n =
+  let osA = C.OptSpec [n] []
+            (C.TwoArg (f L.forward))
+      osD = C.OptSpec [addPrefix "rev" n] []
+            (C.TwoArg (f L.backward))
+      f getInt a1 a2 = do
+        cmp <- Ex.fromMaybe (BadComparator (pack a1))
+               (parseComparer a1)
+        i <- parseInt a2
+        let op pf = case getSerial pf of
+              Nothing -> False
+              Just ser -> getInt ser `cmp` i
+        return $ X.Operand op
+  in C.parseOption [osA, osD]
+
+-- | Takes a string, adds a prefix and capitalizes the first letter of
+-- the old string. e.g. applied to "rev" and "globalTransaction",
+-- returns "revGlobalTransaction".
+addPrefix :: String -> String -> String
+addPrefix pre suf = pre ++ suf' where
+  suf' = case suf of
+    "" -> ""
+    x:xs -> toUpper x : xs
+
+globalTransaction :: Parser (Exceptional Error Operand)
+globalTransaction =
+  let f = fmap L.unGlobalTransaction
+          . L.globalTransaction
+          . L.tMeta
+          . parent
+          . L.unPostFam
+  in serialOption f "globalTransaction"
+
+globalPosting :: Parser (Exceptional Error Operand)
+globalPosting =
+  let f = fmap L.unGlobalPosting
+          . L.globalPosting
+          . L.pMeta
+          . child
+          . L.unPostFam
+  in serialOption f "globalPosting"
+
+filePosting :: Parser (Exceptional Error Operand)
+filePosting =
+  let f = fmap L.unFilePosting
+          . L.filePosting
+          . L.pMeta
+          . child
+          . L.unPostFam
+  in serialOption f "filePosting"
+
+fileTransaction :: Parser (Exceptional Error Operand)
+fileTransaction =
+  let f = fmap L.unFileTransaction
+          . L.fileTransaction
+          . L.tMeta
+          . parent
+          . L.unPostFam
+  in serialOption f "fileTransaction"
+
+-- | Parses operands.
+parseOperand ::
+  Parser (L.DateTime
+          -> DefaultTimeZone
+          -> RadGroup
+          -> CaseSensitive
+          -> MatcherFactory
+          -> Ex.Exceptional Error Operand)
+
+parseOperand =
+  (do { f <- date; return (\_ dtz _ _ _ -> f dtz)})
+  <|> (do { f <- current; return (\dt _ _ _ _ -> return (f dt)) })
+  <|> wrapFactArg account
+  <|> wrapFactArg accountLevel
+  <|> wrapFactArg accountAny
+  <|> wrapFactArg payee
+  <|> wrapFactArg tag
+  <|> wrapFactArg number
+  <|> wrapFactArg flag
+  <|> wrapFactArg commodity
+  <|> wrapFactArg commodityLevel
+  <|> wrapFactArg commodityAny
+  <|> wrapFactArg postingMemo
+  <|> wrapFactArg transactionMemo
+  <|> (do { o <- debit; return (\_ _ _ _ _ -> return o) })
+  <|> (do { o <- credit; return (\_ _ _ _ _ -> return o) })
+  <|> (do { f <- qtyOption; return (\ _ _ rg _ _ -> f rg)})
+  <|> wrapNoArg globalTransaction
+  <|> wrapNoArg globalPosting
+  <|> wrapNoArg filePosting
+  <|> wrapNoArg fileTransaction
+
+
+wrapNoArg ::
+  Parser (Ex.Exceptional Error Operand)
+  -> Parser (L.DateTime
+             -> DefaultTimeZone
+             -> RadGroup
+             -> CaseSensitive
+             -> MatcherFactory
+             -> Ex.Exceptional Error Operand)
+wrapNoArg p = do
+  o <- p
+  return (\_ _ _ _ _ -> o)
+
+wrapFactArg ::
+  Parser (CaseSensitive -> MatcherFactory -> Exceptional Error Operand)
+  -> Parser (L.DateTime
+             -> DefaultTimeZone
+             -> RadGroup
+             -> CaseSensitive
+             -> MatcherFactory
+             -> Ex.Exceptional Error Operand)
+wrapFactArg p = do
+  f <- p
+  return (\_ _ _ cs fact -> f cs fact)
+
+------------------------------------------------------------
+-- Post filters
+------------------------------------------------------------
+
+optHead :: Parser (Exceptional Error PostFilterFn)
+optHead =
+  let os = C.OptSpec ["head"] [] (C.OneArg f)
+      f a = do
+        i <- fmap ItemIndex (parseInt a)
+        return (\_ idx -> idx < i)
+  in C.parseOption [os]
+
+optTail :: Parser (Exceptional Error PostFilterFn)
+optTail =
+  let os = C.OptSpec ["tail"] [] (C.OneArg f)
+      f a = do
+        i <- parseInt a
+        let g (ListLength len) (ItemIndex idx) = idx >= len - i
+        return g
+  in C.parseOption [os]
+
+parsePostFilter :: Parser (Exceptional Error PostFilterFn)
+parsePostFilter = optHead <|> optTail
+
+------------------------------------------------------------
+-- Matcher control
+------------------------------------------------------------
+
+noArg :: a -> String -> Parser a
+noArg a s = let os = C.OptSpec [s] "" (C.NoArg a)
+            in C.parseOption [os]
+
+parseInsensitive :: Parser CaseSensitive
+parseInsensitive =
+  let os = C.OptSpec ["case-insensitive"] ['i'] (C.NoArg Insensitive)
+  in C.parseOption [os]
+
+parseSensitive :: Parser CaseSensitive
+parseSensitive =
+  let os = C.OptSpec ["case-sensitive"] ['I'] (C.NoArg Sensitive)
+  in C.parseOption [os]
+
+
+within :: Parser MatcherFactory
+within = noArg (\c t -> return (TM.within c t)) "within"
+
+pcre :: Parser MatcherFactory
+pcre = noArg TM.pcre "pcre"
+
+posix :: Parser MatcherFactory
+posix = noArg TM.tdfa "posix"
+
+exact :: Parser MatcherFactory
+exact = noArg (\c t -> return (TM.exact c t)) "exact"
+
+parseMatcherSelect :: Parser MatcherFactory
+parseMatcherSelect = within <|> pcre <|> posix <|> exact
+
+parseCaseSelect :: Parser CaseSensitive
+parseCaseSelect = parseInsensitive <|> parseSensitive
+
+------------------------------------------------------------
+-- Operators
+------------------------------------------------------------
+
+-- | Open parentheses
+open :: Parser (X.Token a)
+open = noArg X.TokOpenParen "open"
+
+-- | Close parentheses
+close :: Parser (X.Token a)
+close = noArg X.TokCloseParen "close"
+
+-- | and operator
+parseAnd :: Parser (X.Token (a -> Bool))
+parseAnd = noArg X.tokAnd "and"
+
+-- | or operator
+parseOr :: Parser (X.Token (a -> Bool))
+parseOr = noArg X.tokOr "or"
+
+-- | not operator
+parseNot :: Parser (X.Token (a -> Bool))
+parseNot = noArg X.tokNot "not" where
+
+parseOperator :: Parser (X.Token (a -> Bool))
+parseOperator =
+  open <|> close <|> parseAnd <|> parseOr <|> parseNot
+
+------------------------------------------------------------
+-- Sorting
+------------------------------------------------------------
+type Orderer = L.PostFam -> L.PostFam -> Ordering
+
+ordering ::
+  (Ord b)
+  => (a -> b)
+  -> (a -> a -> Ordering)
+ordering q = f where
+  f p1 p2 = compare (q p1) (q p2)
+
+
+flipOrder :: (a -> a -> Ordering) -> (a -> a -> Ordering)
+flipOrder f = f' where
+  f' p1 p2 = case f p1 p2 of
+    LT -> GT
+    GT -> LT
+    EQ -> EQ
+
+capitalizeFirstLetter :: String -> String
+capitalizeFirstLetter s = case s of
+  [] -> []
+  (x:xs) -> toUpper x : xs
+
+ordPairs :: [(String, Orderer)]
+ordPairs = 
+  [ ("payee", ordering Q.payee)
+  , ("date", ordering Q.dateTime)
+  , ("flag", ordering Q.flag)
+  , ("number", ordering Q.number)
+  , ("account", ordering Q.account)
+  , ("drCr", ordering Q.drCr)
+  , ("qty", ordering Q.qty)
+  , ("commodity", ordering Q.commodity)
+  , ("postingMemo", ordering Q.postingMemo)
+  , ("transactionMemo", ordering Q.transactionMemo) ]
+
+ords :: [(String, Orderer)]
+ords = ordPairs ++ uppers where
+  uppers = map toReversed ordPairs
+  toReversed (s, f) =
+    (capitalizeFirstLetter s, flipOrder f)
+
+
+parseSort :: Parser (Exceptional Error Orderer)
+parseSort =
+  let os = C.OptSpec ["sort"] ['s'] (C.OneArg f)
+      f a =
+        let matches = filter (\p -> a `isPrefixOf` (fst p)) ords
+        in case matches of
+          [] -> Ex.throw $ BadSortKeyError (pack a)
+          x:[] -> return $ snd x
+          _ -> Ex.throw $ BadSortKeyError (pack a)
+  in C.parseOption [os]
diff --git a/Penny/Liberty/Expressions.hs b/Penny/Liberty/Expressions.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Liberty/Expressions.hs
@@ -0,0 +1,63 @@
+module Penny.Liberty.Expressions (
+  I.Precedence(Precedence),
+  
+  I.Associativity(ALeft, ARight),
+  
+  I.Token(TokOperand,
+          TokUnaryPostfix,
+          TokUnaryPrefix,
+          TokBinary,
+          TokOpenParen,
+          TokCloseParen),
+
+  R.Operand(Operand),
+  tokAnd,
+  tokOr,
+  tokNot,
+  evaluate) where
+
+import qualified Data.Foldable as Fdbl
+import Penny.Liberty.Expressions.Infix as I
+import Penny.Liberty.Expressions.RPN as R
+
+
+-- | Tokens should be enqueued from left to right, so that tokens on
+-- the left side of the sequence are those at the beginning of the
+-- expression.
+evaluate ::
+  Fdbl.Foldable l
+  => l (I.Token a)
+  -> Maybe a
+evaluate i = I.infixToRPN i >>= R.process
+
+-- | An And token which is left associative with precedence 3.
+tokAnd :: I.Token (a -> Bool)
+tokAnd = I.TokBinary (I.Precedence 3) I.ALeft f where
+  f x y = \a -> x a && y a
+
+-- | An Or token which is left associative with precedence 2.
+tokOr :: I.Token (a -> Bool)
+tokOr = I.TokBinary (I.Precedence 2) I.ALeft f where
+  f x y = \a -> x a || y a
+
+-- | A unary prefix Not token with precedence 4.
+tokNot :: I.Token (a -> Bool)
+tokNot = I.TokUnaryPrefix (I.Precedence 4) (not .)
+
+--
+-- Testing
+--
+_plus :: I.Token Double
+_plus = I.TokBinary (I.Precedence 4) I.ALeft (+)
+
+_minus :: I.Token Double
+_minus = I.TokBinary (I.Precedence 4) I.ALeft (-)
+
+_times :: I.Token Double
+_times = I.TokBinary (I.Precedence 5) I.ALeft (*)
+
+_divide :: I.Token Double
+_divide = I.TokBinary (I.Precedence 5) I.ALeft (/)
+
+_neg :: I.Token Double
+_neg = I.TokUnaryPrefix (I.Precedence 5) negate
diff --git a/Penny/Liberty/Expressions/Infix.hs b/Penny/Liberty/Expressions/Infix.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Liberty/Expressions/Infix.hs
@@ -0,0 +1,218 @@
+-- | Creates RPN expressions from infix inputs.
+--
+-- Penny accepts only infix expressions, but RPN expressions are
+-- easier to process. This module converts infix expressions to RPN
+-- expressions for further processing.
+--
+-- Uses the shunting-yard algorithm, best described at
+-- http://www.chris-j.co.uk/parsing.php (be sure to use the \"click to
+-- display\" links).
+module Penny.Liberty.Expressions.Infix (
+
+  Precedence(Precedence),
+  
+  Associativity(ALeft,
+                ARight),
+  
+  Token(TokOperand,
+        TokUnaryPostfix,
+        TokUnaryPrefix,
+        TokBinary,
+        TokOpenParen,
+        TokCloseParen),
+  
+  infixToRPN
+  ) where
+
+import qualified Data.Foldable as Fdbl
+import qualified Penny.Liberty.Expressions.RPN as R
+import qualified Data.Sequence as Seq
+import Data.Sequence((|>), Seq)
+
+-- | Precedence can be any integer; the greater the number, the higher
+-- the precedence.
+newtype Precedence = Precedence Int deriving (Show, Eq, Ord)
+
+data Associativity = ALeft | ARight deriving Show
+
+data Token a =
+  TokOperand a
+  | TokUnaryPostfix (a -> a)
+  | TokUnaryPrefix Precedence (a -> a)
+  | TokBinary Precedence Associativity (a -> a -> a)
+  | TokOpenParen
+  | TokCloseParen
+
+instance (Show a) => Show (Token a) where
+  show (TokOperand a) = "<operand " ++ show a ++ ">"
+  show (TokUnaryPostfix _) = "<unary postfix>"
+  show (TokUnaryPrefix (Precedence i) _) =
+    "<unary prefix, precedence " ++ (show i) ++ ">"
+  show (TokBinary (Precedence i) a _) =
+    "<binary, precedence " ++ show i ++ " "
+    ++ show a ++ ">"
+  show TokOpenParen = "<OpenParen>"
+  show TokCloseParen = "<CloseParen>"
+
+data StackVal a =
+  StkUnaryPrefix Precedence (a -> a)
+  | StkBinary Precedence (a -> a -> a)
+  | StkOpenParen
+
+instance Show (StackVal a) where
+  show (StkUnaryPrefix p _) =
+    "<unary prefix, " ++ show p ++ ">"
+  show (StkBinary p _) =
+    "<binary, " ++ show p ++ ">"
+  show StkOpenParen = "<OpenParen>"
+
+-- | Converts an infix expression to an RPN expression.
+infixToRPN ::
+  Fdbl.Foldable l
+  => l (Token a)
+  -- ^ Input tokens. These should be in the sequence from left to
+  -- right in ordinary infix order. The easiest choice is a list,
+  -- though you might want to use Data.Sequence if many appends will
+  -- be needed to build the sequence.
+
+  -> Maybe (Seq (R.Token a))
+  -- ^ The resulting RPN expression. The token type here is a token
+  -- from Penny.Liberty.Expressions.RPN, which is a different type
+  -- than the Token in this module. Fails only if there is a close
+  -- parenthesis without a matching open parenthesis, or if there is
+  -- an open parenthesis without a matching close parenthesis. Other
+  -- nonsensical expressions will still parse to an RPN expression
+  -- successfully, so the RPN parser has to catch these errors.
+
+infixToRPN ls =
+  Fdbl.foldlM processToken ([], Seq.empty) ls
+  >>= popRemainingOperators
+
+
+-- | Process a single input token. Fails if the token is a close
+-- parenthesis and a matching open parenthesis is not
+-- found. Otherwise, succeeds and adjusts the stack and the output
+-- queue accordingly.
+processToken ::
+  ([StackVal a], Seq (R.Token a))
+  -> Token a
+  -> Maybe ([StackVal a], Seq (R.Token a))
+processToken (ss, ts) tok =
+  case tok of
+    TokOperand a -> Just (ss, processOperand a ts)
+    TokUnaryPostfix f ->
+      Just (ss, processUnaryPostfix f ts)
+    TokUnaryPrefix p f ->
+      Just (processUnaryPrefix p f ss, ts)
+    TokBinary p a f ->
+      Just (processBinary p a f (ss, ts))
+    TokOpenParen -> Just (processOpenParen ss, ts)
+    TokCloseParen -> processCloseParen (ss, ts)
+
+-- | If a token is an operand, append it to the postfix output.
+processOperand :: a -> Seq (R.Token a) -> Seq (R.Token a)
+processOperand a sq = sq |> (R.TokOperand (R.Operand a))
+
+-- | If a token is a unary postfix operator, append it to the postfix
+-- output.
+processUnaryPostfix ::
+  (a -> a)
+  -> Seq (R.Token a)
+  -> Seq (R.Token a)
+processUnaryPostfix f sq =
+  sq |> (R.TokOperator (R.Unary f))
+
+-- | If a token is a unary prefix operator, push it onto the stack.
+processUnaryPrefix ::
+  Precedence
+  -> (a -> a)
+  -> [StackVal a]
+  -> [StackVal a]
+processUnaryPrefix p f s = (StkUnaryPrefix p f):s
+
+-- | Pops tokens from the stack and appends them to the ouptut, as
+-- long as the token at the top of the stack is and operator and its
+-- precedence meets the given predicate.
+popTokens ::
+  (Precedence -> Bool)
+  -> ([StackVal a], Seq (R.Token a))
+  -> ([StackVal a], Seq (R.Token a))
+popTokens f (ss, os) =
+  case ss of
+    [] -> (ss, os)
+    x:xs -> case x of
+      StkOpenParen -> (ss, os)
+      StkUnaryPrefix p g -> popper (R.Unary g) p
+      StkBinary p g -> popper (R.Binary g) p
+      where
+        popper tok pr =
+          if f pr
+          then
+            let output' = os |> (R.TokOperator tok)
+            in popTokens f (xs, output')
+          else (ss, os)
+  
+
+-- | If the token is a binary operator A, then:
+--
+-- If A is left associative, while there is an operator B of higher or
+-- equal precedence than A at the top of the stack, pop B off the
+-- stack and append it to the output.
+--
+-- If A is right associative, while there is an operator B of higher
+-- precedence than A at the top of the stack, pop B off the stack and
+-- append it to the output.
+--
+-- Push A onto the stack.
+processBinary ::
+  Precedence
+  -> Associativity
+  -> (a -> a -> a)
+  -> ([StackVal a], Seq (R.Token a))
+  -> ([StackVal a], Seq (R.Token a))
+processBinary p a f pair =
+  let pdct = case a of
+        ALeft -> (>= p)
+        ARight -> (> p)
+      (ss, os) = popTokens pdct pair
+  in ((StkBinary p f):ss, os)
+
+-- | If the token is an opening parenthesis, push it onto the stack.
+processOpenParen :: [StackVal a] -> [StackVal a]
+processOpenParen = (StkOpenParen :)
+
+-- | If the token is a closing parenthesis, pop operators off the top
+-- of the stack and append them to the output until the operator at
+-- the top of the stack is an opening bracket. Pop the opening bracket
+-- off the stack.
+--
+-- Fails if no open paren is found.
+processCloseParen ::
+  ([StackVal a], Seq (R.Token a))
+  -> Maybe ([StackVal a], Seq (R.Token a))
+processCloseParen (ss, os) = case ss of
+  [] -> Nothing
+  (x:xs) ->
+    let popper op = processCloseParen (xs, output')
+          where
+            output' = os |> (R.TokOperator op)
+    in case x of
+      StkUnaryPrefix _ f -> popper (R.Unary f)        
+      StkBinary _ f -> popper (R.Binary f)
+      StkOpenParen -> Just (xs, os)
+
+-- | Removes all remaining operators from the stack and puts them on
+-- the output queue. Fails if the stack has an open parenthesis; as
+-- this is unmatched.
+popRemainingOperators ::
+  ([StackVal a], Seq (R.Token a))
+  -> Maybe (Seq (R.Token a))
+popRemainingOperators (s, os) = case s of
+  [] -> Just os
+  x:xs -> case x of
+    StkOpenParen -> Nothing
+    StkUnaryPrefix _ f -> pusher (R.Unary f)
+    StkBinary _ f -> pusher (R.Binary f)
+    where
+      pusher op = popRemainingOperators (xs, output') where
+        output' = os |> (R.TokOperator op)
diff --git a/Penny/Liberty/Expressions/RPN.hs b/Penny/Liberty/Expressions/RPN.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Liberty/Expressions/RPN.hs
@@ -0,0 +1,112 @@
+-- | Parses reverse polish notation expressions. This module needs
+-- much better error messages (right now it has none).
+--
+-- An RPN expression consists of operands and operators; a token is
+-- either an operand or an operator. For example, in the expression @5
+-- 4 +@, the @5@ and the @4@ are operands; the @+@ is an operator;
+-- each of these three is a token.
+module Penny.Liberty.Expressions.RPN (
+  Operand(Operand),
+  Operator(Unary, Binary),
+  Token(TokOperand, TokOperator),
+  process) where
+
+import qualified Data.Foldable as Fdbl
+
+-- | An operand; for example, in the expression @5 4 +@, @5@ and @4@
+-- are operands.
+newtype Operand a = Operand a deriving Show
+
+-- | Operators; for example, in the expression @5 4 +@, @+@ is an
+-- operator. Because this is RPN, there is no operator precedence.
+data Operator a =
+  Unary (a -> a)
+  -- ^ Unary operators take only one operand (for example, a factorial
+  -- operator).
+
+  | Binary (a -> a -> a)
+    -- ^ Binary operators take two operands (for example, an addition
+    -- operator).
+
+instance Show (Operator a) where
+  show (Unary _) = "<unary operator>"
+  show (Binary _) = "<binary operator>"
+
+-- | A token is either an operator or an operand.
+data Token a =
+  TokOperand (Operand a)
+  | TokOperator (Operator a)
+  deriving Show
+
+
+-- | Given an operator, and the stack of operands, process the
+-- operator. When parsing an RPN expression, encountering an operator
+-- at the front of the queue of tokens to be processed means that the
+-- correct number of tokens (one, for a unary operator, or two, for a
+-- binary operator) must be popped off the stack of operands. The
+-- operator is then applied to the operands. For a binary operator,
+-- the binary function is applied first to the operand that was lowest
+-- on the stack, and then to the operand that was higher up on the
+-- stack. The result of the operator is then pushed onto the top of
+-- the stack.
+--
+-- This function fails if there were insufficient operands on the
+-- stack to process the operator. If successful, returns the new
+-- stack, with the processed operands removed and the result of the
+-- operator pushed onto the top of the stack.
+processOperator ::
+  Operator a
+  -> [Operand a]
+  -> Maybe ([Operand a])
+processOperator t ds = case t of
+  (Unary f) -> case ds of
+    [] -> Nothing
+    (Operand x):xs -> return $ (Operand (f x)) : xs
+  (Binary f) -> case ds of
+    [] -> Nothing
+    (Operand x):dss -> case dss of
+      (Operand y):dsss ->
+        return $ (Operand (f y x)) : dsss
+      [] -> Nothing
+
+-- | Adds an operand to the top of the stack.
+processOperand ::
+  Operand a
+  -> [Operand a]
+  -> [Operand a]
+processOperand = (:)
+
+-- | Processes the next token. Fails if the next token is an operator
+-- and fails; otherwise, returns the new stack of operands.
+processToken ::
+  [Operand a]
+  -> Token a
+  -> Maybe ([Operand a])
+processToken s tok = case tok of
+  TokOperand d -> return (processOperand d s)
+  TokOperator t -> processOperator t s
+
+-- | Processes an entire input sequence of RPN tokens.
+process ::
+  Fdbl.Foldable l
+  => l (Token a)
+  -- ^ The tokens must be in the sequence from left to right in
+  -- postfix order; for example, @5 4 -@ will yield @1@. Typically
+  -- many appends will be required in order to build this sequence. If
+  -- performance is a concern, you can use a Data.Sequence; if the
+  -- list is small (as these lists will typically be) a regular list
+  -- will do just fine.
+
+  -> Maybe a
+  -- ^ Fails if there is not exactly one operand remaining on the
+  -- stack at the end of the parse, or if at any time there are
+  -- insufficient operands on the stack to parse an
+  -- operator. Otherwise, succeeds and returns the result.
+process ls = do
+  os <- Fdbl.foldlM processToken [] ls
+  (top, rest) <- case os of
+    (Operand x) : oss -> return (x, oss)
+    _ -> Nothing
+  case rest of
+    [] -> return top
+    _ -> Nothing
diff --git a/Penny/Lincoln.hs b/Penny/Lincoln.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln.hs
@@ -0,0 +1,210 @@
+-- | Lincoln - the Penny core
+--
+-- Penny's core types and classes are here. This module re-exports the
+-- most useful things. For more details you will want to look at the
+-- sub-modules. Also, not all types and functions are re-exported due
+-- to naming conflicts. In particular, neither
+-- "Penny.Lincoln.Predicates" nor "Penny.Lincoln.Queries" is exported
+-- from here due to the blizzard of name conflicts that would result.
+module Penny.Lincoln (
+  -- * Balances
+  B.Balance
+  , B.unBalance
+  , B.Balanced(Balanced, Inferable, NotInferable)
+  , B.isBalanced
+  , B.entryToBalance
+  , B.addBalances
+  , B.removeZeroCommodities
+  , B.BottomLine (Zero, NonZero)
+  , B.Column (Column)
+  
+    -- * Bits
+    -- ** Accounts
+  , I.SubAccountName (SubAccountName, unSubAccountName)
+  , I.Account(Account, unAccount)
+  
+    -- ** Amounts
+  , I.Amount (Amount, qty, commodity)
+  
+    -- ** Commodities
+  , I.Commodity (Commodity, unCommodity)
+  , I.SubCommodity (SubCommodity, unSubCommodity)
+  , I.charCommodity
+    
+    -- ** DateTime
+  , I.DateTime
+  , I.dateTime
+  , I.localTime
+  , I.timeZone
+  , I.TimeZoneOffset
+  , I.minsToOffset
+  , I.offsetToMins
+  , I.noOffset
+    
+    -- ** Debits and credits
+  , I.DrCr(Debit, Credit)
+  , I.opposite
+    
+    -- ** Entries
+  , I.Entry (Entry, drCr, amount)
+    
+    -- ** Flag
+  , I.Flag (Flag, unFlag)
+    
+    -- ** Memos
+  , I.MemoLine (MemoLine, unMemoLine)
+  , I.Memo (Memo, unMemo)
+    
+    -- ** Number
+  , I.Number (Number, unNumber)
+    
+    -- ** Payee
+  , I.Payee (Payee, unPayee)
+    
+    -- ** Prices and price points
+  , I.From(From, unFrom)
+  , I.To(To, unTo)
+  , I.CountPerUnit(CountPerUnit, unCountPerUnit)
+  , I.Price(from, to, countPerUnit)
+  , I.newPrice
+  , I.PricePoint(PricePoint, price, ppMeta)
+
+    -- ** Quantities                               
+  , I.Qty
+  , I.unQty
+  , I.partialNewQty
+  , I.newQty
+  , I.add
+  , I.subt
+  , I.mult
+  , I.zero
+  , I.difference
+  , I.Difference(LeftBiggerBy, RightBiggerBy, Equal)
+    
+    -- ** Tags
+  , I.Tag(Tag, unTag)
+  , I.Tags(Tags, unTags)
+    
+    
+    -- * Builders
+  , Bd.crashy
+  , Bd.account
+    
+    -- * Families
+    -- ** Family types
+  , F.Family(Family)
+  , F.Child(Child)
+  , F.Siblings(Siblings)
+    
+    -- ** Manipulating families
+  , F.children
+  , F.orphans
+  , F.adopt
+  , F.marryWith
+  , F.marry
+  , F.divorceWith
+  , F.divorce
+    
+    -- * HasText
+  , HT.HasText(text)
+  , HT.Delimited(Delimited)
+  , HT.HasTextList(textList)
+  , HT.HasTextNonEmpty(textNonEmpty)
+  , HT.HasTextNonEmptyList(textNonEmptyList)
+
+    -- * TextNonEmpty
+  , TNE.TextNonEmpty(TextNonEmpty)
+    
+    -- * Transactions
+    -- ** Postings and transactions
+  , T.Posting
+  , T.Transaction
+  , T.PostFam
+
+    -- ** Making transactions
+  , T.transaction
+  , T.Error ( UnbalancedError, CouldNotInferError)
+  
+    -- ** Querying postings
+  , T.Inferred(Inferred, NotInferred)
+  , T.pPayee
+  , T.pNumber
+  , T.pFlag
+  , T.pAccount
+  , T.pTags
+  , T.pEntry
+  , T.pMemo
+  , T.pInferred
+  , T.pMeta
+  , T.changePostingMeta
+
+    -- ** Querying transactions
+  , T.TopLine
+  , T.tDateTime
+  , T.tFlag
+  , T.tNumber
+  , T.tPayee
+  , T.tMemo
+  , T.tMeta
+  , T.changeTransactionMeta
+  , T.postFam
+    
+    -- ** Adding serials to transactions
+  , T.addSerialsToList
+  , T.addSerialsToEithers
+    
+    -- ** Unwrapping Transactions
+  , T.unTransaction
+  , T.unPostFam
+    
+    -- ** Transaction boxes
+  , T.Box (Box, boxMeta, boxPostFam)
+    
+  -- * Metadata
+  , M.TopLineLine(TopLineLine, unTopLineLine)
+  , M.TopMemoLine(TopMemoLine, unTopMemoLine)
+  , M.Side(CommodityOnLeft, CommodityOnRight)
+  , M.SpaceBetween(SpaceBetween, NoSpaceBetween)
+  , M.Format(Format, side, between)
+  , M.Filename(Filename, unFilename)
+  , M.PriceLine(PriceLine, unPriceLine)
+  , M.PostingLine(PostingLine, unPostingLine)
+  , M.PriceMeta(PriceMeta, priceLine, priceFormat)
+  , M.GlobalPosting(GlobalPosting, unGlobalPosting)
+  , M.FilePosting(FilePosting, unFilePosting)
+  , M.GlobalTransaction(GlobalTransaction, unGlobalTransaction)
+  , M.FileTransaction(FileTransaction, unFileTransaction)
+  , M.PostingMeta(PostingMeta, postingLine, postingFormat,
+                  globalPosting, filePosting)
+  , M.emptyPostingMeta
+  , M.TopLineMeta(TopLineMeta, topMemoLine, topLineLine, filename,
+                globalTransaction, fileTransaction)
+  , M.emptyTopLineMeta
+  
+    -- * PriceDb
+  , DB.PriceDb
+  , DB.emptyDb
+  , DB.addPrice
+  , DB.getPrice
+  , DB.PriceDbError(FromNotFound, ToNotFound, CpuNotFound)
+  , DB.convert
+    
+    -- * Serials
+  , S.Serial
+  , S.forward
+  , S.backward
+  , S.serials
+  , S.serialItems
+
+  ) where
+
+import qualified Penny.Lincoln.Balance as B
+import qualified Penny.Lincoln.Bits as I
+import qualified Penny.Lincoln.Builders as Bd
+import qualified Penny.Lincoln.Family as F
+import qualified Penny.Lincoln.HasText as HT
+import qualified Penny.Lincoln.Meta as M
+import qualified Penny.Lincoln.PriceDb as DB
+import qualified Penny.Lincoln.Serial as S
+import qualified Penny.Lincoln.TextNonEmpty as TNE
+import qualified Penny.Lincoln.Transaction as T
diff --git a/Penny/Lincoln/Balance.hs b/Penny/Lincoln/Balance.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Balance.hs
@@ -0,0 +1,107 @@
+module Penny.Lincoln.Balance ( 
+  Balance,
+  unBalance,
+  Balanced(Balanced, Inferable, NotInferable),
+  isBalanced,
+  entryToBalance,
+  addBalances,
+  removeZeroCommodities,
+  BottomLine(Zero, NonZero),
+  Column(Column, drCr, qty)
+  ) where
+
+import Data.Map ( Map )
+import qualified Data.Map as M
+import Data.Monoid ( Monoid, mempty, mappend )
+import qualified Data.Semigroup as Semi
+
+import Penny.Lincoln.Bits (
+  add, difference, Difference(LeftBiggerBy, RightBiggerBy, Equal))
+import qualified Penny.Lincoln.Bits as B
+
+-- | A balance summarizes several entries. You do not create a Balance
+-- directly. Instead, use 'entryToBalance'. Balance used to be a
+-- monoid, but there is nothing appropriate for mempty. Instead,
+-- Balance is really a semigroup, but not a monoid.
+newtype Balance = Balance (Map B.Commodity BottomLine)
+                  deriving (Show, Eq)
+
+-- | The map returned by unBalance is never empty.
+unBalance :: Balance -> Map B.Commodity BottomLine
+unBalance (Balance m) = m
+
+-- | Returned by 'isBalanced'.
+data Balanced = Balanced
+              | Inferable B.Entry
+              | NotInferable
+              deriving (Show, Eq)
+
+-- | Is this balance balanced?
+isBalanced :: Balance -> Balanced
+isBalanced (Balance m) = M.foldrWithKey f Balanced m where
+  f c n b = case n of
+    Zero -> b
+    (NonZero col) -> case b of
+      Balanced -> let
+        e = B.Entry dc a
+        dc = case drCr col of
+          B.Debit -> B.Credit
+          B.Credit -> B.Debit
+        q = qty col
+        a = B.Amount q c
+        in Inferable e
+      _ -> NotInferable
+
+-- | Converts an Entry to a Balance.
+entryToBalance :: B.Entry -> Balance
+entryToBalance (B.Entry dc am) = Balance $ M.singleton c no where
+  c = B.commodity am
+  no = NonZero (Column dc (B.qty am))
+
+data BottomLine = Zero
+            | NonZero Column
+            deriving (Show, Eq)
+
+instance Monoid BottomLine where
+  mempty = Zero
+  mappend n1 n2 = case (n1, n2) of
+    (Zero, Zero) -> Zero
+    (Zero, (NonZero c)) -> NonZero c
+    ((NonZero c), Zero) -> NonZero c
+    ((NonZero c1), (NonZero c2)) ->
+      let (Column dc1 q1) = c1
+          (Column dc2 q2) = c2
+      in if dc1 == dc2
+         then NonZero $ Column dc1 (q1 `add` q2)
+         else case difference q1 q2 of
+           LeftBiggerBy diff ->
+             NonZero $ Column dc1 diff
+           RightBiggerBy diff ->
+             NonZero $ Column dc2 diff
+           Equal -> Zero
+
+data Column = Column { drCr :: B.DrCr
+                     , qty :: B.Qty }
+              deriving (Show, Eq)
+
+-- | Add two Balances together. Commodities are never removed from the
+-- balance, even if their balance is zero. Instead, they are left in
+-- the balance. Sometimes you want to know that a commodity was in the
+-- account but its balance is now zero.
+addBalances :: Balance -> Balance -> Balance
+addBalances (Balance t1) (Balance t2) = 
+    Balance $ M.unionWith mappend t1 t2
+
+instance Semi.Semigroup Balance where
+  (<>) = addBalances
+
+-- | Removes zero balances from a Balance. Will not return a Balance
+-- with no commodities; instead, returns Nothing if there would be a
+-- balance with no commodities.
+removeZeroCommodities :: Balance -> Maybe Balance
+removeZeroCommodities (Balance m) =
+  let p b = case b of
+        Zero -> False
+        _ -> True
+      m' = M.filter p m
+  in if M.null m' then Nothing else Just (Balance m')
diff --git a/Penny/Lincoln/Bits.hs b/Penny/Lincoln/Bits.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits.hs
@@ -0,0 +1,74 @@
+-- | Essential data types used to make Transactions and Postings.
+module Penny.Lincoln.Bits (
+  -- * Accounts
+  Ac.SubAccountName(SubAccountName, unSubAccountName),
+  Ac.Account(Account, unAccount),
+
+  -- * Amounts
+  Am.Amount(Amount, qty, commodity),
+
+  -- * Commodities
+  C.Commodity(Commodity, unCommodity),
+  C.SubCommodity(SubCommodity, unSubCommodity),
+  C.charCommodity,
+
+  -- * DateTime
+  DT.DateTime,
+  DT.dateTime,
+  DT.localTime,
+  DT.timeZone,
+  DT.TimeZoneOffset,
+  DT.minsToOffset, DT.offsetToMins, DT.noOffset,
+
+  -- * Debits and Credits
+  DC.DrCr(Debit, Credit),
+  DC.opposite,
+  
+  -- * Entries
+  E.Entry(Entry, drCr, amount),
+
+  -- * Flag
+  F.Flag(Flag, unFlag),
+
+  -- * Memos
+  M.MemoLine(MemoLine, unMemoLine),
+  M.Memo(Memo, unMemo),
+
+  -- * Number
+  N.Number(Number, unNumber),
+
+  -- * Payee
+  Pa.Payee(Payee, unPayee),
+
+  -- * Prices and price points
+  Pr.From(From, unFrom), Pr.To(To, unTo),
+  Pr.CountPerUnit(CountPerUnit, unCountPerUnit),
+  Pr.Price(from, to, countPerUnit),
+  Pr.convert, Pr.newPrice,
+  PP.PricePoint(PricePoint, price, ppMeta),
+
+  -- * Quantities
+  Q.Qty, Q.unQty, Q.partialNewQty,
+  Q.newQty, Q.add, Q.subt, Q.mult, Q.zero,
+  Q.difference,
+  Q.Difference(Q.LeftBiggerBy, Q.RightBiggerBy, Q.Equal),
+
+  -- * Tags
+  T.Tag(Tag, unTag),
+  T.Tags(Tags, unTags)) where
+
+
+import qualified Penny.Lincoln.Bits.Account as Ac
+import qualified Penny.Lincoln.Bits.Amount as Am
+import qualified Penny.Lincoln.Bits.Commodity as C
+import qualified Penny.Lincoln.Bits.DateTime as DT
+import qualified Penny.Lincoln.Bits.DrCr as DC
+import qualified Penny.Lincoln.Bits.Entry as E
+import qualified Penny.Lincoln.Bits.Flag as F
+import qualified Penny.Lincoln.Bits.Memo as M
+import qualified Penny.Lincoln.Bits.Number as N
+import qualified Penny.Lincoln.Bits.Payee as Pa
+import qualified Penny.Lincoln.Bits.Price as Pr
+import qualified Penny.Lincoln.Bits.PricePoint as PP
+import qualified Penny.Lincoln.Bits.Qty as Q
+import qualified Penny.Lincoln.Bits.Tags as T
diff --git a/Penny/Lincoln/Bits/Account.hs b/Penny/Lincoln/Bits/Account.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/Account.hs
@@ -0,0 +1,12 @@
+module Penny.Lincoln.Bits.Account where
+
+import Penny.Lincoln.TextNonEmpty (TextNonEmpty)
+import Data.List.NonEmpty (NonEmpty)
+
+newtype SubAccountName =
+  SubAccountName { unSubAccountName :: TextNonEmpty }
+  deriving (Eq, Ord, Show)
+
+newtype Account = Account { unAccount :: NonEmpty SubAccountName }
+                  deriving (Eq, Show, Ord)
+
diff --git a/Penny/Lincoln/Bits/Amount.hs b/Penny/Lincoln/Bits/Amount.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/Amount.hs
@@ -0,0 +1,8 @@
+module Penny.Lincoln.Bits.Amount where
+
+import Penny.Lincoln.Bits.Qty ( Qty )
+import Penny.Lincoln.Bits.Commodity ( Commodity )
+
+data Amount = Amount { qty :: Qty
+                     , commodity :: Commodity }
+              deriving (Eq, Show, Ord)
diff --git a/Penny/Lincoln/Bits/Commodity.hs b/Penny/Lincoln/Bits/Commodity.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/Commodity.hs
@@ -0,0 +1,19 @@
+module Penny.Lincoln.Bits.Commodity where
+
+import Penny.Lincoln.TextNonEmpty (
+  TextNonEmpty ( TextNonEmpty ) )
+import Data.List.NonEmpty (NonEmpty((:|)))
+import Data.Text ( empty )
+
+newtype Commodity =
+  Commodity { unCommodity :: NonEmpty SubCommodity }
+  deriving (Eq, Ord, Show)
+
+newtype SubCommodity =
+  SubCommodity { unSubCommodity :: TextNonEmpty }
+  deriving (Eq, Ord, Show)
+
+-- | Creates a Commodity whose name is only a single character.
+charCommodity :: Char -> Commodity
+charCommodity c =
+  Commodity ((SubCommodity (TextNonEmpty c empty)) :| [])
diff --git a/Penny/Lincoln/Bits/DateTime.hs b/Penny/Lincoln/Bits/DateTime.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/DateTime.hs
@@ -0,0 +1,57 @@
+-- | Perhaps this could be called @moment@, as it aims to identify a
+-- moment in time. A DateTime is a combination of a LocalTime from
+-- Data.Time and a TimeZoneOffset. Previously a DateTime was simply a
+-- ZonedTime from Data.Time but ZonedTime has data that Penny does not
+-- need.
+module Penny.Lincoln.Bits.DateTime (
+  DateTime
+  , dateTime
+  , localTime
+  , timeZone
+  , TimeZoneOffset
+  , offsetToMins
+  , minsToOffset
+  , noOffset
+  ) where
+
+import qualified Data.Time as T
+
+-- | The number of minutes that this timezone is offset from UTC. Can
+-- be positive, negative, or zero.
+newtype TimeZoneOffset = TimeZoneOffset { offsetToMins :: Int }
+                         deriving (Eq, Ord, Show)
+
+-- | Convert minutes to a time zone offset. I'm having a hard time
+-- deciding whether to be liberal or strict in what to accept
+-- here. Currently it is somewhat strict in that it will fail if
+-- absolute value is greater than 840 minutes; currently the article
+-- at http://en.wikipedia.org/wiki/List_of_time_zones_by_UTC_offset
+-- says there is no offset greater than 14 hours, or 840 minutes.
+minsToOffset :: Int -> Maybe TimeZoneOffset
+minsToOffset m = if abs m > 840
+                 then Nothing
+                 else Just $ TimeZoneOffset m
+
+noOffset :: TimeZoneOffset
+noOffset = TimeZoneOffset 0
+
+-- | A DateTime is a UTC time that also remembers the local time from
+-- which it was set. The Eq and Ord instances will compare two
+-- DateTimes based on their equivalent UTC times.
+data DateTime = DateTime { localTime :: T.LocalTime
+                         , timeZone :: TimeZoneOffset }
+                   deriving Show
+
+-- | Construct a DateTime.
+dateTime :: T.LocalTime -> TimeZoneOffset -> DateTime
+dateTime = DateTime
+
+toUTC :: DateTime -> T.UTCTime
+toUTC (DateTime lt (TimeZoneOffset tzo)) = T.localTimeToUTC tz lt where
+  tz = T.minutesToTimeZone tzo
+
+instance Eq DateTime where
+  l == r = toUTC l == toUTC r
+
+instance Ord DateTime where
+  compare l r = compare (toUTC l) (toUTC r)
diff --git a/Penny/Lincoln/Bits/DrCr.hs b/Penny/Lincoln/Bits/DrCr.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/DrCr.hs
@@ -0,0 +1,9 @@
+module Penny.Lincoln.Bits.DrCr where
+
+data DrCr = Debit | Credit deriving (Eq, Show, Ord)
+
+-- | Debit returns Credit; Credit returns Debit
+opposite :: DrCr -> DrCr
+opposite d = case d of
+  Debit -> Credit
+  Credit -> Debit
diff --git a/Penny/Lincoln/Bits/Entry.hs b/Penny/Lincoln/Bits/Entry.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/Entry.hs
@@ -0,0 +1,9 @@
+module Penny.Lincoln.Bits.Entry where
+
+import Penny.Lincoln.Bits.Amount (Amount)
+import Penny.Lincoln.Bits.DrCr (DrCr)
+
+data Entry = Entry { drCr :: DrCr
+                   , amount :: Amount }
+             deriving (Eq, Show, Ord)
+
diff --git a/Penny/Lincoln/Bits/Flag.hs b/Penny/Lincoln/Bits/Flag.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/Flag.hs
@@ -0,0 +1,7 @@
+module Penny.Lincoln.Bits.Flag where
+
+import Penny.Lincoln.TextNonEmpty (TextNonEmpty)
+
+newtype Flag = Flag { unFlag :: TextNonEmpty }
+             deriving (Eq, Show, Ord)
+
diff --git a/Penny/Lincoln/Bits/Memo.hs b/Penny/Lincoln/Bits/Memo.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/Memo.hs
@@ -0,0 +1,9 @@
+module Penny.Lincoln.Bits.Memo where
+
+import Penny.Lincoln.TextNonEmpty (TextNonEmpty)
+
+newtype MemoLine = MemoLine { unMemoLine :: TextNonEmpty }
+                   deriving (Eq, Ord, Show)
+
+newtype Memo = Memo { unMemo :: [MemoLine] }
+             deriving (Eq, Show, Ord)
diff --git a/Penny/Lincoln/Bits/Number.hs b/Penny/Lincoln/Bits/Number.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/Number.hs
@@ -0,0 +1,6 @@
+module Penny.Lincoln.Bits.Number where
+
+import Penny.Lincoln.TextNonEmpty (TextNonEmpty)
+
+newtype Number = Number { unNumber :: TextNonEmpty }
+                 deriving (Eq, Show, Ord)
diff --git a/Penny/Lincoln/Bits/Payee.hs b/Penny/Lincoln/Bits/Payee.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/Payee.hs
@@ -0,0 +1,7 @@
+module Penny.Lincoln.Bits.Payee where
+
+import Penny.Lincoln.TextNonEmpty ( TextNonEmpty )
+
+newtype Payee = Payee { unPayee :: TextNonEmpty }
+              deriving (Eq, Show, Ord)
+
diff --git a/Penny/Lincoln/Bits/Price.hs b/Penny/Lincoln/Bits/Price.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/Price.hs
@@ -0,0 +1,43 @@
+module Penny.Lincoln.Bits.Price (
+  From ( From, unFrom ),
+  To ( To, unTo ),
+  CountPerUnit ( CountPerUnit, unCountPerUnit ),
+  Price ( from, to, countPerUnit ),
+  convert,
+  newPrice) where
+
+import Penny.Lincoln.Bits.Amount (Amount(Amount))
+import Penny.Lincoln.Bits.Commodity (Commodity)
+import Penny.Lincoln.Bits.Qty (Qty, mult)
+
+newtype From = From { unFrom :: Commodity }
+               deriving (Eq, Ord, Show)
+
+newtype To = To { unTo :: Commodity }
+             deriving (Eq, Ord, Show)
+
+newtype CountPerUnit = CountPerUnit { unCountPerUnit :: Qty }
+                       deriving (Eq, Ord, Show)
+
+data Price = Price { from :: From
+                   , to :: To
+                   , countPerUnit :: CountPerUnit }
+             deriving (Eq, Ord, Show)
+
+-- | Convert an amount from the From price to the To price. Fails if
+-- the From commodity in the Price is not the same as the commodity in
+-- the Amount.
+convert :: Price -> Amount -> Maybe Amount
+convert p (Amount q c) =
+  if (unFrom . from $ p) /= c
+  then Nothing
+  else let q' = q `mult` (unCountPerUnit . countPerUnit $ p)
+       in Just (Amount q' (unTo . to $ p))
+
+-- | Succeeds only if From and To are different commodities.
+newPrice :: From -> To -> CountPerUnit -> Maybe Price
+newPrice f t cpu =
+  if unFrom f == unTo t
+  then Nothing
+  else Just $ Price f t cpu
+
diff --git a/Penny/Lincoln/Bits/PricePoint.hs b/Penny/Lincoln/Bits/PricePoint.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/PricePoint.hs
@@ -0,0 +1,10 @@
+module Penny.Lincoln.Bits.PricePoint where
+
+import Penny.Lincoln.Bits.Price (Price)
+import Penny.Lincoln.Bits.DateTime (DateTime)
+import qualified Penny.Lincoln.Meta as M
+
+data PricePoint = PricePoint { dateTime :: DateTime
+                             , price :: Price
+                             , ppMeta :: M.PriceMeta }
+                  deriving Show
diff --git a/Penny/Lincoln/Bits/Qty.hs b/Penny/Lincoln/Bits/Qty.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/Qty.hs
@@ -0,0 +1,70 @@
+-- | Penny quantities. A quantity is simply a count (possibly
+-- fractional) of something. It does not have a commodity or a
+-- Debit/Credit.
+module Penny.Lincoln.Bits.Qty (
+  Qty, unQty, partialNewQty,
+  newQty, add, subt, mult,
+  zero, Difference(LeftBiggerBy, RightBiggerBy, Equal),
+  difference) where
+
+import Data.Decimal ( DecimalRaw ( Decimal ), Decimal )
+
+-- | A quantity is always greater than zero. Various odd questions
+-- happen if quantities can be zero. For instance, what if you have a
+-- debit whose quantity is zero? Does it require a balancing credit
+-- that is also zero? And how can you have a debit of zero anyway?
+--
+-- I can imagine situations where a quantity of zero might be useful;
+-- for instance maybe you want to specifically indicate that a
+-- particular posting in a transaction did not happen (for instance,
+-- that a paycheck deduction did not take place). I think the better
+-- way to handle that though would be through an addition to
+-- Debit/Credit - maybe Debit/Credit/Zero. Barring the addition of
+-- that, though, the best way to indicate a situation such as this
+-- would be through transaction memos.
+newtype Qty = Qty Decimal
+              deriving (Eq, Ord, Show)
+
+data Difference =
+  LeftBiggerBy Qty
+  | RightBiggerBy Qty
+  | Equal
+
+-- | Subtract the second Qty from the first.
+difference :: Qty -> Qty -> Difference
+difference (Qty q1) (Qty q2) = case compare q1 q2 of
+  GT -> LeftBiggerBy (Qty $ q1 - q2)
+  LT -> RightBiggerBy (Qty $ q2 - q1)
+  EQ -> Equal
+
+-- | Unwrap a Qty to get the underlying Decimal. This Decimal will
+-- always be greater than zero.
+unQty :: Qty -> Decimal
+unQty (Qty d) = d
+
+-- | Make a new Qty. This function is partial. It will call error if
+-- its argument is less than or equal to zero.
+partialNewQty :: Decimal -> Qty
+partialNewQty d =
+  if d <= 0
+  then error
+       $ "partialNewQty: argument less than or equal to zero: "
+       ++ show d
+  else Qty d
+
+-- | Make a new Qty. Returns Nothing if its argument is less than
+-- zero.
+newQty :: Decimal -> Maybe Qty
+newQty d = if d <= 0 then Nothing else Just (Qty d)
+
+add :: Qty -> Qty -> Qty
+add (Qty q1) (Qty q2) = Qty $ q1 + q2
+
+subt :: Qty -> Qty -> Maybe Qty
+subt (Qty q1) (Qty q2) = if q2 > q1 then Nothing else Just $ Qty (q1 - q2)
+
+mult :: Qty -> Qty -> Qty
+mult (Qty q1) (Qty q2) = Qty $ q1 * q2
+
+zero :: Qty
+zero = Qty $ Decimal 0 0
diff --git a/Penny/Lincoln/Bits/Tags.hs b/Penny/Lincoln/Bits/Tags.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Bits/Tags.hs
@@ -0,0 +1,10 @@
+module Penny.Lincoln.Bits.Tags where
+
+import Penny.Lincoln.TextNonEmpty (TextNonEmpty)
+
+newtype Tag = Tag { unTag :: TextNonEmpty }
+                  deriving (Eq, Show, Ord)
+
+newtype Tags = Tags { unTags :: [Tag] }
+               deriving (Eq, Show, Ord)
+
diff --git a/Penny/Lincoln/Builders.hs b/Penny/Lincoln/Builders.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Builders.hs
@@ -0,0 +1,62 @@
+-- | Partial functions that make common types in Lincoln. Some data
+-- types in Lincoln are deeply nested, with TextNonEmpty nested inside
+-- of a newtype, nested inside of a NonEmptyList, nested inside
+-- of... :) All the nesting ensures to the maximum extent possible
+-- that the type system reflects the restrictions that exist on
+-- Penny's data. For example, it would make no sense to have an empty
+-- account (that is, an account with no sub-accounts) or a sub-account
+-- whose name is an empty Text.
+--
+-- The disadvantage of the nesting is that building these data types
+-- can be tedious if, for example, you want to build some data within
+-- a short custom Haskell program. Thus, this module.
+
+module Penny.Lincoln.Builders ( 
+  crashy
+  , account
+  , commodity
+  ) where
+
+import Control.Monad.Exception.Synchronous as Ex
+import qualified Data.List.Split as S
+import Data.List.NonEmpty (NonEmpty((:|)))
+import qualified Penny.Lincoln.Bits as B
+import Penny.Lincoln.TextNonEmpty (TextNonEmpty(TextNonEmpty))
+import Data.Text (pack)
+import qualified Data.Traversable as T
+
+-- | Makes a function partial. Use if you don't want to bother dealing
+-- with the Exceptional type.
+crashy :: Show e => Ex.Exceptional e a -> a
+crashy = Ex.resolve (error . show)
+
+-- | Create an Account. You supply a single String, with colons to
+-- separate the different sub-accounts.
+account :: String -> Ex.Exceptional String B.Account
+account input = do
+  subStrs <- case S.splitOn ":" input of
+    [] -> error "splitOn returned an empty list; should never happen"
+    []:[] -> Ex.throw "account name is null"
+    (s:ss) -> return $ s :| ss
+  let makeSub s = case s of
+        [] -> Ex.throw $
+              "sub account name is null from account: " ++ input
+        (c:cs) -> return $ B.SubAccountName (TextNonEmpty c (pack cs))
+  subs <- T.traverse makeSub subStrs
+  return $ B.Account subs
+    
+-- | Create a Commodity. You supply a single String, with colons to
+-- separate the different sub-commodities.
+commodity :: String -> Ex.Exceptional String B.Commodity
+commodity input = do
+  subStrs <- case S.splitOn ":" input of
+    [] -> error "splitOn returned an empty list; should never happen"
+    []:[] -> Ex.throw "account name is null"
+    (s:ss) -> return $ s :| ss
+  let makeSub s = case s of
+        [] -> Ex.throw $
+              "sub account name is null from account: " ++ input
+        (c:cs) -> return $ B.SubCommodity (TextNonEmpty c (pack cs))
+  subs <- T.traverse makeSub subStrs
+  return $ B.Commodity subs
+    
diff --git a/Penny/Lincoln/Family.hs b/Penny/Lincoln/Family.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Family.hs
@@ -0,0 +1,102 @@
+-- | A Transaction consists of a TopLine (information common to all
+-- postings, such as the DateTime) and of at least two Postings. This
+-- data relationship is so important and useful that it is expressed
+-- in these modules. This module has its own functions and re-exports
+-- functions and types from other modules in this hierarchy. There are
+-- handy accessor functions for the records within each of the family
+-- types, but these are not exported due to name conflicts; if you
+-- want these simply import the necessary module (maybe qualified).
+module Penny.Lincoln.Family (
+  -- * Family types
+  F.Family(Family),
+  C.Child(Child),
+  S.Siblings(Siblings),
+  
+  -- * Mapping families
+  F.mapChildrenM,
+  F.mapChildren,
+  F.mapParentM,
+  F.mapParent,
+
+  -- * Functions to manipulate families
+  children,
+  orphans,
+  adopt,
+  marryWith,
+  marry,
+  divorceWith,
+  divorce,
+  S.collapse ) where
+
+import qualified Penny.Lincoln.Family.Family as F
+import qualified Penny.Lincoln.Family.Child as C
+import qualified Penny.Lincoln.Family.Siblings as S
+
+-- | Gets a family's children. The Child type contains information on
+-- the parent, and each Child contains information on the other
+-- Siblings.
+children :: F.Family p c -> S.Siblings (C.Child p c)
+children (F.Family p c1 c2 cRest) = S.Siblings fc sc rc where
+  fc = C.Child c1 c2 cRest p
+  sc = C.Child c2 c1 cRest p
+  rc = map toChild rest
+  rest = others cRest
+  toChild (c, cs) = C.Child c c1 (c2:cs) p
+
+-- | Separates the children from their parent.
+orphans :: F.Family p c -> S.Siblings c
+orphans (F.Family _ c1 c2 cs) = S.Siblings c1 c2 cs
+
+-- | Unites a parent and some siblings into one family; the dual of
+-- orphans.
+adopt :: p -> S.Siblings c -> F.Family p c
+adopt p (S.Siblings c1 c2 cs) = F.Family p c1 c2 cs
+
+-- | Marries two families into one. This function is rather cruel: if
+-- one family has more children than the other family, then the extra
+-- children are discarded. That is, all children must pair one-by-one.
+marryWith :: (p1 -> p2 -> p3)
+             -> (c1 -> c2 -> c3)
+             -> F.Family p1 c1
+             -> F.Family p2 c2
+             -> F.Family p3 c3
+marryWith fp fc (F.Family lp lc1 lc2 lcs) (F.Family rp rc1 rc2 rcs) =
+  F.Family (fp lp rp) (fc lc1 rc1) (fc lc2 rc2)
+  (zipWith fc lcs rcs)
+
+-- | marryWith a tupling function.
+marry :: F.Family p1 c1
+         -> F.Family p2 c2
+         -> F.Family (p1, p2) (c1, c2)
+marry = marryWith (,) (,)
+  
+-- | Splits up a family.
+divorceWith :: (p1 -> (p2, p3))
+             -> (c1 -> (c2, c3))
+             -> F.Family p1 c1
+             -> (F.Family p2 c2, F.Family p3 c3)
+divorceWith fp fc (F.Family p c1 c2 cs) = (f2, f3) where
+  f2 = F.Family p2 c21 c22 c2s
+  f3 = F.Family p3 c31 c32 c3s
+  (p2, p3) = fp p
+  (c21, c31) = fc c1
+  (c22, c32) = fc c2
+  cps = map fc cs
+  (c2s, c3s) = (map fst cps, map snd cps)
+
+-- | divorceWith an untupling function.
+divorce :: F.Family (p1, p2) (c1, c2)
+         -> (F.Family p1 c1, F.Family p2 c2)
+divorce = divorceWith id id
+
+others :: [a] -> [(a, [a])]
+others = map yank . allIndexes
+
+allIndexes :: [a] -> [(Int, [a])]
+allIndexes as = zip [0..] (replicate (length as) as)
+
+yank :: (Int, [a]) -> (a, [a])
+yank (i, as) = let
+  (ys, zs) = splitAt i as
+  in (head zs, ys ++ (tail zs))
+
diff --git a/Penny/Lincoln/Family/Child.hs b/Penny/Lincoln/Family/Child.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Family/Child.hs
@@ -0,0 +1,9 @@
+module Penny.Lincoln.Family.Child where
+
+-- | A Child has at least one sibling and a parent.
+data Child p c =
+  Child { child :: c
+        , sibling1 :: c
+        , siblings :: [c]
+        , parent :: p }
+  deriving Show
diff --git a/Penny/Lincoln/Family/Family.hs b/Penny/Lincoln/Family/Family.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Family/Family.hs
@@ -0,0 +1,50 @@
+module Penny.Lincoln.Family.Family where
+
+import qualified Data.Functor.Identity as I
+
+-- | A Family has one parent (ah, the anomie, sorry) and at least two
+-- children.
+data Family p c =
+  Family { parent :: p
+         , child1 :: c
+         , child2 :: c
+         , children :: [c] }
+  deriving (Eq, Show)
+
+-- | Maps over all children in a monad, in order starting with child
+-- 1, then child 2, then the children in the list from left to right.
+mapChildrenM ::
+  Monad m
+  => (a -> m b)
+  -> Family p a
+  -> m (Family p b)
+mapChildrenM f (Family p c1 c2 cs) = do
+  c1' <- f c1
+  c2' <- f c2
+  cs' <- mapM f cs
+  return $ Family p c1' c2' cs'
+
+
+-- | Maps over all children, in order starting with child
+-- 1, then child 2, then the children in the list from left to right.
+mapChildren ::
+  (a -> b)
+  -> Family p a
+  -> Family p b
+mapChildren f fam = I.runIdentity (mapChildrenM f' fam) where
+  f' = return . f
+
+-- | Maps over the parent in a monad.
+mapParentM ::
+  Monad m
+  => (a -> m b)
+  -> Family a c
+  -> m (Family b c)
+mapParentM f (Family p c1 c2 cs) = do
+  p' <- f p
+  return $ Family  p' c1 c2 cs
+
+-- | Maps over the parent.
+mapParent :: (a -> b) -> Family a c -> Family b c
+mapParent f fam = I.runIdentity (mapParentM f' fam) where
+  f' = return . f
diff --git a/Penny/Lincoln/Family/Siblings.hs b/Penny/Lincoln/Family/Siblings.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Family/Siblings.hs
@@ -0,0 +1,54 @@
+module Penny.Lincoln.Family.Siblings (
+  Siblings(Siblings, first, second, rest),
+  collapse
+  ) where
+
+import qualified Prelude as P
+import Prelude hiding (concat)
+import qualified Data.Semigroup as S
+import qualified Data.List.NonEmpty as NE
+import Data.List.NonEmpty (NonEmpty((:|)))
+import qualified Data.Foldable as Foldable
+import qualified Data.Traversable as T
+import Control.Applicative ((<*>), (<$>))
+
+-- | Describes the siblings of a family, but tells you nothing about
+-- the parent. There are always at least two Siblings.
+data Siblings a = Siblings { first :: a
+                           , second :: a
+                           , rest :: [a] }
+                  deriving (Eq, Show)
+
+instance S.Semigroup (Siblings a) where
+  (Siblings a1 a2 ar) <> (Siblings b1 b2 br) =
+    Siblings a1 a2 (ar ++ (b1:b2:br))
+
+instance Functor Siblings where
+  fmap g (Siblings f s rs) = Siblings (g f) (g s) (map g rs)
+
+instance Foldable.Foldable Siblings where
+  foldr g b (Siblings f s rs) = g f (g s (foldr g b rs))
+
+instance T.Traversable Siblings where
+  -- traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
+  traverse g (Siblings f s rs) =
+    Siblings
+    <$> g f
+    <*> g s
+    <*> T.traverse g rs
+
+-- | Change a Siblings of NonEmpty lists to a Siblings. The original
+-- order of the elements contained in the Siblings and within the
+-- NonEmpty lists is preserved.
+collapse :: Siblings (NE.NonEmpty a)
+            -> Siblings a
+collapse (Siblings (s1_1:|s1_r) s2@(s2_1:|s2_r) sr) =
+  Siblings r1 r2 rr where
+    r1 = s1_1
+    (r2, rr) = case s1_r of
+      [] -> (s2_1, (s2_r ++ concatNE sr))
+      x:xs -> (x, xs ++ concatNE (s2 : sr))
+
+concatNE :: [NE.NonEmpty a] -> [a]
+concatNE = foldr f [] where
+  f (a :| as) soFar = (a:as) ++ soFar
diff --git a/Penny/Lincoln/HasText.hs b/Penny/Lincoln/HasText.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/HasText.hs
@@ -0,0 +1,105 @@
+module Penny.Lincoln.HasText where
+
+import Data.Foldable (toList)
+import Data.List (intersperse)
+import Data.List.NonEmpty (NonEmpty)
+import Data.Text (Text, cons)
+import qualified Data.Text as X
+
+import qualified Penny.Lincoln.Bits as B
+import Penny.Lincoln.TextNonEmpty (TextNonEmpty(TextNonEmpty))
+
+class HasText a where
+  text :: a -> Text
+
+instance HasText Text where
+  text = id
+
+instance HasText TextNonEmpty where
+  text (TextNonEmpty f r) = f `cons` r
+
+instance HasText B.SubAccountName where
+  text = text . B.unSubAccountName
+
+instance HasText B.SubCommodity where
+  text = text . B.unSubCommodity
+
+instance HasText B.Flag where
+  text = text . B.unFlag
+
+instance HasText B.MemoLine where
+  text = text . B.unMemoLine
+
+instance HasText B.Number where
+  text = text . B.unNumber
+
+instance HasText B.Payee where
+  text = text . B.unPayee
+
+instance HasText B.Tag where
+  text = text . B.unTag
+  
+-- | Applying 'text' to a Delimited type will give you a single Text
+-- with the delimiter interspersed between the values of the list.
+data Delimited a = Delimited Text [a]
+                 deriving Show
+
+instance HasText a => HasText (Delimited a) where
+  text (Delimited sep ts) = X.concat . intersperse sep . map text $ ts
+
+class HasTextList a where
+  textList :: a -> [Text]
+
+instance HasText a => HasTextList (NonEmpty a) where
+  textList = map text . toList
+
+instance HasTextList B.Account where
+  textList = textList . B.unAccount
+
+instance HasTextList B.Commodity where
+  textList = textList . B.unCommodity
+
+instance HasTextList B.Tags where
+  textList = map text . B.unTags
+
+instance HasTextList B.Memo where
+  textList = map text . B.unMemo
+
+instance HasText a => HasTextList [a] where
+  textList = map text
+
+class HasTextNonEmpty a where
+  textNonEmpty :: a -> TextNonEmpty
+
+instance HasTextNonEmpty TextNonEmpty where
+  textNonEmpty = id
+
+instance HasTextNonEmpty B.SubAccountName where
+  textNonEmpty = B.unSubAccountName
+
+instance HasTextNonEmpty B.SubCommodity where
+  textNonEmpty = B.unSubCommodity
+
+instance HasTextNonEmpty B.Flag where
+  textNonEmpty = B.unFlag
+  
+instance HasTextNonEmpty B.Number where
+  textNonEmpty = B.unNumber
+
+instance HasTextNonEmpty B.Payee where
+  textNonEmpty = B.unPayee
+
+instance HasTextNonEmpty B.Tag where
+  textNonEmpty = B.unTag
+
+class HasTextNonEmptyList a where
+  textNonEmptyList :: a -> NonEmpty TextNonEmpty
+
+instance HasTextNonEmpty a => HasTextNonEmptyList (NonEmpty a) where
+  textNonEmptyList = fmap textNonEmpty
+
+instance HasTextNonEmptyList B.Account where
+  textNonEmptyList = fmap textNonEmpty . B.unAccount
+
+instance HasTextNonEmptyList B.Commodity where
+  textNonEmptyList = fmap textNonEmpty . B.unCommodity
diff --git a/Penny/Lincoln/Meta.hs b/Penny/Lincoln/Meta.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Meta.hs
@@ -0,0 +1,71 @@
+module Penny.Lincoln.Meta where
+
+import qualified Penny.Lincoln.Serial as S
+import qualified Data.Text as X
+
+newtype TopLineLine = TopLineLine { unTopLineLine :: Int }
+                      deriving (Eq, Show)
+
+newtype TopMemoLine = TopMemoLine { unTopMemoLine :: Int }
+                      deriving (Eq, Show)
+
+data Side = CommodityOnLeft | CommodityOnRight deriving (Eq, Show)
+data SpaceBetween = SpaceBetween | NoSpaceBetween deriving (Eq, Show)
+
+data Format =
+  Format { side :: Side
+         , between :: SpaceBetween }
+  deriving (Eq, Show)
+
+newtype Filename = Filename { unFilename :: X.Text }
+                   deriving (Eq, Show)
+
+newtype PriceLine = PriceLine { unPriceLine :: Int }
+                    deriving (Eq, Show)
+
+newtype PostingLine = PostingLine { unPostingLine :: Int }
+                      deriving (Eq, Show)
+
+data PriceMeta =
+  PriceMeta { priceLine :: Maybe PriceLine
+            , priceFormat :: Maybe Format }
+  deriving (Eq, Show)
+
+newtype GlobalPosting =
+  GlobalPosting { unGlobalPosting :: S.Serial }
+  deriving (Eq, Show)
+
+newtype FilePosting =
+  FilePosting { unFilePosting :: S.Serial }
+  deriving (Eq, Show)
+
+emptyPostingMeta :: PostingMeta
+emptyPostingMeta = PostingMeta Nothing Nothing Nothing Nothing
+
+data PostingMeta =
+  PostingMeta { postingLine :: Maybe PostingLine
+              , postingFormat :: Maybe Format
+              , globalPosting :: Maybe GlobalPosting
+              , filePosting :: Maybe FilePosting }
+  deriving (Eq, Show)
+
+newtype GlobalTransaction =
+  GlobalTransaction { unGlobalTransaction :: S.Serial }
+  deriving (Eq, Show)
+
+newtype FileTransaction =
+  FileTransaction { unFileTransaction :: S.Serial }
+  deriving (Eq, Show)
+
+
+emptyTopLineMeta :: TopLineMeta
+emptyTopLineMeta = TopLineMeta Nothing Nothing Nothing Nothing Nothing
+
+data TopLineMeta =
+  TopLineMeta { topMemoLine :: Maybe TopMemoLine
+              , topLineLine :: Maybe TopLineLine
+              , filename :: Maybe Filename
+              , globalTransaction :: Maybe GlobalTransaction
+              , fileTransaction :: Maybe FileTransaction }
+  deriving (Eq, Show)
+
diff --git a/Penny/Lincoln/NestedMap.hs b/Penny/Lincoln/NestedMap.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/NestedMap.hs
@@ -0,0 +1,268 @@
+-- | A nested map. The values in each NestedMap are tuples, with the
+-- first element of the tuple being a label that you select and the
+-- second value being another NestedMap. Functions are provided so you
+-- may query the map at any level or insert new labels (and,
+-- therefore, new keys) at any level.
+module Penny.Lincoln.NestedMap (
+  NestedMap ( NestedMap ),
+  unNestedMap,
+  empty,
+  relabel,
+  descend,
+  insert,
+  cumulativeTotal,
+  traverse,
+  traverseWithTrail ) where
+
+import Control.Applicative ((<*>), (<$>))
+import Data.Map ( Map )
+import qualified Data.Foldable as F
+import qualified Data.Traversable as T
+import qualified Data.Map as M
+import Data.Monoid ( Monoid, mconcat, mappend, mempty )
+
+newtype NestedMap k l =
+  NestedMap { unNestedMap :: Map k (l, NestedMap k l) }
+  deriving (Eq, Show, Ord)
+
+instance Functor (NestedMap k) where
+  fmap f (NestedMap m) = let
+    g (l, s) = (f l, fmap f s)
+    in NestedMap $ M.map g m
+
+instance (Ord k) => F.Foldable (NestedMap k) where
+  foldMap = T.foldMapDefault
+
+instance (Ord k) => T.Traversable (NestedMap k) where
+  -- traverse :: Applicative f
+  --          => (a -> f b)
+  --          -> NestedMap k a
+  --          -> f (NestedMap k b)
+  traverse f (NestedMap m) = let
+      f' (l, m') = (,) <$> f l <*> T.traverse f m'
+      in NestedMap <$> T.traverse f' m
+                 
+-- | An empty NestedMap.
+empty :: NestedMap k l
+empty = NestedMap (M.empty)
+
+-- | Helper function for relabel. For a given key and function
+-- that modifies the label, return the new submap to insert into the
+-- given map. Does not actually insert the submap though. That way,
+-- relabel can then modify the returned submap before
+-- inserting it into the mother map with the given label.
+newSubmap ::
+  (Ord k)
+  => NestedMap k l
+  -> k
+  -> (Maybe l -> l)
+  -> (l, NestedMap k l)
+newSubmap (NestedMap m) k g = (newL, NestedMap newM) where
+  (newL, newM) = case M.lookup k m of
+    Nothing -> (g Nothing, M.empty)
+    (Just (oldL, (NestedMap oldM))) -> (g (Just oldL), oldM)
+
+-- | Descends through a NestedMap with successive keys in the list,
+-- proceeding from left to right. At any given level, if the key
+-- given does not already exist, then inserts an empty submap and
+-- applies the given label modification function to Nothing to
+-- determine the new label. If the given key already does exist, then
+-- preserves the existing submap and applies the given label
+-- modification function to (Just oldlabel) to determine the new
+-- label.
+relabel ::
+  (Ord k)
+  => NestedMap k l
+  -> [(k, (Maybe l -> l))]
+  -> NestedMap k l
+relabel m [] = m
+relabel (NestedMap m) ((k, f):vs) = let
+  (newL, newM) = newSubmap (NestedMap m) k f
+  newM' = relabel newM vs
+  in NestedMap $ M.insert k (newL, newM') m
+
+-- | Given a list of keys, find the key that is furthest down in the
+-- map that matches the requested list of keys. Returns [(k, l)],
+-- where the first item in the list is the topmost key found and its
+-- matching label, and the last item in the list is the deepest key
+-- found and its matching label. (Often you will be most interested
+-- in the deepest key.)
+descend ::
+  Ord k
+  => [k]
+  -> NestedMap k l
+  -> [(k, l)]
+descend keys (NestedMap mi) = descend' keys mi where
+  descend' [] _ = []
+  descend' (k:ks) m = case M.lookup k m of
+    Nothing -> []
+    Just (l, (NestedMap im)) -> (k, l) : descend' ks im
+
+
+-- | Descends through the NestedMap one level at a time, proceeding
+-- key by key from left to right through the list of keys given. At
+-- the last key, appends the given label to the labels already
+-- present; if no label is present, uses mempty and mappend to create
+-- a new label. If the list of keys is empty, does nothing.
+insert ::
+  (Ord k, Monoid l)
+  => NestedMap k l
+  -> [k]
+  -> l
+  -> NestedMap k l
+insert m [] _ = m
+insert m ks l = relabel m ts where
+  ts = firsts ++ [end]
+  firsts = map (\k -> (k, keepOld)) (init ks) where
+    keepOld mk = case mk of
+      (Just old) -> old
+      Nothing -> mempty
+  end = (key, newL) where
+    key = last ks
+    newL mk = case mk of
+      (Just old) -> old `mappend` l
+      Nothing -> mempty `mappend` l
+  
+totalMap ::
+  (Monoid l)
+  => NestedMap k l
+  -> l
+totalMap (NestedMap m) =
+  if M.null m
+  then mempty
+  else mconcat . map totalTuple . M.elems $ m
+
+totalTuple ::
+  (Monoid l)
+  => (l, NestedMap k l)
+  -> l
+totalTuple (l, (NestedMap top)) =
+  if M.null top
+  then l
+  else mappend l (totalMap (NestedMap top))
+
+remapWithTotals ::
+  (Monoid l)
+  => NestedMap k l
+  -> NestedMap k l
+remapWithTotals (NestedMap top) =
+  if M.null top
+  then NestedMap M.empty
+  else NestedMap $ M.map f top where
+    f a@(_, m) = (totalTuple a, remapWithTotals m)
+
+-- | Leaves all keys of the map and submaps the same. Changes each
+-- label to reflect the total of that label and of all the labels of
+-- the maps within the NestedMap accompanying the label. Returns the
+-- total of the entire NestedMap.
+cumulativeTotal ::
+  (Monoid l)
+  => NestedMap k l
+  -> (l, NestedMap k l)
+cumulativeTotal m = (totalMap m, remapWithTotals m)
+
+-- | Supply a function that takes a key, a label, and a
+-- NestedMap. traverse will traverse the NestedMap. For each (label,
+-- NestedMap) pair, traverse will first apply the given function to
+-- the label before descending through the NestedMap. The function is
+-- applied to the present key and label and the accompanying
+-- NestedMap. The function you supply must return a Maybe. If the
+-- result is Nothing, then the pair is deleted as a value from its
+-- parent NestedMap. If the result is (Just s), then the label of this
+-- level of the NestedMap is changed to s before descending to the
+-- next level of the NestedMap.
+--
+-- All this is done in a monad, so you can carry out arbitrary side
+-- effects such as inspecting or changing a state or doing IO. If you
+-- don't need a monad, just use Identity.
+--
+-- Thus this function can be used to inspect, modify, and prune a
+-- NestedMap.
+--
+-- For a simpler traverse that does not provide you with so much
+-- information, NestedMap is also an instance of Data.Traversable.
+traverse ::
+  (Monad m, Ord k)
+  => (k -> l -> NestedMap k l -> m (Maybe a))
+  -> NestedMap k l
+  -> m (NestedMap k a)
+traverse f m = traverseWithTrail (\_ -> f) m
+
+-- | Like traverse, but the supplied function is also applied to a
+-- list that tells it about the levels of NestedMap that are parents
+-- to this NestedMap.
+traverseWithTrail ::
+  (Monad m, Ord k)
+  => ( [(k, l)] -> k -> l -> NestedMap k l -> m (Maybe a) )
+  -> NestedMap k l
+  -> m (NestedMap k a)
+traverseWithTrail f = traverseWithTrail' f []
+
+traverseWithTrail' ::
+  (Monad m, Ord k)
+  => ([(k, l)] -> k -> l -> NestedMap k l -> m (Maybe a))
+  -> [(k, l)]
+  -> NestedMap k l
+  -> m (NestedMap k a)
+traverseWithTrail' f ts (NestedMap m) =
+  if M.null m
+  then return $ NestedMap M.empty
+  else do
+    let ps = M.assocs m
+    mlsMaybes <- mapM (traversePairWithTrail f ts) ps
+    let ps' = zip (M.keys m) mlsMaybes
+        folder (k, ma) rs = case ma of
+          (Just r) -> (k, r):rs
+          Nothing -> rs
+        ps'' = foldr folder [] ps'
+    return (NestedMap (M.fromList ps''))
+
+traversePairWithTrail ::
+  (Monad m, Ord k)
+  => ( [(k, l)] -> k -> l -> NestedMap k l -> m (Maybe a) )
+  -> [(k, l)]
+  -> (k, (l, NestedMap k l))
+  -> m (Maybe (a, NestedMap k a))
+traversePairWithTrail f ls (k, (l, m)) = do
+  ma <- f ls k l m
+  case ma of
+    Nothing -> return Nothing
+    (Just a) -> do
+      m' <- traverseWithTrail' f ((k, l):ls) m
+      return (Just (a, m'))
+
+-- For testing
+_new :: (k, l) -> (k, (Maybe l -> l))
+_new (k, l) = (k, const l)
+
+_map1, _map2, _map3, _map4 :: NestedMap Int String
+_map1 = NestedMap M.empty
+_map2 = relabel _map1 [_new (5, "hello"), _new (66, "goodbye"), _new (777, "yeah")]
+_map3 = relabel _map2 [_new (6, "what"), _new (77, "zeke"), _new (888, "foo")]
+_map4 = relabel _map3
+       [ (6, (\m -> case m of Nothing -> "_new"; (Just s) -> s ++ "_new"))
+       , (77, (\m -> case m of Nothing -> "_new"; (Just s) -> s ++ "more _new")) ]
+
+_printer :: Int -> String -> a -> IO (Maybe ())
+_printer i s _ = do
+  putStrLn (show i)
+  putStrLn s
+  return $ Just ()
+
+_printerWithTrail :: [(Int, String)] -> Int -> String -> a -> IO (Maybe ())
+_printerWithTrail ps n str _ = do
+  let ptr (i, s) = putStr ("(" ++ show i ++ ", " ++ s ++ ") ")
+  mapM_ ptr . reverse $ ps
+  ptr (n, str)
+  putStrLn ""
+  return $ Just ()
+
+_showMap4 :: IO ()
+_showMap4 = do
+  _ <- traverse _printer _map4
+  return ()
+
+_showMapWithTrail :: IO ()
+_showMapWithTrail = do
+  _ <- traverseWithTrail _printerWithTrail _map4
+  return ()
diff --git a/Penny/Lincoln/Predicates.hs b/Penny/Lincoln/Predicates.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Predicates.hs
@@ -0,0 +1,136 @@
+-- | Functions that return a boolean based upon some criterion that
+-- matches something, often a PostingBox. Useful when filtering
+-- Postings.
+module Penny.Lincoln.Predicates where
+
+import Data.Text (Text, singleton)
+
+import qualified Penny.Lincoln.Bits as B
+import Penny.Lincoln.HasText (HasText, text, HasTextList, textList,
+                              Delimited(Delimited))
+import qualified Penny.Lincoln.Queries as Q
+import Penny.Lincoln.Transaction (PostFam)
+
+-- * Matching helpers
+
+match :: HasText a => (Text -> Bool) -> a -> Bool
+match f = f . text
+
+matchMaybe :: HasText a => (Text -> Bool) -> Maybe a -> Bool
+matchMaybe f a = case a of
+  Nothing -> False
+  (Just t) -> match f t
+
+matchAny :: HasTextList a => (Text -> Bool) -> a -> Bool
+matchAny f = any f . textList
+
+matchAnyMaybe :: HasTextList a => (Text -> Bool) -> Maybe a -> Bool
+matchAnyMaybe f ma = case ma of
+  Nothing -> False
+  (Just ls) -> matchAny f ls
+
+matchLevel :: HasTextList a => Int -> (Text -> Bool) -> a -> Bool
+matchLevel i f a = let ts = textList a in
+  if i < 0 || i >= length ts
+  then False
+  else f (ts !! i)
+
+matchMemo :: (Text -> Bool) -> B.Memo -> Bool
+matchMemo f m = f m' where
+  m' = text $ Delimited (singleton ' ') (textList m)
+  
+
+matchMaybeLevel ::
+  HasTextList a
+  => Int
+  -> (Text -> Bool)
+  -> Maybe a
+  -> Bool
+matchMaybeLevel i f ma = case ma of
+  Nothing -> False
+  (Just l) -> matchLevel i f l
+
+-- * Pattern matching fields
+
+payee :: (Text -> Bool) -> PostFam -> Bool
+payee f = matchMaybe f . Q.payee
+
+number :: (Text -> Bool) -> PostFam -> Bool
+number f = matchMaybe f . Q.number
+
+flag :: (Text -> Bool) -> PostFam -> Bool
+flag f = matchMaybe f . Q.flag
+
+postingMemo :: (Text -> Bool) -> PostFam -> Bool
+postingMemo f = matchMemo f . Q.postingMemo
+
+transactionMemo :: (Text -> Bool) -> PostFam -> Bool
+transactionMemo f = matchMemo f . Q.transactionMemo
+
+-- * Date
+
+date ::
+  (B.DateTime -> Bool)
+  -> PostFam
+  -> Bool
+date f c = f (Q.dateTime c)
+
+
+-- * Qty
+
+qty ::
+  (B.Qty -> Bool)
+  -> PostFam
+  -> Bool
+qty f c = f (Q.qty c)
+
+
+-- * DrCr
+drCr :: B.DrCr -> PostFam -> Bool
+drCr dc p = dc == Q.drCr p
+
+debit :: PostFam -> Bool
+debit p = Q.drCr p == B.Debit
+
+credit :: PostFam -> Bool
+credit p = Q.drCr p == B.Credit
+
+-- * Matching delimited fields
+
+matchDelimited ::
+  HasTextList a
+  => Text
+  -> (Text -> Bool)
+  -> a -> Bool
+matchDelimited d f = f . text . Delimited d . textList
+
+-- * Commodity
+
+commodity :: Text -> (Text -> Bool) -> PostFam -> Bool
+commodity t f = matchDelimited t f
+                . B.unCommodity
+                . Q.commodity
+
+commodityLevel :: Int -> (Text -> Bool) -> PostFam -> Bool
+commodityLevel i f = matchLevel i f . Q.commodity
+
+commodityAny :: (Text -> Bool) -> PostFam -> Bool
+commodityAny f = matchAny f . Q.commodity
+
+
+-- * Account
+account :: Text -> (Text -> Bool) -> PostFam -> Bool
+account t f = matchDelimited t f
+                . B.unAccount
+                . Q.account
+
+accountLevel :: Int -> (Text -> Bool) -> PostFam -> Bool
+accountLevel i f = matchLevel i f . Q.account
+
+accountAny :: (Text -> Bool) -> PostFam -> Bool
+accountAny f = matchAny f . Q.account
+
+-- * Tags
+tag :: (Text -> Bool) -> PostFam -> Bool
+tag f = matchAny f . Q.tags
+
diff --git a/Penny/Lincoln/PriceDb.hs b/Penny/Lincoln/PriceDb.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/PriceDb.hs
@@ -0,0 +1,139 @@
+-- | A database of price information. A PricePoint has a DateTime, a
+-- From commodity, a To commodity, and a QtyPerUnit. The PriceDb holds
+-- this information for several prices. You can query the database by
+-- supplying a from commodity, a to commodity, and a DateTime, and the
+-- database will give you the QtyPerUnit, if there is one.
+module Penny.Lincoln.PriceDb (
+  PriceDb,
+  emptyDb,
+  addPrice,
+  getPrice,
+  PriceDbError(FromNotFound, ToNotFound, CpuNotFound),
+  convert
+  ) where
+
+import qualified Control.Monad.Exception.Synchronous as Ex
+import qualified Data.Foldable as Fdbl
+import qualified Data.Map as M
+import qualified Data.Time as T
+import qualified Penny.Lincoln.NestedMap as NM
+import qualified Penny.Lincoln.Bits as B
+
+type CpuMap = M.Map T.UTCTime B.CountPerUnit
+type ToMap = M.Map B.To CpuMap
+
+-- | The PriceDb holds information about prices. Create an empty one
+-- using 'emptyDb' then fill it with values using foldl or similar.
+newtype PriceDb = PriceDb (NM.NestedMap B.SubCommodity ToMap)
+
+-- | An empty PriceDb
+emptyDb :: PriceDb
+emptyDb = PriceDb NM.empty
+
+-- | Add a single price to the PriceDb.
+addPrice :: PriceDb -> B.PricePoint -> PriceDb
+addPrice (PriceDb db) pp@(B.PricePoint _ pr _) =
+  PriceDb $ NM.relabel db ls where
+    ls = firsts ++ [lst]
+    cmdtyList = Fdbl.toList . B.unCommodity . B.unFrom . B.from $ pr
+    firsts = map toFst (init cmdtyList) where
+      toFst cty = (cty, f)
+      f maybeL = case maybeL of
+        Nothing -> M.empty
+        Just m -> m
+    lst = (last cmdtyList, insertIntoToMap pp)
+
+
+-- | Returns a function to use when inserting a new value into the
+-- ToMap label of a PriceDb.
+insertIntoToMap ::
+  B.PricePoint
+  -> Maybe ToMap
+  -> ToMap
+insertIntoToMap (B.PricePoint dt pr _) =
+  let toCmdty = B.to pr
+      newToMap oldMap = M.alter alterTo toCmdty oldMap
+      alterTo mayCpuMap =
+        let newKey = dateTimeToUTC dt
+            newVal = B.countPerUnit pr
+        in Just $ case mayCpuMap of
+          Nothing -> M.singleton newKey newVal
+          Just m -> M.insert newKey newVal m
+      relabeler maybeToMap =
+        newToMap (maybe M.empty id maybeToMap)
+  in relabeler
+
+dateTimeToUTC :: B.DateTime -> T.UTCTime
+dateTimeToUTC dt = T.localTimeToUTC tz lt where
+  tz = T.minutesToTimeZone . B.offsetToMins $ tzo
+  lt = B.localTime dt
+  tzo = B.timeZone dt
+
+
+-- | Getting prices can fail; if it fails, an Error is returned.
+data PriceDbError = FromNotFound | ToNotFound | CpuNotFound
+
+-- | Looks up values from the PriceDb. Throws "Error" if something
+-- fails.
+--
+-- First, tries to find the best possible From match. For example, if
+-- From is LUV:2001, first tries to see if there is a From match for
+-- LUV:2001. If there is not an exact match for LUV:2001 but there
+-- is a match for LUV, then LUV is used. If there is not a match
+-- for either LUV:2001 or for LUV, then FromNotFound is thrown.
+--
+-- The To commodity must match exactly. So, if the TO commodity is
+-- LUV:2001, only LUV:2001 will do. If the To commodity is not
+-- found, ToNotFound is thrown.
+--
+-- The DateTime is the time at which to find a price. If a price
+-- exists for that exact DateTime, that price is returned. If no price
+-- exists for that exact DateTime, but there is a price for an earlier
+-- DateTime, the latest possible price is returned. If there are no
+-- earlier prices, CpuNotFound is thrown.
+--
+-- There is no backtracking on earlier decisions. For example, if From
+-- is LUV:2001, and there is indeed at least one From price in the
+-- PriceDb and CpuNotFound occurs, getPrice does not check to see if
+-- the computation would have succeeded had it used LUV rather than
+-- LUV:2001.
+
+getPrice ::
+  PriceDb
+  -> B.From
+  -> B.To
+  -> B.DateTime
+  -> Ex.Exceptional PriceDbError B.CountPerUnit
+getPrice (PriceDb db) fr to dt = do
+  let utc = dateTimeToUTC dt
+      subs = Fdbl.toList . B.unCommodity . B.unFrom $ fr
+  toMap <- case NM.descend subs db of
+    [] -> Ex.throw FromNotFound
+    xs -> return . snd . last $ xs
+  cpuMap <- case M.lookup to toMap of
+    Nothing -> Ex.throw ToNotFound
+    Just m -> return m
+  let (lower, exact, _) = M.splitLookup utc cpuMap
+  cpu <- case exact of
+    Just c -> return c
+    Nothing ->
+      if M.null lower
+      then Ex.throw CpuNotFound
+      else return . snd . M.findMax $ lower
+  return cpu
+
+-- | Given an Amount and a Commodity to convert the amount to,
+-- converts the Amount to the given commodity. If the Amount given is
+-- already in the To commodity, simply returns what was passed in. Can
+-- fail and throw PriceDbError. Internally uses 'getPrice', so read its
+-- documentation for details on how price lookup works.
+convert ::
+  PriceDb
+  -> B.DateTime
+  -> B.To
+  -> B.Amount
+  -> Ex.Exceptional PriceDbError B.Amount
+convert db dt to (B.Amount qt fr) = do
+  cpu <- fmap B.unCountPerUnit (getPrice db (B.From fr) to dt)
+  let qt' = B.mult cpu qt
+  return $ B.Amount qt' (B.unTo to)
diff --git a/Penny/Lincoln/Queries.hs b/Penny/Lincoln/Queries.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Queries.hs
@@ -0,0 +1,92 @@
+module Penny.Lincoln.Queries where
+
+import qualified Penny.Lincoln.Bits as B
+import qualified Penny.Lincoln.Meta as M
+import Penny.Lincoln.Family.Child (child, parent)
+import qualified Penny.Lincoln.Transaction as T
+import Penny.Lincoln.Balance (Balance, entryToBalance)
+
+best ::
+  (T.Posting -> Maybe a)
+  -> (T.TopLine -> Maybe a)
+  -> T.PostFam
+  -> Maybe a
+best fp ft c = case fp . child . T.unPostFam $ c of
+  Just r -> Just r
+  Nothing -> ft . parent . T.unPostFam $ c
+
+
+payee :: T.PostFam -> Maybe B.Payee
+payee = best T.pPayee T.tPayee
+
+number :: T.PostFam -> Maybe B.Number
+number = best T.pNumber T.tNumber
+
+flag :: T.PostFam -> Maybe B.Flag
+flag = best T.pFlag T.tFlag
+
+postingMemo :: T.PostFam -> B.Memo
+postingMemo = T.pMemo . child . T.unPostFam
+
+transactionMemo :: T.PostFam -> B.Memo
+transactionMemo = T.tMemo . parent . T.unPostFam
+
+dateTime :: T.PostFam -> B.DateTime
+dateTime = T.tDateTime . parent . T.unPostFam
+
+account :: T.PostFam -> B.Account
+account = T.pAccount . child . T.unPostFam
+
+tags :: T.PostFam -> B.Tags
+tags = T.pTags . child . T.unPostFam
+
+entry :: T.PostFam -> B.Entry
+entry = T.pEntry . child . T.unPostFam
+
+balance :: T.PostFam -> Balance
+balance = entryToBalance . entry
+
+drCr :: T.PostFam -> B.DrCr
+drCr = B.drCr . entry
+
+amount :: T.PostFam -> B.Amount
+amount = B.amount . entry
+
+qty :: T.PostFam -> B.Qty
+qty = B.qty . amount
+
+commodity :: T.PostFam -> B.Commodity
+commodity = B.commodity . amount
+
+postingMeta :: T.PostFam -> M.PostingMeta
+postingMeta = T.pMeta . child . T.unPostFam
+
+topLineMeta :: T.PostFam -> M.TopLineMeta
+topLineMeta = T.tMeta . parent . T.unPostFam
+
+topMemoLine :: T.PostFam -> Maybe M.TopMemoLine
+topMemoLine = M.topMemoLine . topLineMeta
+
+topLineLine :: T.PostFam -> Maybe M.TopLineLine
+topLineLine = M.topLineLine . topLineMeta
+
+filename :: T.PostFam -> Maybe M.Filename
+filename = M.filename . topLineMeta
+
+globalTransaction :: T.PostFam -> Maybe M.GlobalTransaction
+globalTransaction = M.globalTransaction . topLineMeta
+
+fileTransaction :: T.PostFam -> Maybe M.FileTransaction
+fileTransaction = M.fileTransaction . topLineMeta
+
+postingLine :: T.PostFam -> Maybe M.PostingLine
+postingLine = M.postingLine . postingMeta
+
+postingFormat :: T.PostFam -> Maybe M.Format
+postingFormat = M.postingFormat . postingMeta
+
+globalPosting :: T.PostFam -> Maybe M.GlobalPosting
+globalPosting = M.globalPosting . postingMeta
+
+filePosting :: T.PostFam -> Maybe M.FilePosting
+filePosting = M.filePosting . postingMeta
diff --git a/Penny/Lincoln/Serial.hs b/Penny/Lincoln/Serial.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Serial.hs
@@ -0,0 +1,105 @@
+module Penny.Lincoln.Serial (
+  Serial, forward, backward, serials,
+  serialItems, serialItemsM,
+  serialChildrenInFamily, serialEithers,
+  NextFwd, NextBack, initNexts) where
+
+import Control.Monad (zipWithM)
+import Control.Monad.Trans.Class (lift)
+import qualified Data.Either as E
+import qualified Penny.Lincoln.Family as F
+import qualified Control.Monad.Trans.State as St
+
+-- | A type for serial numbers, used widely for different purposes
+-- throughout Penny.
+
+data Serial = Serial {
+  -- | Numbered from first to last, beginning at zero.
+  forward :: !Int
+  
+  -- | Numbered from last to first, ending at zero.
+  , backward :: !Int
+
+  } deriving (Eq, Show)
+             
+
+-- | Label a list of items with serials.
+serialItems :: (Serial -> a -> b)
+               -> [a]
+               -> [b]
+serialItems f as =
+  let ss = serials as
+  in zipWith f ss as
+
+-- | Label a list of items with serials, in a monad.
+serialItemsM ::
+  Monad m
+  => (Serial -> a -> m b)
+  -> [a]
+  -> m [b]
+serialItemsM f as =
+  let ss = serials as
+  in zipWithM f ss as
+
+-- | Applied to a list of items, return a list of Serials usable to
+-- identify the list of items.
+serials :: [a] -> [Serial]
+serials as = zipWith Serial fs rs where
+  len = length as
+  fs = take len . iterate succ $ 0
+  rs = take len . iterate pred $ (len - 1)
+
+
+serialChildrenInFamily ::
+  (Serial -> cOld -> cNew)
+  -> F.Family p cOld
+  -> St.State (NextFwd, NextBack) (F.Family p cNew)
+serialChildrenInFamily f = F.mapChildrenM (serialItemSt f)
+
+newtype NextFwd = NextFwd Int deriving Show
+newtype NextBack = NextBack Int deriving Show
+
+serialItemSt ::
+  (Serial -> cOld -> cNew)
+  -> cOld
+  -> St.State (NextFwd, NextBack) cNew
+serialItemSt f old = do
+  (NextFwd fwd, NextBack bak) <- St.get
+  let s = Serial fwd bak
+  St.put (NextFwd $ succ fwd, NextBack $ pred bak)
+  return (f s old)
+
+newtype Index = Index Int deriving (Eq, Ord, Show)
+newtype Total = Total Int deriving (Eq, Ord, Show)
+
+serialEithers ::
+  Monad m
+  => (Serial -> a -> m c)
+  -> (Serial -> b -> m d)
+  -> [Either a b]
+  -> m [Either c d]
+serialEithers fa fb ls =
+  let allA = E.lefts ls
+      allB = E.rights ls
+      totA = Total . length $ allA
+      totB = Total . length $ allB
+      initState = (0 :: Int, 0 :: Int)
+      k e = do
+        (nextA, nextB) <- St.get
+        case e of
+          Left a -> do
+            c <- lift $ fa (serial totA (Index nextA)) a
+            St.put (succ nextA, nextB)
+            return $ Left c
+          Right b -> do
+            d <- lift $ fb (serial totB (Index nextB)) b
+            St.put (nextA, succ nextB)
+            return $ Right d
+  in St.evalStateT (mapM k ls) initState
+
+serial :: Total -> Index -> Serial
+serial (Total t) (Index i) = Serial i b where
+  b = t - i - 1
+
+initNexts :: Int -> (NextFwd, NextBack)
+initNexts i = (NextFwd 0, NextBack $ i - 1)
diff --git a/Penny/Lincoln/TextNonEmpty.hs b/Penny/Lincoln/TextNonEmpty.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/TextNonEmpty.hs
@@ -0,0 +1,25 @@
+module Penny.Lincoln.TextNonEmpty (
+  TextNonEmpty (TextNonEmpty, first, rest),
+  unsafeTextNonEmpty,
+  any, all,
+  toText) where
+
+import Prelude hiding (any, all)
+import Data.Text ( Text, pack, cons )
+import qualified Data.Text as X
+
+data TextNonEmpty = TextNonEmpty { first :: Char
+                                 , rest :: Text }
+                    deriving (Eq, Ord, Show)
+
+unsafeTextNonEmpty :: String -> TextNonEmpty
+unsafeTextNonEmpty s = TextNonEmpty (head s) (pack . tail $ s)
+
+any :: (Char -> Bool) -> TextNonEmpty -> Bool
+any f (TextNonEmpty c ts) = f c || X.any f ts
+
+all :: (Char -> Bool) -> TextNonEmpty -> Bool
+all f (TextNonEmpty c ts) = f c && X.all f ts
+
+toText :: TextNonEmpty -> Text
+toText (TextNonEmpty t ts) = t `cons` ts
diff --git a/Penny/Lincoln/Transaction.hs b/Penny/Lincoln/Transaction.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Transaction.hs
@@ -0,0 +1,278 @@
+-- | Transactions, the heart of Penny. The Transaction data type is
+-- abstract, so that only this module can create Transactions. This
+-- provides assurance that if a Transaction exists, it is a valid,
+-- balanced Transaction. In addition, the Posting data type is
+-- abstract as well, so you know that if you have a Posting, it was
+-- created as part of a balanced Transaction.
+--
+-- Functions prefixed with a @p@ query a particular posting for its
+-- properties. Functions prefixed with a @t@ query transactions. Every
+-- transaction has a single DateTime, and all the postings have this
+-- same DateTime, so there is no function to query a posting's
+-- DateTime. Just query the parent transaction. For other things such
+-- as Number and Flag, the transaction might have data and the posting
+-- might have data as well, so functions are provided to query both.
+--
+-- Often you will want to query a single posting and have a function
+-- that gives you, for example, the posting's flag if it has one, or
+-- the transaction's flag if it has one, or Nothing if neither the
+-- posting nor the transaction has a flag. The functions in
+-- "Penny.Lincoln.Queries" do that.
+module Penny.Lincoln.Transaction (
+  
+  -- * Postings and transactions
+  Posting,
+  Transaction,
+  PostFam (unPostFam),
+
+  -- * Making transactions
+  transaction,
+  Error ( UnbalancedError, CouldNotInferError),
+  
+  -- * Querying postings
+  Inferred(Inferred, NotInferred),
+  pPayee, pNumber, pFlag, pAccount, pTags,
+  pEntry, pMemo, pInferred, pMeta, changePostingMeta,
+
+  -- * Querying transactions
+  TopLine,
+  tDateTime, tFlag, tNumber, tPayee, tMemo, tMeta,
+  unTransaction, postFam, changeTransactionMeta,
+  
+  -- * Adding serials
+  addSerialsToList, addSerialsToEithers,
+  
+  -- * Box
+  Box ( Box, boxMeta, boxPostFam )
+  
+  ) where
+
+import qualified Penny.Lincoln.Bits as B
+import Penny.Lincoln.Family ( children, orphans, adopt )
+import qualified Penny.Lincoln.Meta as M
+import qualified Penny.Lincoln.Family.Family as F
+import qualified Penny.Lincoln.Family.Child as C
+import qualified Penny.Lincoln.Family.Siblings as S
+import qualified Penny.Lincoln.Transaction.Unverified as U
+import qualified Penny.Lincoln.Balance as Bal
+import qualified Penny.Lincoln.Serial as Ser
+
+import Control.Monad.Exception.Synchronous (
+  Exceptional (Exception, Success) , throw )
+import qualified Control.Monad.Exception.Synchronous as Ex
+import qualified Data.Either as E
+import qualified Data.Foldable as Fdbl
+import Data.Maybe ( catMaybes )
+import qualified Data.Traversable as Tr
+import qualified Control.Monad.Trans.State.Lazy as St
+import Control.Monad.Trans.Class ( lift )
+
+-- | Indicates whether the entry for this posting was inferred. That
+-- is, if the user did not supply an entry for this posting, then it
+-- was inferred.
+data Inferred = Inferred | NotInferred deriving (Eq, Show)
+
+-- | Each Transaction consists of at least two Postings.
+data Posting =
+  Posting { pPayee   :: (Maybe B.Payee)
+          , pNumber  :: (Maybe B.Number)
+          , pFlag    :: (Maybe B.Flag)
+          , pAccount :: B.Account
+          , pTags    :: B.Tags
+          , pEntry   :: B.Entry
+          , pMemo    :: B.Memo
+          , pInferred :: Inferred
+          , pMeta     :: M.PostingMeta }
+  deriving (Eq, Show)
+
+-- | The TopLine holds information that applies to all the postings in
+-- a transaction (so named because in a ledger file, this information
+-- appears on the top line.)
+data TopLine =
+  TopLine { tDateTime :: B.DateTime
+          , tFlag     :: (Maybe B.Flag)
+          , tNumber   :: (Maybe B.Number)
+          , tPayee    :: (Maybe B.Payee)
+          , tMemo     :: B.Memo
+          , tMeta     :: M.TopLineMeta }
+  deriving (Eq, Show)
+
+-- | All the Postings in a Transaction must produce a Total whose
+-- debits and credits are equal. That is, the Transaction must be
+-- balanced. No Transactions are created that are not balanced.
+newtype Transaction =
+  Transaction { unTransaction :: F.Family TopLine Posting }
+  deriving (Eq, Show)
+  
+-- | Errors that can arise when making a Transaction.
+data Error = UnbalancedError
+           | CouldNotInferError
+           deriving (Eq, Show)
+
+newtype PostFam = PostFam { unPostFam :: C.Child TopLine Posting }
+                  deriving Show
+
+-- | Get the Postings from a Transaction, with information on the
+-- sibling Postings.
+postFam :: Transaction -> [PostFam]
+postFam (Transaction ps) = map PostFam . Fdbl.toList . children $ ps
+
+{- BNF-like grammar for the various sorts of allowed postings.
+
+postingGroup ::= (inferGroup balancedGroup*) | balancedGroup+
+inferGroup ::= "at least 1 posting. All postings have same account and
+                commodity. The balance is inferable."
+balancedGroup ::= "at least 2 postings. All postings have the same
+                   account and commodity. The balance is balanced."
+
+-}
+
+-- | Makes transactions.
+transaction ::
+  F.Family U.TopLine U.Posting
+  -> Exceptional Error Transaction
+transaction f@(F.Family p _ _ _) = do
+  let os = orphans f
+      t = totalAll os
+      p' = toTopLine p
+  a2 <- inferAll os t
+  return $ Transaction (adopt p' a2)
+
+totalAll :: S.Siblings U.Posting
+         -> Bal.Balance
+totalAll =
+  Fdbl.foldr1 Bal.addBalances
+  . catMaybes
+  . Fdbl.toList
+  . fmap (fmap Bal.entryToBalance . U.entry)
+
+infer ::
+  U.Posting
+  -> Ex.ExceptionalT Error
+  (St.State (Maybe B.Entry)) Posting
+infer po =
+  case U.entry po of
+    Nothing -> do
+      st <- lift St.get
+      case st of
+        Nothing -> Ex.throwT CouldNotInferError
+        (Just e) -> do
+          lift $ St.put Nothing
+          return $ toPosting po e Inferred
+    (Just e) -> return $ toPosting po e NotInferred
+          
+runInfer ::
+  Maybe B.Entry
+  -> S.Siblings U.Posting
+  -> Exceptional Error (S.Siblings Posting)
+runInfer me pos = do
+  let (res, finalSt) = St.runState ext me
+      ext = Ex.runExceptionalT (Tr.mapM infer pos)
+  case finalSt of
+    (Just _) -> throw UnbalancedError
+    Nothing -> case res of 
+      (Exception e) -> throw e
+      (Success g) -> return g
+
+inferAll ::
+  S.Siblings U.Posting
+  -> Bal.Balance
+  -> Exceptional Error (S.Siblings Posting)
+inferAll pos t = do
+  en <- case Bal.isBalanced t of
+    Bal.Balanced -> return Nothing
+    (Bal.Inferable e) -> return $ Just e
+    Bal.NotInferable -> throw UnbalancedError
+  runInfer en pos
+
+toPosting :: U.Posting
+             -> B.Entry
+             -> Inferred
+             -> Posting
+toPosting (U.Posting p n f a t _ m mt) e i =
+  Posting p n f a t e m i mt
+
+toTopLine :: U.TopLine -> TopLine
+toTopLine (U.TopLine d f n p m mt) =
+  TopLine d f n p m mt
+
+
+-- | Change the metadata on a transaction.
+changeTransactionMeta ::
+  (M.TopLineMeta -> M.TopLineMeta)
+  -- ^ Function that, when applied to the old top line meta, returns
+  -- the new meta.
+  
+  -> Transaction
+  -- ^ The old transaction with metadata
+
+  -> Transaction
+  -- ^ Transaction with new metadata
+
+changeTransactionMeta fm (Transaction f) = Transaction f' where
+  f' = F.Family tl c1 c2 cs
+  (F.Family p c1 c2 cs) = f
+  tl = p { tMeta = fm (tMeta tl) }
+
+-- | Change the metadata on a posting.
+changePostingMeta ::
+  (M.PostingMeta -> M.PostingMeta)
+  -> Transaction
+  -> Transaction
+changePostingMeta f (Transaction fam) =
+  Transaction . F.mapChildren g $ fam
+  where
+    g p = p { pMeta = f (pMeta p) }
+
+addSerials ::
+  (Ser.Serial -> M.TopLineMeta -> M.TopLineMeta)
+  -> (Ser.Serial -> M.PostingMeta -> M.PostingMeta)
+  -> Ser.Serial
+  -> Transaction
+  -> St.State (Ser.NextFwd, Ser.NextBack) Transaction
+addSerials ft fp s (Transaction fam) = do
+  let topMapper pm = pm { tMeta = ft s (tMeta pm) }
+      pstgMapper ser pstg = pstg { pMeta = fp ser (pMeta pstg) }
+      fam' = F.mapParent topMapper fam
+  fam'' <- Ser.serialChildrenInFamily pstgMapper fam'
+  return $ Transaction fam''
+
+addSerialsToList ::
+  (Ser.Serial -> M.TopLineMeta -> M.TopLineMeta)
+  -> (Ser.Serial -> M.PostingMeta -> M.PostingMeta)
+  -> [Transaction]
+  -> [Transaction]
+addSerialsToList ft fp ls =
+  let nPstgs = length . concatMap Fdbl.toList . map orphans
+               . map unTransaction $ ls
+      initState = Ser.initNexts nPstgs
+      processor = addSerials ft fp
+  in St.evalState (Ser.serialItemsM processor ls) initState
+
+
+addSerialsToEithers ::
+  (Ser.Serial -> M.TopLineMeta -> M.TopLineMeta)
+  -> (Ser.Serial -> M.PostingMeta -> M.PostingMeta)
+  -> [Either a Transaction]
+  -> [Either a Transaction]
+addSerialsToEithers ft fp ls =
+  let txns = E.rights ls
+      nPstgs = length . concatMap Fdbl.toList . map orphans
+               . map unTransaction $ txns
+      initState = Ser.initNexts nPstgs
+      processA _ a = return a
+      processTxn = addSerials ft fp
+      k = Ser.serialEithers processA processTxn ls
+  in St.evalState k initState
+
+-- | A box stores a family of transaction data along with
+-- metadata. The transaction is stored in child form, indicating a
+-- particular posting of interest. The metadata is in addition to the
+-- metadata associated with the TopLine and with each posting.
+data Box m =
+  Box { boxMeta :: m
+      , boxPostFam :: PostFam }
+  deriving Show
+
+instance Functor Box where
+  fmap f (Box m pf) = Box (f m) pf
diff --git a/Penny/Lincoln/Transaction/Unverified.hs b/Penny/Lincoln/Transaction/Unverified.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Lincoln/Transaction/Unverified.hs
@@ -0,0 +1,27 @@
+module Penny.Lincoln.Transaction.Unverified where
+
+import qualified Penny.Lincoln.Bits as B
+import qualified Penny.Lincoln.Meta as M
+
+data TopLine = TopLine
+               B.DateTime
+               (Maybe B.Flag)
+               (Maybe B.Number)
+               (Maybe B.Payee)
+               B.Memo
+               M.TopLineMeta
+             deriving (Eq, Show)
+
+data Posting = Posting
+                 (Maybe B.Payee)
+                 (Maybe B.Number) 
+                 (Maybe B.Flag) 
+                 B.Account
+                 B.Tags
+                 (Maybe B.Entry)
+                 B.Memo
+                 M.PostingMeta
+                 deriving (Eq, Show)
+
+entry :: Posting -> Maybe B.Entry
+entry (Posting _ _ _ _ _ e _ _) = e
diff --git a/Penny/Shield.hs b/Penny/Shield.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Shield.hs
@@ -0,0 +1,88 @@
+-- | Shield - the Penny runtime environment
+--
+-- Both Cabin and Copper can benefit from knowing information about
+-- the Penny runtime environment, such as environment variables and
+-- whether standard output is a terminal. That information is provided
+-- by the Runtime type. In the future this module may also provide
+-- information about the POSIX locale configuration. For now, that
+-- information would require reaching into the FFI and so it is not
+-- implemented.
+
+module Penny.Shield (
+  ScreenLines,
+  unScreenLines,
+  ScreenWidth,
+  unScreenWidth,
+  Output(IsTTY, NotTTY),
+  Term,
+  unTerm,
+  Runtime,
+  environment,
+  currentTime,
+  output,
+  screenLines,
+  screenWidth,
+  term,
+  runtime)
+  where
+
+import Control.Applicative ((<$>), (<*>))
+import qualified Data.Time as T
+import System.Environment (getEnvironment)
+import System.IO (hIsTerminalDevice, stdout)
+
+import qualified Penny.Lincoln.Bits as B
+
+data ScreenLines = ScreenLines { unScreenLines :: Int }
+                 deriving Show
+
+newtype ScreenWidth = ScreenWidth { unScreenWidth :: Int }
+                      deriving Show
+
+data Output = IsTTY | NotTTY
+
+newtype Term = Term { unTerm :: String } deriving Show
+
+-- | Information about the runtime environment.
+data Runtime = Runtime { environment :: [(String, String)]
+                       , currentTime :: B.DateTime
+                       , output :: Output }
+
+zonedToDateTime :: T.ZonedTime -> B.DateTime
+zonedToDateTime (T.ZonedTime lt tz) = B.dateTime lt off where
+  off = maybe (error "could not convert time to local time")
+        id (B.minsToOffset (T.timeZoneMinutes tz))
+
+runtime :: IO Runtime
+runtime = Runtime
+          <$> getEnvironment
+          <*> (zonedToDateTime <$> T.getZonedTime)
+          <*> findOutput
+
+findOutput :: IO Output
+findOutput = do
+  isTerm <- hIsTerminalDevice stdout
+  return $ if isTerm then IsTTY else NotTTY
+
+screenLines :: Runtime -> Maybe ScreenLines
+screenLines r = 
+  (lookup "LINES" . environment $ r)
+  >>= safeRead
+  >>= return . ScreenLines
+
+screenWidth :: Runtime -> Maybe ScreenWidth
+screenWidth r =
+  (lookup "COLUMNS" . environment $ r)
+  >>= safeRead
+  >>= return . ScreenWidth
+
+term :: Runtime -> Maybe Term
+term r =
+  (lookup "TERM" . environment $ r)
+  >>= return . Term
+
+-- | Read, but without crashes.
+safeRead :: (Read a) => String -> Maybe a
+safeRead s = case reads s of
+  (a, []):[] -> Just a
+  _ -> Nothing
diff --git a/Penny/Zinc.hs b/Penny/Zinc.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Zinc.hs
@@ -0,0 +1,29 @@
+-- | Zinc - the Penny command-line interface
+module Penny.Zinc (runPenny, P.T(..),
+                   P.defaultFromRuntime) where
+
+import qualified Control.Monad.Exception.Synchronous as Ex
+import qualified System.Console.MultiArg.Prim as M
+import System.Console.MultiArg.GetArgs (getArgs)
+import System.Exit (exitFailure)
+import System.IO (stderr, hPutStrLn)
+
+import qualified Penny.Cabin.Interface as I
+import qualified Penny.Copper as Cop
+import qualified Penny.Shield as S
+import qualified Penny.Zinc.Parser as P
+
+runPenny ::
+  S.Runtime
+  -> Cop.DefaultTimeZone
+  -> Cop.RadGroup
+  -> (S.Runtime -> P.T)
+  -> [I.Report]
+  -> IO ()
+runPenny rt dtz rg df rs = do
+  as <- getArgs
+  case M.parse as (P.parser rt dtz rg df rs) of
+    Ex.Exception e -> do
+      hPutStrLn stderr . show $ e
+      exitFailure
+    Ex.Success g -> g
diff --git a/Penny/Zinc/Error.hs b/Penny/Zinc/Error.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Zinc/Error.hs
@@ -0,0 +1,11 @@
+module Penny.Zinc.Error where
+
+import Data.Text (Text, pack)
+
+data Error =
+  ParseError Text
+  | ReportError Text
+  deriving Show
+
+printError :: Error -> Text
+printError = pack . show
diff --git a/Penny/Zinc/Help.hs b/Penny/Zinc/Help.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Zinc/Help.hs
@@ -0,0 +1,125 @@
+module Penny.Zinc.Help where
+
+import Data.Text (Text, pack)
+
+help :: Text
+help = pack $ unlines [
+  "usage: zinc [posting filters] report [report options] file . . .",
+  "",
+  "Posting filters",
+  "------------------------------------------",
+  "",
+  "Dates",
+  "-----",
+  "",
+  "--date cmp timespec, -d cmp timespec",
+  "  Date must be within the time frame given. timespec",
+  "  is a day or a day and a time. Valid values for cmp:",
+  "     <, >, <=, >=, ==, /=, !=",
+  "--current",
+  "  Same as \"--date <= (right now) \"",
+  "",
+  "Serials",
+  "----------------",
+  "These options take the form --option cmp num; the given",
+  "sequence number must fall within the given range. \"rev\"",
+  "in the option name indicates numbering is from end to beginning.",
+  "",
+  "--globalTransaction, --revGlobalTransaction",
+  "  All transactions, after reading the ledger files",
+  "--globalPosting, --revGlobalPosting",
+  "  All postings, after reading the leder files",
+  "--fileTransaction, --revFileTransaction",
+  "  Transactions in each ledger file, after reading the files",
+  "  (numbering restarts with each file)",
+  "--filePosting, --revFilePosting",
+  "  Postings in each ledger file, after reading the files",
+  "  (numbering restarts with each file)",
+  "",
+  "Pattern matching",
+  "----------------",
+  "",
+  "-a pattern, --account pattern",
+  "  Pattern must match colon-separated account name",
+  "--account-level num pat",
+  "  Pattern must match sub account at given level",
+  "--account-any pat",
+  "  Pattern must match sub account at any level",
+  "-p pattern, --payee pattern",
+  "  Payee must match pattern",
+  "-t pattern, --tag pattern",
+  "  Tag must match pattern",
+  "--number pattern",
+  "  Number must match pattern",
+  "--flag pattern",
+  "  Flag must match pattern",
+  "--commodity pattern",
+  "  Pattern must match colon-separated commodity name",
+  "--commodity-level num pattern",
+  "  Pattern must match sub commodity at given level",
+  "--commodity-any pattern",
+  "  Pattern must match sub commodity at any level",
+  "--posting-memo pattern",
+  "  Posting memo must match pattern",
+  "--transaction-memo pattern",
+  "  Transaction memo must match pattern",
+  "",
+  "Other posting characteristics",
+  "-----------------------------",
+  "--debit",
+  "  Entry must be a debit",
+  "--credit",
+  "  Entry must be a credit",
+  "--qty cmp number",
+  "  Entry quantity must fall within given range",
+  "",
+  "Operators - from highest to lowest precedence",
+  "(all are left associative)",
+  "--------------------------",
+  "--open expr --close",
+  "  Force precedence (as in \"open\" and \"close\" parentheses)",
+  "--not expr",
+  "  True if expr is false",
+  "expr1 --and expr2 ",
+  "  True if expr and expr2 are both true",
+  "expr1 --or expr2",
+  "  True if either expr1 or expr2 is true",
+  "",
+  "Options affecting patterns",
+  "--------------------------",
+  "",
+  "-i, --case-insensitive",
+  "  Be case insensitive (default)",
+  "-I, --case-sensitive",
+  "  Be case sensitive",
+  "",
+  "--within",
+  "  Use \"within\" matcher (default)",
+  "--pcre",
+  "  Use \"pcre\" matcher",
+  "--posix",
+  "  Use \"posix\" matcher",
+  "--exact",
+  "  Use \"exact\" matcher",
+  "",
+  "Removing postings after sorting and filtering",
+  "---------------------------------------------",
+  "--head n",
+  "  Keep only the first n postings",
+  "--tail n",
+  "  Keep only the last n postings",
+  "",
+  "Sorting",
+  "-------",
+  "",
+  "-s key, --sort key",
+  "  Sort postings according to key",
+  "",
+  "Keys:",
+  "  payee, date, flag, number, account, drCr,",
+  "  qty, commodity, postingMemo, transactionMemo",
+  "",
+  "  Ascending order by default; for descending order,",
+  "  capitalize the name of the key.",
+  ""
+  ]
diff --git a/Penny/Zinc/Parser.hs b/Penny/Zinc/Parser.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Zinc/Parser.hs
@@ -0,0 +1,86 @@
+module Penny.Zinc.Parser (
+  Defaults.T(..)
+  , Defaults.defaultFromRuntime
+  , parser
+  ) where
+
+import qualified Control.Monad.Exception.Synchronous as Ex
+import Data.Monoid (mappend, mconcat)
+import qualified Data.Text as X
+import qualified Data.Text.IO as StrictIO
+import qualified Data.Text.Lazy.IO as LazyIO
+import System.Console.MultiArg.Prim (Parser)
+import System.Exit (exitSuccess, exitFailure)
+import System.IO (stderr, hPutStrLn)
+import qualified Penny.Cabin.Interface as I
+import qualified Penny.Shield as S
+import qualified Penny.Zinc.Help as Help
+import qualified Penny.Zinc.Parser.Filter as F
+import qualified Penny.Zinc.Parser.Report as R
+import qualified Penny.Zinc.Parser.Ledgers as L
+import qualified Penny.Zinc.Parser.Defaults as Defaults
+
+import Penny.Copper.DateTime (DefaultTimeZone)
+import Penny.Copper.Qty (RadGroup)
+
+-- | Parses all command line options and arguments. Returns an IO
+-- action which will print appropriate error messages if something
+-- failed, or will print a report and exit successfully if everything
+-- went well.
+parser ::
+  S.Runtime
+  -> DefaultTimeZone
+  -> RadGroup
+  -> (S.Runtime -> Defaults.T)
+  -> [I.Report]
+  -> Parser (IO ())
+parser rt dtz rg getDf rs = do
+  let df = getDf rt
+  errFilt <- F.parseFilter df
+  case errFilt of
+    Ex.Exception e -> return $ do
+      hPutStrLn stderr $ ("penny: error: " ++ show e)
+      exitFailure
+    Ex.Success r -> case r of
+      Left F.NeedsHelp -> return $ do
+        StrictIO.putStrLn . helpText $ rs
+        exitSuccess
+      Right rslt -> parseReportsAndFilesAndPrint rt dtz rg rs rslt
+        
+
+-- | Returns an IO action that will parse the report options and the
+-- files on the command line and print the resulting report.
+parseReportsAndFilesAndPrint ::
+  S.Runtime
+  -> DefaultTimeZone
+  -> RadGroup
+  -> [I.Report]
+  -> F.Result
+  -> Parser (IO ())
+parseReportsAndFilesAndPrint rt dtz rg rs rslt = do
+  let (F.Result factory sensitive sortFilt) = rslt
+  parserFunc <- R.report rs
+  let reportFunc = parserFunc rt sensitive factory
+  filenames <- L.filenames
+  return $ do
+    ledgers <- L.readLedgers filenames
+    let parsed = L.parseLedgers dtz rg ledgers
+    case parsed of
+      Ex.Exception e -> do
+        hPutStrLn stderr . show $ e
+        exitFailure
+      Ex.Success (txns, prices) -> do
+        let boxes = sortFilt txns
+        case reportFunc boxes prices of
+          Ex.Exception bad -> do
+            StrictIO.putStrLn bad
+            exitFailure
+          Ex.Success good -> do
+            LazyIO.putStr good
+            exitSuccess
+
+helpText ::
+  [I.Report]
+  -> X.Text
+helpText = mappend Help.help . mconcat . fmap I.help
+
diff --git a/Penny/Zinc/Parser/Defaults.hs b/Penny/Zinc/Parser/Defaults.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Zinc/Parser/Defaults.hs
@@ -0,0 +1,29 @@
+module Penny.Zinc.Parser.Defaults where
+
+import qualified Text.Matchers.Text as M
+import qualified Penny.Lincoln as L
+import qualified Penny.Copper as Cop
+import qualified Penny.Shield as S
+import qualified Data.Text as X
+import qualified Control.Monad.Exception.Synchronous as Ex
+
+data T =
+  T { sensitive :: M.CaseSensitive
+    , factory :: M.CaseSensitive -> X.Text
+                 -> Ex.Exceptional X.Text (X.Text -> Bool)
+    , currentTime :: L.DateTime
+    , defaultTimeZone :: Cop.DefaultTimeZone
+    , radGroup :: Cop.RadGroup }
+
+defaultFromRuntime ::
+  Cop.DefaultTimeZone
+  -> Cop.RadGroup
+  -> S.Runtime
+  -> T
+defaultFromRuntime dtz rg rt =
+  T { sensitive = M.Insensitive
+    , factory = (\c t -> return (M.within c t))
+    , currentTime = S.currentTime rt
+    , defaultTimeZone = dtz
+    , radGroup = rg }
+  
diff --git a/Penny/Zinc/Parser/Filter.hs b/Penny/Zinc/Parser/Filter.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Zinc/Parser/Filter.hs
@@ -0,0 +1,265 @@
+module Penny.Zinc.Parser.Filter (
+  parseFilter
+  , Error(LibertyError, TokenParseError)
+  , NeedsHelp(NeedsHelp)
+  , Result(Result, resultFactory, resultSensitive, sorterFilterer)
+  ) where
+
+import Control.Applicative ((<|>), (<$>), Applicative, pure, many)
+import Control.Monad ((>=>))
+import qualified Control.Monad.Exception.Synchronous as Ex
+import Data.Monoid (mempty, mappend)
+import Data.Text (Text)
+import qualified Text.Matchers.Text as M
+import qualified System.Console.MultiArg.Combinator as C
+import System.Console.MultiArg.Prim (Parser)
+
+import qualified Penny.Copper as Cop
+import qualified Penny.Lincoln as L
+import qualified Penny.Liberty as Ly
+import qualified Penny.Liberty.Expressions as X
+
+import qualified Penny.Zinc.Parser.Defaults as D
+import qualified Penny.Zinc.Parser.Defaults as Defaults
+
+-- | Parses all filtering options. Returns a parser that contains an
+-- Exception if some error occurred after parsing the options, or a
+-- Success with a result if the parse was successful.
+parseFilter ::
+  Defaults.T
+  -> Parser (Ex.Exceptional Error (Either NeedsHelp Result))
+parseFilter d = fmap f (many parser) where
+  f ls =
+    let k = foldl (>=>) return ls
+    in case k (newState d) of
+      Ex.Success st' ->
+        if help st'
+        then return . Left $ NeedsHelp
+        else
+          case Ly.parsePredicate . tokens $ st' of
+            Nothing -> Ex.throw TokenParseError
+            Just pdct ->
+              let fn = Ly.xactionsToFiltered pdct
+                       (postFilter st') (orderer st')
+                  r = Result { resultFactory = factory st'
+                             , resultSensitive = sensitive st'
+                             , sorterFilterer = fn }
+              in return . Right $ r
+      Ex.Exception e -> Ex.Exception e
+
+data Error = LibertyError Ly.Error
+             | TokenParseError
+             deriving Show
+
+-- | Returned if the user requested help.
+data NeedsHelp = NeedsHelp
+                 deriving Show
+
+-- | Indicates the result of a successful parse of filtering options.
+data Result =
+  Result { resultFactory :: M.CaseSensitive
+                            -> Text -> Ex.Exceptional Text (Text -> Bool)
+           -- ^ The factory indicated, so that it can be used in
+           -- subsequent parses of the same command line.
+
+         , resultSensitive :: M.CaseSensitive
+           -- ^ Indicated case sensitivity, so that it can be used in
+           -- subsequent parses of the command line.
+           
+         , sorterFilterer :: [L.Transaction] -> [L.Box Ly.LibertyMeta]
+           -- ^ Applied to a list of Transaction, will sort and filter
+           -- the transactions and assign them LibertyMeta.
+         }
+
+
+data State =
+  State { sensitive :: M.CaseSensitive
+        , factory :: M.CaseSensitive
+                     -> Text -> Ex.Exceptional Text (Text -> Bool)
+        , tokens :: [X.Token (L.PostFam -> Bool)]
+        , postFilter :: [Ly.PostFilterFn]
+        , orderer :: Ly.Orderer
+        , help :: Bool
+        , currentTime :: L.DateTime
+        , defaultTimeZone :: Cop.DefaultTimeZone
+        , radGroup :: Cop.RadGroup }
+
+newState ::
+  Defaults.T
+  -> State
+newState d =
+  State { sensitive = D.sensitive d
+        , factory = D.factory d
+        , tokens = []
+        , postFilter = []
+        , orderer = mempty
+        , help = False
+        , currentTime = D.currentTime d
+        , defaultTimeZone = D.defaultTimeZone d
+        , radGroup = D.radGroup d }
+
+parser :: Parser (State -> Ex.Exceptional Error State)
+parser =
+  operand
+  <|> parsePostFilter
+  <|> impurify parseMatcherSelect
+  <|> impurify parseCaseSelect
+  <|> impurify parseOperator
+  <|> parseSort
+  <|> impurify parseHelp
+
+option :: [String] -> [Char] -> C.ArgSpec a -> Parser a
+option ss cs a = C.parseOption [C.OptSpec ss cs a]
+
+operand :: Parser (State -> Ex.Exceptional Error State)
+operand = f <$> Ly.parseOperand
+  where
+    f lyFn =
+      let g st =
+            let r = lyFn (currentTime st) (defaultTimeZone st)
+                    (radGroup st) (sensitive st) (factory st)
+            in case r of
+              Ex.Exception e -> Ex.throw . LibertyError $ e
+              Ex.Success (X.Operand o) ->
+                let tok' = tokens st ++ [X.TokOperand o]
+                in return st { tokens = tok' }
+      in g
+                   
+parsePostFilter :: Parser (State -> Ex.Exceptional Error State)
+parsePostFilter = f <$> Ly.parsePostFilter
+  where
+    f lyResult =
+      let g st = case lyResult of
+            Ex.Exception e -> Ex.throw . LibertyError $ e
+            Ex.Success pf ->
+              let ls' = postFilter st ++ [pf]
+              in return st { postFilter = ls' }
+      in g
+
+impurify ::
+  (Functor f, Applicative a)
+  => f (b -> b)
+  -> f (b -> a b)
+impurify = fmap (pure .)
+
+parseMatcherSelect :: Parser (State -> State)
+parseMatcherSelect = f <$> Ly.parseMatcherSelect
+  where
+    f fty = g
+      where
+        g st = st { factory = fty }
+
+parseCaseSelect :: Parser (State -> State)
+parseCaseSelect = f <$> Ly.parseCaseSelect
+  where
+    f sel = g
+      where
+        g st = st { sensitive = sel }
+
+parseOperator :: Parser (State -> State)
+parseOperator = f <$> Ly.parseOperator
+  where
+    f tok = g
+      where
+        g st = st { tokens = tokens st ++ [tok] }
+
+parseSort :: Parser (State -> Ex.Exceptional Error State)
+parseSort = f <$> Ly.parseSort
+  where
+    f exOrd = g
+      where
+        g st = case exOrd of
+          Ex.Exception e -> Ex.throw . LibertyError $ e
+          Ex.Success o ->
+            return st { orderer = mappend o (orderer st) }
+
+parseHelp :: Parser (State -> State)
+parseHelp = option ["help"] ['h'] (C.NoArg f)
+  where
+    f st = st { help = True }
+
+{-
+wrapLiberty ::
+  DefaultTimeZone
+  -> DateTime
+  -> RadGroup
+  -> State
+  -> ParserE Error State
+wrapLiberty dtz dt rg st = let
+  toLibSt = LF.State { LF.sensitive = sensitive st
+                     , LF.factory = factory st
+                     , LF.tokens = tokens st
+                     , LF.postFilter = postFilter st }
+  fromLibSt libSt = State { sensitive = LF.sensitive libSt
+                          , factory = LF.factory libSt
+                          , tokens = LF.tokens libSt
+                          , postFilter = LF.postFilter libSt
+                          , orderer = orderer st
+                          , help = help st }
+  in fromLibSt <$> LF.parseOption dtz dt rg toLibSt
+
+wrapOrderer :: State -> ParserE Error State
+wrapOrderer st = mkSt <$> S.sort where
+  mkSt o = st { orderer = o `mappend` (orderer st) }
+
+helpOpt :: ParserE Error ()
+helpOpt = do
+  let lo = makeLongOpt . pack $ "help"
+      so = makeShortOpt 'h'
+  _ <- mixedNoArg lo [] [so]
+  return ()
+
+wrapHelp :: State -> ParserE Error State
+wrapHelp st = (\_ -> st { help = Help }) <$> helpOpt
+
+parseOption ::
+  DefaultTimeZone
+  -> DateTime
+  -> RadGroup
+  -> State
+  -> ParserE Error State
+parseOption dtz dt rg st =
+  wrapLiberty dtz dt rg st
+  <|> wrapOrderer st
+  <|> wrapHelp st
+
+parseOptions ::
+  DefaultTimeZone
+  -> DateTime
+  -> RadGroup
+  -> State
+  -> ParserE Error State
+parseOptions dtz dt rg st =
+  option st $ do
+    rs <- runUntilFailure (parseOption dtz dt rg) st
+    if null rs then return st else return (last rs)
+
+parseFilter ::
+  DefaultTimeZone
+  -> DateTime
+  -> RadGroup
+  -> ParserE Error (Either NeedsHelp Result)
+parseFilter dtz dt rg = do
+  st' <- parseOptions dtz dt rg newState
+  case help st' of
+    Help -> return . Left $ NeedsHelp
+    NoHelp -> do
+      p <- case Oo.getPredicate (tokens st') of
+        Just pr -> return pr
+        Nothing -> throw E.BadExpression
+      let f = sortFilterAndPostFilter (orderer st') p (postFilter st')
+          r = Result { resultFactory = factory st'
+                     , resultSensitive = sensitive st'
+                     , sorterFilterer = f }
+      return . Right $ r
+
+sortFilterAndPostFilter ::
+  S.Orderer
+  -> (T.PostingInfo -> Bool)
+  -> ([T.PostingInfo] -> [T.PostingInfo])
+  -> [PostingBox] -> [T.PostingInfo]
+sortFilterAndPostFilter o p pf =
+  pf
+  . filter p
+  . PSq.sortedPostingInfos o
+-}
diff --git a/Penny/Zinc/Parser/Ledgers.hs b/Penny/Zinc/Parser/Ledgers.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Zinc/Parser/Ledgers.hs
@@ -0,0 +1,79 @@
+module Penny.Zinc.Parser.Ledgers (
+  filenames
+  , parseLedgers
+  , readLedgers
+  ) where
+
+import Control.Applicative ((<$>), many, optional)
+import Control.Monad (when)
+import qualified Control.Monad.Exception.Synchronous as Ex
+import Data.Text (Text, pack, unpack)
+import qualified Data.Text.IO as TIO
+
+import qualified Penny.Copper as C
+import qualified Penny.Lincoln as L
+import qualified Penny.Zinc.Error as ZE
+import System.Console.MultiArg.Prim (Parser, nextArg)
+import System.IO (hIsTerminalDevice, stdin, stderr, hPutStrLn)
+
+
+warnTerminal :: IO ()
+warnTerminal =
+  hPutStrLn stderr $ "zinc: warning: reading from standard input, "
+  ++ "which is a terminal"
+
+data Filename =
+  Filename Text
+  | Stdin
+
+-- | Converts a Ledgers filename to a Lincoln filename.
+convertFilename :: Filename -> L.Filename
+convertFilename (Filename x) = L.Filename x
+convertFilename Stdin = L.Filename . pack $ "<stdin>"
+
+-- | Actually reads the file off disk. For now just let this crash if
+-- any of the IO errors occur.
+ledgerText :: Filename -> IO Text
+ledgerText f = case f of
+  Stdin -> do
+    isTerm <- hIsTerminalDevice stdin
+    when isTerm warnTerminal
+    TIO.hGetContents stdin
+  Filename fn -> TIO.readFile (unpack fn)
+
+readLedgers :: [Filename] -> IO [(Filename, Text)]
+readLedgers = mapM f where
+  f fn = (\txt -> (fn, txt)) <$> ledgerText fn
+
+parseLedgers ::
+  C.DefaultTimeZone
+  -> C.RadGroup
+  -> [(Filename, Text)]
+  -> Ex.Exceptional ZE.Error ([L.Transaction], [L.PricePoint])
+parseLedgers dtz rg ls =
+  let toPair (f, t) = (convertFilename f, C.FileContents t)
+      parsed = C.parse dtz rg (map toPair ls)
+      folder i (ts, ps) = case snd i of
+        C.Transaction t -> (t:ts, ps)
+        C.Price p -> (ts, p:ps)
+        _ -> (ts, ps)
+      toResult (C.Ledger is) = foldr folder ([], []) is
+      toErr (C.ErrorMsg x) = ZE.ParseError x
+  in Ex.mapExceptional toErr toResult parsed
+
+
+filename :: Parser Filename
+filename = f <$> nextArg
+  where
+    f a = if a == "-"
+          then Stdin
+          else Filename . pack $ a
+
+filenames :: Parser [Filename]
+filenames = do
+  fn1 <- optional filename
+  case fn1 of
+    Nothing -> return [Stdin]
+    Just fn -> do
+      fns <- many filename
+      return (fn:fns)
diff --git a/Penny/Zinc/Parser/Report.hs b/Penny/Zinc/Parser/Report.hs
new file mode 100644
--- /dev/null
+++ b/Penny/Zinc/Parser/Report.hs
@@ -0,0 +1,30 @@
+module Penny.Zinc.Parser.Report (report) where
+
+import System.Console.MultiArg.Prim (Parser)
+import qualified System.Console.MultiArg.Combinator as C
+
+import qualified Penny.Cabin.Interface as I
+import qualified Data.Set as Set
+
+-- | Given a Runtime and a list of available Reports, returns a Parser
+-- that will return a function corresponding to the report the user
+-- desires. The parser returned assumes that the filtering options
+-- have already been parsed. The returned parser parses all options
+-- corresponding to report options in order to return the function.
+report ::
+  [I.Report]
+  -- ^ List of available Reports
+
+  -> Parser I.ReportFunc
+report rs = do
+  let toPair r = (I.name r, I.parseReport r)
+      alist = map toPair rs
+      set = Set.fromList . map fst $ alist
+  (_, n) <- C.matchApproxWord set
+  case lookup n alist of
+    Nothing -> error $ "Penny.Zinc.Parser.Report: error: "
+               ++ "report not found"
+    Just rptFunc -> rptFunc
+
+
+
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/penny-lib.cabal b/penny-lib.cabal
new file mode 100644
--- /dev/null
+++ b/penny-lib.cabal
@@ -0,0 +1,290 @@
+Name: penny-lib
+Version: 0.2.0.0
+Cabal-version: >=1.8
+Build-Type: Simple
+License: MIT
+Copyright: 2012 Omari Norman.
+author: Omari Norman
+maintainer: omari@smileystation.com
+stability: Experimental
+homepage: http://www.github.com/massysett/penny
+bug-reports: omari@smileystation.com
+Category: Console, Finance
+License-File: LICENSE
+synopsis: Extensible double-entry accounting system - library
+
+description: Penny is a double-entry accounting system. It is inspired
+  by, but incompatible with, John Wiegley's Ledger, which is available
+  at <http://ledger-cli.org/>. You will want to install the package
+  penny-bin by using cabal install penny-bin, which will install the
+  executable program and this library as well. Features:
+
+  .
+
+  * Penny is a double-entry accounting system. It uses traditional
+  accounting terminology, such as the terms \"Debit\" and
+  \"Credit\". If you need a refresher on the basics of double-entry
+  accounting, pick up a used accounting textbook from your favorite
+  bookseller (they can be had cheaply, for less than ten U.S. dollars
+  including shipping) or check out
+  <http://www.principlesofaccounting.com/>, a great free online text.
+
+  .
+
+  * Penny is based around "Penny.Lincoln", a core library to represent
+    transactions and postings and their components, such as their
+    amounts and whether they are debits and credits. You can use
+    Lincoln all by itself even if you don't use the other components
+    of Penny, which you may find handy if you are a Haskell
+    programmer. I wrote Penny because I wanted a precise library to
+    represent my accounting data so I could analyze it programatically
+    and verify its consistency. I wrote it in Haskell not because I
+    wanted to write something in Haskell but because Haskell is the
+    best tool for this job (I used to use the shell, combined with
+    Ledger, which is a messy combination.)
+
+  .
+
+  * Penny's command line interface, Zinc, and its reports, Cabin, give
+    you unparalleled flexibility to filter and sort postings. Each
+    posting within a transaction may have its own flags assigned
+    (e.g. to indicate whether the posting is cleared) and each posting
+    may have infinite \"tags\" assigned to it, giving you another way
+    to categorize your postings. For instance, you might have vacation
+    related postings in several different accounts, but you can give
+    them all a \"vacation\" tag.
+
+  .
+
+  * Full Unicode support. Also, you may set which characters you wish
+    to use to represent the radix point and the digit grouping
+    character in your ledger file.
+
+  .
+
+  * Penny's reports, in "Penny.Cabin", have color baked in from the
+    beginning. You do not have to use color, though, which is handy if
+    you are sending output to a file or if, well, you just don't like
+    color.
+
+  .
+
+  * Penny's reports are customizable in Haskell, giving you an easy
+    and powerful way to make your own reports without writing cryptic
+    formatting strings.
+
+  .
+
+  * Penny handles multiple commodities (for example, multiple
+    currencies, stocks and bonds, tracking other assets, etc.) in an
+    easy and transparent way that is consistent with double-entry
+    accounting principles. It embraces the philosophy outlined in this
+    tutorial on multiple commodity accounting:
+    <http://www.mscs.dal.ca/~selinger/accounting/tutorial.html>.
+
+  .
+
+  * Penny stores amounts using only integers, building from the
+    Data.Decimal library available at
+    <http://hackage.haskell.org/package/Decimal>. This ensures the
+    accuracy of your data, as using floating point values to represent
+    money is a bad idea. Here is one explanation:
+    <http://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency>. The
+    use of integer arithmetic also makes Penny simpler internally, as
+    there is no need for arbitrary rounding to compensate for the
+    bizarre and inaccurate results that sometimes arise from the use of
+    floating-point values to represent currencies.
+
+  .
+
+  * Freely licensed under the MIT license. If you take this code,
+    improve it, lock it up and make it proprietary, and sell it,
+    AWESOME! I haven't lost anything because I still have my code and,
+    what's more, then maybe I can buy your product and not have to
+    maintain this one any more!
+
+  .
+
+  * Uses no GHC extensions. However, the code is only tested under GHC
+    and for all practical purposes it will only run under GHC at this
+    time because it uses libraries such as Data.Text that are
+    available only under GHC. Despite this I expect I will continue to
+    avoid language extensions.
+
+  .
+
+  Non-features / disadvantages:
+
+  .
+
+  * Written in Haskell. Yes, I think Haskell is the best tool ever,
+    but its compiler is not as commonly installed as compilers for C
+    or C++, and non-Haskellers will probably find Penny to be more
+    difficult to install than Ledger, as the latter is written in C++.
+
+  .
+
+  * Handling commodities requires that you set up multiple accounts;
+    some might find this cumbersome.
+
+  .
+
+  * Young and not well tested yet. Also, only tested on Unix. It
+    probably would not be difficult to make Penny run on Windows; if
+    someone wants to do that, go ahead.
+
+  .
+
+  * Full Penny functionality is available without a Haskell compiler;
+    you could even use a pre-compiled binary. However, to fully
+    customize Penny, you will need a Haskell compiler installed.
+
+  .
+
+  * Can be slow and memory hungry with large data sets. I have a
+    ledger file with about 28,000 lines. On my least capable machine
+    (which has an Intel Core 2 Duo at 1.6 GHz) this takes about 1.4
+    seconds to parse. Not horrible but not instantaneous
+    either. Generating a report about all these transactions can take
+    about seven seconds and a little less than 300 MB of memory. I
+    have eliminated all the obvious slowness from the code and
+    attempted a rewrite of the parser, which made no difference; other
+    ideas to speed up Penny with large data sets would involve
+    substantial changes and this is not at the top of my list because
+    the program is currently usable with relatively recent hardware.
+
+  .
+
+  More details about the organization of the Penny modules is
+  available by examining the top "Penny" module.
+
+  .
+
+  This is only a library. For the executable package, which also
+  includes more documentation, search for the penny-bin package on
+  Hackage.
+
+source-repository head
+    type: git
+    location: git://github.com/massysett/penny.git
+
+Library
+    Build-depends:
+        base ==4.*,
+        text ==0.11.*,
+        explicit-exception ==0.1.*,
+        containers ==0.4.*,
+        transformers == 0.2.*,
+        time ==1.2.*,
+        Decimal >= 0.2.2 && < 0.3,
+        parsec >= 3.1.2 && < 3.2,
+        multiarg ==0.4.*,
+        matchers ==0.2.*,
+        old-locale ==1.0.*,
+        semigroups ==0.8.*,
+        split ==0.1.*
+
+    Exposed-modules:
+        Penny,
+        Penny.Cabin,
+        Penny.Cabin.Allocate,
+        Penny.Cabin.Balance,
+        Penny.Cabin.Balance.Parser,
+        Penny.Cabin.Balance.Help,
+        Penny.Cabin.Balance.Options,
+        Penny.Cabin.Balance.Tree,
+        Penny.Cabin.Chunk,
+        Penny.Cabin.Chunk.Switch,
+        Penny.Cabin.Colors,
+        Penny.Cabin.Colors.DarkBackground,
+        Penny.Cabin.Colors.LightBackground,
+        Penny.Cabin.Interface,
+        Penny.Cabin.Meta,
+        Penny.Cabin.Options,
+        Penny.Cabin.Posts,
+        Penny.Cabin.Posts.Allocate,
+        Penny.Cabin.Posts.Allocated,
+        Penny.Cabin.Posts.BottomRows,
+        Penny.Cabin.Posts.Fields,
+        Penny.Cabin.Posts.Growers,
+        Penny.Cabin.Posts.Chunk,
+        Penny.Cabin.Posts.Help,
+        Penny.Cabin.Posts.Meta,
+        Penny.Cabin.Posts.Options,
+        Penny.Cabin.Posts.Parser,
+        Penny.Cabin.Posts.Spacers,
+        Penny.Cabin.Row,
+        Penny.Cabin.TextFormat,
+        Penny.Copper,
+        Penny.Copper.Account,
+        Penny.Copper.Amount,
+        Penny.Copper.Comments,
+        Penny.Copper.Commodity,
+        Penny.Copper.DateTime,
+        Penny.Copper.Entry,
+        Penny.Copper.Flag,
+        Penny.Copper.Item,
+        Penny.Copper.Memos.Posting,
+        Penny.Copper.Memos.Transaction,
+        Penny.Copper.Number,
+        Penny.Copper.Payees,
+        Penny.Copper.Posting,
+        Penny.Copper.Price,
+        Penny.Copper.Qty,
+        Penny.Copper.Tags,
+        Penny.Copper.TopLine,
+        Penny.Copper.Transaction,
+        Penny.Copper.Util,
+        Penny.Liberty,
+        Penny.Liberty.Expressions,
+        Penny.Liberty.Expressions.Infix,
+        Penny.Liberty.Expressions.RPN,
+        Penny.Lincoln,
+        Penny.Lincoln.Balance,
+        Penny.Lincoln.Bits,
+        Penny.Lincoln.Bits.Account,
+        Penny.Lincoln.Bits.Amount,
+        Penny.Lincoln.Bits.Commodity,
+        Penny.Lincoln.Bits.DateTime,
+        Penny.Lincoln.Bits.DrCr,
+        Penny.Lincoln.Bits.Entry,
+        Penny.Lincoln.Bits.Flag,
+        Penny.Lincoln.Bits.Memo,
+        Penny.Lincoln.Bits.Number,
+        Penny.Lincoln.Bits.Payee,
+        Penny.Lincoln.Bits.Price,
+        Penny.Lincoln.Bits.PricePoint,
+        Penny.Lincoln.Bits.Qty,
+        Penny.Lincoln.Bits.Tags,
+        Penny.Lincoln.Builders,
+        Penny.Lincoln.Family,
+        Penny.Lincoln.Family.Child,
+        Penny.Lincoln.Family.Family,
+        Penny.Lincoln.Family.Siblings,
+        Penny.Lincoln.HasText,
+        Penny.Lincoln.Meta,
+        Penny.Lincoln.NestedMap,
+        Penny.Lincoln.Predicates,
+        Penny.Lincoln.PriceDb,
+        Penny.Lincoln.Queries,
+        Penny.Lincoln.Serial,
+        Penny.Lincoln.TextNonEmpty,
+        Penny.Lincoln.Transaction,
+        Penny.Lincoln.Transaction.Unverified,
+        Penny.Shield,
+        Penny.Zinc,
+        Penny.Zinc.Error,
+        Penny.Zinc.Help,
+        Penny.Zinc.Parser,
+        Penny.Zinc.Parser.Defaults,
+        Penny.Zinc.Parser.Filter,
+        Penny.Zinc.Parser.Ledgers,
+        Penny.Zinc.Parser.Report
+
+    ghc-options: -Wall
+    if flag(debug)
+      ghc-options: -auto-all -caf-all
+
+Flag debug
+    Description: turns on some debugging options
+    Default: False
