diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2009, Josh Hoyt.
+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.
+
+    * The names of the contributors may not 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/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/src/Demo.hs b/src/Demo.hs
new file mode 100644
--- /dev/null
+++ b/src/Demo.hs
@@ -0,0 +1,102 @@
+module Main where
+
+import Data.Maybe ( fromJust )
+import Control.Applicative ( (<$>) )
+import Control.Monad.Trans ( liftIO )
+import Control.Monad.State ( StateT, put, get, gets, evalStateT )
+
+import Graphics.Vty.Widgets.Base
+import Graphics.Vty.Widgets.List
+import Graphics.Vty
+
+titleAttr :: Attr
+titleAttr = def_attr
+            `with_back_color` blue
+            `with_fore_color` bright_white
+
+bodyAttr :: Attr
+bodyAttr = def_attr
+           `with_back_color` black
+           `with_fore_color` bright_green
+
+selAttr :: Attr
+selAttr = def_attr
+           `with_back_color` yellow
+           `with_fore_color` black
+
+-- Construct the user interface based on the contents of the
+-- application state.
+buildUi :: StateT AppState IO VBox
+buildUi = do
+  list <- gets theList
+  msgs <- gets theMessages
+  let body = fromJust $ lookup (getSelected list) msgs
+      ui = list
+           <--> hFill titleAttr '-' 1
+           <--> text bodyAttr body
+           <--> vFill bodyAttr ' '
+           <--> footer
+      footer = text titleAttr "- Status "
+               <++> hFill titleAttr '-' 1
+
+  return ui
+
+-- The application state; this encapsulates what can vary based on
+-- user input and what is used to construct the interface.  This is a
+-- place for widgets whose state need to be stored so they can be
+-- modified and used to reconstruct the interface as input is handled
+data AppState = AppState { theList :: List
+                         , theMessages :: [(String, String)]
+                         }
+
+-- Process events from VTY, possibly modifying the application state.
+eventloop :: Vty -> StateT AppState IO ()
+eventloop vty = do
+  w <- buildUi
+  evt <- liftIO $ do
+                  pic_for_image <$> mkImage vty w >>= update vty
+                  next_event vty
+  case evt of
+    -- If we got an up or down arrow key, modify the app state (list
+    -- widget) and continue processing events.
+    EvKey KUp [] -> do
+                  appst <- get
+                  put (appst { theList = scrollUp $ theList appst })
+                  eventloop vty
+    EvKey KDown [] -> do
+                  appst <- get
+                  put (appst { theList = scrollDown $ theList appst })
+                  eventloop vty
+
+    -- If we get 'q', quit.
+    EvKey (KASCII 'q') [] -> return ()
+
+    -- Any other key means keep looping (including terminal resize).
+    _ -> eventloop vty
+
+-- Construct the application state using the message map.
+mkAppState :: [(String, String)] -> AppState
+mkAppState messages =
+    let list = mkList bodyAttr selAttr 3 $ map fst messages
+    in AppState { theList = list
+                , theMessages = messages
+                }
+
+main :: IO ()
+main = do
+  vty <- mkVty
+
+  -- The data that we'll present in the interface.
+  let messages = [ ("First", "the first message")
+                 , ("Second", "the second message")
+                 , ("Third", "the third message")
+                 , ("Fourth", "the fourth message")
+                 , ("Fifth", "the fifth message")
+                 , ("Sixth", "the sixth message")
+                 , ("Seventh", "the seventh message")
+                 ]
+
+  evalStateT (eventloop vty) $ mkAppState messages
+  -- Clear the screen.
+  reserve_display $ terminal vty
+  shutdown vty
diff --git a/src/Graphics/Vty/Widgets/Base.hs b/src/Graphics/Vty/Widgets/Base.hs
new file mode 100644
--- /dev/null
+++ b/src/Graphics/Vty/Widgets/Base.hs
@@ -0,0 +1,234 @@
+{-# LANGUAGE ExistentialQuantification #-}
+-- |A collection of primitive user interface widgets for composing and
+-- laying out 'Graphics.Vty' user interfaces.  This module provides
+-- basic static and box layout widgets and a type class for rendering
+-- widgets to Vty 'Graphics.Vty.Image's.
+--
+-- Each widget type supplied by this library is exported as a type and
+-- an associated constructor function (e.g., 'Text' and 'text', 'VBox'
+-- and 'vBox').
+module Graphics.Vty.Widgets.Base
+    ( Widget(..)
+    , GrowthPolicy(..)
+    , mkImage
+    , AnyWidget
+    , Text
+    , HBox
+    , VBox
+    , HFill
+    , VFill
+    , (<++>)
+    , (<-->)
+    , anyWidget
+    , text
+    , hBox
+    , vBox
+    , hFill
+    , vFill
+    )
+where
+
+import GHC.Word ( Word )
+
+import Graphics.Vty ( DisplayRegion(DisplayRegion), Vty, Image, Attr
+                    , string, char_fill, (<|>), (<->), image_width
+                    , image_height, region_width, region_height
+                    , terminal, display_bounds )
+
+-- |The growth policy of a widget determines how its container will
+-- reserve space to render it.
+data GrowthPolicy = Static
+                  -- ^'Static' widgets have a fixed size that is not
+                  -- influenced by available space
+                  | GrowVertical
+                  -- ^'GrowVertical' widgets may grow vertically with
+                  -- available space
+                  | GrowHorizontal
+                    -- ^'GrowHorizontal' widgets may grow horizontally
+                    -- with available space
+                    deriving (Show, Eq)
+
+-- |The class of user interface widgets.
+class Widget w where
+    -- |Given a widget, render it with the given dimensions.  The
+    -- resulting Image should not be larger than the specified
+    -- dimensions, but may be smaller.
+    render :: DisplayRegion -> w -> Image
+
+    -- |The growth policy of this widget.
+    growthPolicy :: w -> GrowthPolicy
+
+-- |A wrapper for all widget types used in normalizing heterogeneous
+-- lists of widgets.  See 'anyWidget'.
+data AnyWidget = forall a. (Widget a) => AnyWidget a
+
+-- |A text widget consisting of a string rendered using an
+-- attribute. See 'text'.
+data Text = Text Attr String
+
+-- |A vertical fill widget for filling available vertical space in a
+-- box layout.  See 'vFill'.
+data VFill = VFill Attr Char
+
+-- |A horizontal fill widget for filling available horizontal space in
+-- a box layout.  See 'hFill'.
+data HFill = HFill Attr Char Int
+
+-- |A vertical box layout widget capable of containing two 'Widget's.
+-- See 'vBox'.
+data VBox = forall a b. (Widget a, Widget b) => VBox a b
+
+-- |A horizontal box layout widget capable of containing two
+-- 'Widget's.  See 'hBox'.
+data HBox = forall a b. (Widget a, Widget b) => HBox a b
+
+instance Widget AnyWidget where
+    growthPolicy (AnyWidget w) = growthPolicy w
+    render s (AnyWidget w) = render s w
+
+instance Widget Text where
+    growthPolicy _ = Static
+    render _ (Text att content) = string att content
+
+instance Widget VFill where
+    growthPolicy _ = GrowVertical
+    render s (VFill att c) = char_fill att c (width s) (height s)
+
+instance Widget HFill where
+    growthPolicy _ = Static
+    render s (HFill att c h) = char_fill att c (width s) (toEnum h)
+
+instance Widget VBox where
+    growthPolicy (VBox top bottom) =
+        if t == GrowVertical
+        then t else growthPolicy bottom
+            where t = growthPolicy top
+
+    render s (VBox top bottom) =
+        t <-> b
+            where
+              renderHalves = let half = s `withHeight` div (height s) 2
+                                 half' = if height s `mod` 2 == 0
+                                         then half
+                                         else region (width half) (height half + 1)
+                             in ( render half top
+                                , render half' bottom )
+              renderTopFirst = let renderedTop = render s top
+                                   renderedBottom = render s' bottom
+                                   s' = s `withHeight` (height s - image_height renderedTop)
+                               in (renderedTop, renderedBottom)
+              renderBottomFirst = let renderedTop = render s' top
+                                      renderedBottom = render s bottom
+                                      s' = s `withHeight` (height s - image_height renderedBottom)
+                                  in (renderedTop, renderedBottom)
+              (t, b) = case (growthPolicy top, growthPolicy bottom) of
+                         (GrowVertical, GrowVertical) -> renderHalves
+                         (Static, _) -> renderTopFirst
+                         (_, Static) -> renderBottomFirst
+                         -- Horizontal contents take precedence
+                         (GrowHorizontal, _) -> renderTopFirst
+                         (_, GrowHorizontal) -> renderBottomFirst
+
+instance Widget HBox where
+    growthPolicy (HBox left right) =
+        if l == GrowHorizontal
+        then l else growthPolicy right
+            where l = growthPolicy left
+
+    render s (HBox left right) =
+        t <|> b
+            where
+              renderHalves = let half = s `withWidth` div (width s) 2
+                                 half' = if width s `mod` 2 == 0
+                                         then half
+                                         else region (width half + 1) (height half)
+                             in ( render half left
+                                , render half' right )
+              renderLeftFirst = let renderedLeft = render s left
+                                    renderedRight = render s' right
+                                    s' = region (width s - image_width renderedLeft)
+                                         (image_height renderedLeft)
+                                in (renderedLeft, renderedRight)
+              renderRightFirst = let renderedLeft = render s' left
+                                     renderedRight = render s right
+                                     s' = region (width s - image_width renderedRight)
+                                          (image_height renderedRight)
+                                 in (renderedLeft, renderedRight)
+              (t, b) = case (growthPolicy left, growthPolicy right) of
+                         (GrowHorizontal, GrowHorizontal) -> renderHalves
+                         (Static, _) -> renderLeftFirst
+                         (_, Static) -> renderRightFirst
+                         (GrowVertical, GrowVertical) -> renderHalves
+                         (_, _) -> renderLeftFirst
+
+width :: DisplayRegion -> Word
+width = region_width
+
+height :: DisplayRegion -> Word
+height = region_height
+
+region :: Word -> Word -> DisplayRegion
+region = DisplayRegion
+
+withWidth :: DisplayRegion -> Word -> DisplayRegion
+withWidth (DisplayRegion _ h) w = DisplayRegion w h
+
+withHeight :: DisplayRegion -> Word -> DisplayRegion
+withHeight (DisplayRegion w _) h = DisplayRegion w h
+
+-- |Given a 'Widget' and a 'Vty' object, render the widget using the
+-- current size of the terminal controlled by Vty. Returns the
+-- rendered 'Widget' as an 'Image'.
+mkImage :: (Widget a) => Vty -> a -> IO Image
+mkImage vty w = do
+  size <- display_bounds $ terminal vty
+  return $ render size w
+
+-- |Wrap a 'Widget' in the 'AnyWidget' type for normalization
+-- purposes.
+anyWidget :: (Widget a) => a -> AnyWidget
+anyWidget = AnyWidget
+
+-- |Create a 'Text' widget.
+text :: Attr -- ^The attribute to use to render the text
+     -> String -- ^The text to display
+     -> Text
+text = Text
+
+-- |Create an horizonal fill widget.
+hFill :: Attr -- ^The attribute to use to render the fill
+      -> Char -- ^The character to fill
+      -> Int -- ^The height, in rows, of the filled area; width of the
+             -- fill depends on available space
+      -> HFill
+hFill = HFill
+
+-- |Create a vertical fill widget.  The dimensions of the widget will
+-- depend on available space.
+vFill :: Attr -- ^The attribute to use to render the fill
+      -> Char -- ^The character to fill
+      -> VFill
+vFill = VFill
+
+-- |Create a horizontal box layout widget containing two widgets side
+-- by side.  Space consumed by the box will depend on its contents and
+-- the available space.
+hBox :: (Widget a, Widget b) => a -- ^The left widget
+     -> b -- ^The right widget
+     -> HBox
+hBox = HBox
+
+-- |An alias for 'hBox' intended as sugar to chain widgets
+-- horizontally.
+(<++>) :: (Widget a, Widget b) => a -> b -> HBox
+(<++>) = HBox
+
+-- |Create a vertical box layout widget containing two widgets.  Space
+-- consumed by the box will depend on its contents and the available
+-- space.
+vBox :: (Widget a, Widget b) => a -> b -> VBox
+vBox = VBox
+
+-- |An alias for 'vBox' intended as sugar to chain widgets vertically.
+(<-->) :: (Widget a, Widget b) => a -> b -> VBox
+(<-->) = VBox
diff --git a/src/Graphics/Vty/Widgets/List.hs b/src/Graphics/Vty/Widgets/List.hs
new file mode 100644
--- /dev/null
+++ b/src/Graphics/Vty/Widgets/List.hs
@@ -0,0 +1,122 @@
+-- |This module provides a 'List' widget for rendering a list of
+-- single-line strings.  A 'List' has the following features:
+--
+-- * A style for the list elements
+--
+-- * A styled cursor indicating which element is selected
+--
+-- * A /window size/ indicating how many elements should be visible to
+--   the user
+--
+-- * An internal pointer to the start of the visible window, which
+--   automatically shifts as the list is scrolled
+--
+-- To create a list, see 'mkList'.  To modify the list's state, see
+-- 'scrollDown' and 'scrollUp'.  To inspect the list, see, see
+-- 'getSelected' and 'getVisibleItems'.
+module Graphics.Vty.Widgets.List
+    ( List
+    , mkList
+    , scrollDown
+    , scrollUp
+    , getSelected
+    , getVisibleItems
+    )
+where
+
+import Graphics.Vty ( Attr, (<->) )
+import Graphics.Vty.Widgets.Base
+    ( Widget(..)
+    , text
+    , GrowthPolicy(Static)
+    )
+
+-- |The list widget type.
+data List = List { normalAttr :: Attr
+                 , selectedAttr :: Attr
+                 , selectedIndex :: Int
+                 , scrollTopIndex :: Int
+                 , scrollWindowSize :: Int
+                 , listItems :: [String]
+                 }
+
+-- |Create a new list.  Emtpy lists are not allowed.
+mkList :: Attr -- ^The attribute of normal, non-selected items
+       -> Attr -- ^The attribute of the selected item
+       -> Int -- ^The scrolling window size, i.e., the number of items
+              -- which should be visible to the user at any given time
+       -> [String] -- ^The list items
+       -> List
+mkList _ _ _ [] = error "Lists cannot be empty"
+mkList normAttr selAttr swSize contents = List normAttr selAttr 0 0 swSize contents
+
+-- note that !! here will always succeed because selectedIndex will
+-- never be out of bounds and the list will always be non-empty.
+-- |Get the currently selected list item.
+getSelected :: List -> String
+getSelected list = (listItems list) !! (selectedIndex list)
+
+-- |Scroll a list down one position and return the new scrolled list.
+-- This automatically takes care of managing all list state:
+--
+-- * Moves the cursor down one position, unless the cursor is already
+--   in the last position (in which case this does nothing)
+--
+-- * Moves the scrolling window position if necessary (i.e., if the
+--   cursor moves to an item not currently in view)
+scrollDown :: List -> List
+scrollDown list
+    -- If the list is already at the last position, do nothing.
+    | selectedIndex list == length (listItems list) - 1 = list
+    -- If the list requires scrolling the visible area, scroll it.
+    | selectedIndex list == scrollTopIndex list + scrollWindowSize list - 1 =
+        list { selectedIndex = selectedIndex list + 1
+             , scrollTopIndex = scrollTopIndex list + 1
+             }
+    -- Otherwise, just increment the selectedIndex.
+    | otherwise = list { selectedIndex = selectedIndex list + 1 }
+
+-- |Scroll a list up one position and return the new scrolled list.
+-- This automatically takes care of managing all list state:
+--
+-- * Moves the cursor up one position, unless the cursor is already
+--   in the first position (in which case this does nothing)
+--
+-- * Moves the scrolling window position if necessary (i.e., if the
+--   cursor moves to an item not currently in view)
+scrollUp :: List -> List
+scrollUp list
+    -- If the list is already at the first position, do nothing.
+    | selectedIndex list == 0 = list
+    -- If the list requires scrolling the visible area, scroll it.
+    | selectedIndex list == scrollTopIndex list =
+        list { selectedIndex = selectedIndex list - 1
+             , scrollTopIndex = scrollTopIndex list - 1
+             }
+    -- Otherwise, just decrement the selectedIndex.
+    | otherwise = list { selectedIndex = selectedIndex list - 1 }
+
+-- |Given a 'List', return the items that are currently visible
+-- according to the state of the list.  Returns the visible items and
+-- flags indicating whether each is selected.
+getVisibleItems :: List -> [(String, Bool)]
+getVisibleItems list =
+    let start = scrollTopIndex list
+        stop = scrollTopIndex list + scrollWindowSize list
+        adjustedStop = (min stop $ length $ listItems list) - 1
+    in [ (listItems list !! i, i == selectedIndex list)
+             | i <- [start..adjustedStop] ]
+
+instance Widget List where
+    -- Statically sized, because we know how many items should be
+    -- visible.
+    growthPolicy _ = Static
+
+    render s w =
+        foldl (<->) (head widgets) (tail widgets)
+            where
+              widgets = map (render s . mkWidget) (getVisibleItems w)
+              mkWidget (str, selected) = let att = if selected
+                                                   then selectedAttr
+                                                   else normalAttr
+                                         in text (att w) str
diff --git a/vty-ui.cabal b/vty-ui.cabal
new file mode 100644
--- /dev/null
+++ b/vty-ui.cabal
@@ -0,0 +1,31 @@
+Name:                vty-ui
+Version:             0.1
+Synopsis:            A user interface composition library for Vty
+Description:         An extensible library of user interface widgets
+                     for composing and laying out Vty user interfaces.
+                     This library provides a collection of widgets and
+                     a type class for rendering widgets to Vty Images.
+Category:            User Interfaces
+Author:              Jonathan Daugherty <drcygnus@gmail.com>
+Maintainer:          Jonathan Daugherty <drcygnus@gmail.com>
+Build-Type:          Simple
+License:             BSD3
+License-File:        LICENSE
+Cabal-Version:       >= 1.2
+Homepage:            http://repos.codevine.org/vty-ui/
+
+Library
+  Build-Depends:
+    base >= 4 && < 5,
+    vty >= 4.0 && < 4.1
+
+  Hs-Source-Dirs:    src
+  Exposed-Modules:
+          Graphics.Vty.Widgets.Base
+          Graphics.Vty.Widgets.List
+
+Executable vty-ui-demo
+  Hs-Source-Dirs:  src
+  Main-is:         Demo.hs
+  Build-Depends:
+    mtl >= 1.1 && < 1.2
