diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Mark Wotton (c) 2016
+
+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 Mark Wotton nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,6 @@
+module Main where
+
+import           Dustme
+
+main :: IO ()
+main = dustme 1
diff --git a/dustme.cabal b/dustme.cabal
new file mode 100644
--- /dev/null
+++ b/dustme.cabal
@@ -0,0 +1,63 @@
+name:                dustme
+version:             0.1.0.0
+synopsis:            Initial project template from stack
+description:         Please see README.md
+homepage:            https://github.com/mwotton/dustme#readme
+license:             BSD3
+license-file:        LICENSE
+author:              Mark Wotton
+maintainer:          mwotton@gmail.com
+copyright:           AllRightsReserved
+category:            Web
+build-type:          Simple
+-- extra-source-files:
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Dustme
+                       Dustme.Score
+                       Dustme.Types
+                       Dustme.Search
+                       Dustme.Renderer
+                       Dustme.TTY
+  build-depends: ansi-terminal
+               , ansi-wl-pprint
+               , async
+               , attoparsec
+               , base >= 4.7 && < 5
+               , bytestring
+               , containers
+               , deepseq
+               , extra
+               , hashable
+               , safe
+               , semigroups
+               , terminfo >= 0.4.0.2 && < 0.5
+               , text
+               , unordered-containers
+
+
+  default-language:    Haskell2010
+
+executable dustme
+  hs-source-dirs:      app
+  main-is:             Main.hs
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  build-depends:       base
+                     , dustme
+  default-language:    Haskell2010
+
+test-suite dustme-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  build-depends:       base
+                     , hspec
+                     , dustme
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/mwotton/dustme
diff --git a/src/Dustme.hs b/src/Dustme.hs
new file mode 100644
--- /dev/null
+++ b/src/Dustme.hs
@@ -0,0 +1,55 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Dustme where
+
+import           Control.Concurrent       (newEmptyMVar, takeMVar)
+import           Control.Concurrent.Async (async, cancel, race)
+import           Control.DeepSeq          (force)
+import           Control.Exception        (bracket, evaluate)
+import qualified Data.Text                as T
+import qualified Data.Text.IO             as TIO
+import           Dustme.Renderer
+import           Dustme.Search
+import           Dustme.TTY               (withTTY)
+import           Dustme.Types
+import           System.IO
+
+dustme config = do
+  hSetBuffering stdout NoBuffering
+  input <- T.lines <$> TIO.getContents
+  let applyOp' = applyOp input
+  withTTY "/dev/tty0" (setup applyOp' (map mkTrivialMatch input))
+
+mkTrivialMatch = Match 10000 1 0
+
+setup applyOp' initMatch tty = go 0 initMatch (Search "") emptyCache
+  where
+    getCommand = takeMVar (ttyGetCommand tty)
+
+    go :: Int -> SearchResult -> Search -> SearchCache -> IO ()
+    go    index  matches'         search    cache = do
+      let matches = case search of
+            (Search "") -> initMatch
+            _ -> matches'
+
+      -- the idea here is that that ttyGetCommand can always take
+      -- priority over printing. This means that if we type a bunch
+      -- of keys in in quick succession, the UI will remain responsive.
+      r <- race (evaluate $ force matches) getCommand
+      s <- case r of
+             Left matches' -> renderSearch tty index matches search >> getCommand
+             Right x -> return x
+
+      case s of
+        Accept  -> TIO.putStrLn (matchText $ matches !! index)
+        Up      -> go (clamp (length matches) (index-1)) matches search cache
+        Down    -> go (clamp (length matches) (index+1)) matches search cache
+        Edit op ->
+          let (newsearch,newcandidates) = applyOp'  op  search matches
+              (searchResult, newcache) =
+                getResults newcandidates cache newsearch
+          in go 0 searchResult newsearch newcache
+
+clamp hi i
+ | i < 0     = 0
+ | i >= hi   = hi - 1
+ | otherwise = i
diff --git a/src/Dustme/Renderer.hs b/src/Dustme/Renderer.hs
new file mode 100644
--- /dev/null
+++ b/src/Dustme/Renderer.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Dustme.Renderer where
+import           Data.Char                    (chr)
+import           Data.List                    (intersperse)
+import           Data.Monoid                  ((<>))
+import qualified Data.Text                    as T
+import           Dustme.TTY
+import           Dustme.Types
+import           System.Console.ANSI          (hClearFromCursorToScreenEnd,
+                                               hClearLine, hClearScreen,
+                                               hCursorDown, hCursorUp,
+                                               hSetCursorColumn, hShowCursor)
+import           Text.PrettyPrint.ANSI.Leijen hiding ((<$>), (<>))
+
+buildMatch :: Int -> Int -> Match -> Int -> Doc
+buildMatch width selected match current =
+  let (normal,highlight) =
+        if  selected == current
+        then (onwhite,onred . black)
+        else (id,red)
+      fulltext = matchText match
+      before = ttext $ T.take (matchStart match) fulltext
+      matching = ttext $ T.drop (matchStart match) (T.take (matchEnd match + 1) fulltext)
+      end = ttext $ T.drop (matchEnd match + 1) fulltext
+  in normal before <> highlight matching <> normal end
+
+tshow :: Show a => a -> T.Text
+tshow = T.pack . show
+
+ttext :: T.Text -> Doc
+ttext = text . T.unpack
+
+-- renderSearch :: TTY -> Int -> [Match] -> IO ()
+renderSearch tty index matches (Search search) = do
+  let width = getWidth tty
+      height = getHeight tty
+      body :: Doc
+      body = linebreak <> (mconcat . intersperse linebreak
+             $ zipWith (buildMatch width index) matches [0..height - 2])
+      searchLine :: T.Text
+      searchLine =  tshow (length matches) <> " > " <> search
+      h = ttyHandle tty
+  hClearFromCursorToScreenEnd h
+  termPrint tty body
+  hCursorUp h (length matches)
+  hSetCursorColumn h 0
+  hClearLine h
+  termPrint tty (ttext searchLine)
+  hShowCursor h
+
+termPrint (TTY handle term _ _ ) = hPutDoc handle
diff --git a/src/Dustme/Score.hs b/src/Dustme/Score.hs
new file mode 100644
--- /dev/null
+++ b/src/Dustme/Score.hs
@@ -0,0 +1,69 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+module Dustme.Score where
+import           Data.Char          (isSpace, toLower)
+import           Data.List          (minimumBy, sortBy)
+import           Data.List.NonEmpty (NonEmpty (..))
+import           Data.Map           (Map)
+import qualified Data.Map           as Map
+import           Data.Maybe         (fromMaybe, listToMaybe, mapMaybe)
+import           Data.Ord           (comparing)
+import           Data.Set           (Set)
+import qualified Data.Set           as Set
+import           Data.Text          (Text, pack)
+import qualified Data.Text          as T
+import           Dustme.Types
+import           Prelude            hiding ((!!))
+import           Safe
+
+type Score = Int
+type Position = Int
+
+getIndices :: Text -> Map Char (Set Int)
+getIndices =
+  snd . T.foldl'
+  (\(i::Int,dict) c ->
+      (i+1, Map.insertWith Set.union (toLower c) (Set.singleton i) dict))
+  (0, Map.empty)
+
+matchComparison m1 m2 =
+  case compare (matchScore m1) (matchScore m2) of
+    EQ -> compare (matchStart m1) (matchStart m2)
+    x -> x
+
+mkMatch :: Text -> ([Int], Int) -> Maybe Match
+mkMatch _ ([],_) = Nothing
+mkMatch t (xs,cost) = Just $ Match cost (head xs) (last xs) t
+
+bestMatches :: Text -> Text -> [Match] -- [([Int], Int)]
+bestMatches t keys =
+    sortBy matchComparison
+  $ mapMaybe (mkMatch keys . (\(p,_) -> (reverse p, scorePath p)))
+  $ T.foldl' search [([],0)] t
+  where
+    dict = getIndices keys
+
+    initials :: Set Int
+    -- we add 1 because we want the value _after_ whitespace.
+    initials = Set.unions . map (Set.map (+1) . snd)  . Map.toList
+             $ Map.filterWithKey (\k _ -> isSpace k)  dict
+
+    scorePath :: [Int] -> Int
+    scorePath [] = 10000
+    scorePath [_] = 0
+    scorePath (x:y:xs)
+      | Set.member y initials = 1 + scorePath (y:xs)
+      | otherwise             = x - y + scorePath (y:xs)
+
+    search :: [([Int], Int)] -> Char -> [([Int], Int)]
+    search paths c =
+      concatMap
+      (\(path, earliest) ->
+         let next = Set.toList $ okPaths earliest continuations
+         in map (\j -> (j:path, j+1)) next
+      ) paths
+      where continuations = fromMaybe Set.empty $ Map.lookup (toLower c) dict
+
+okPaths :: Ord a => a -> Set a -> Set a
+okPaths x xs = case Set.splitMember x xs of
+  (_,True,b) -> Set.insert x b
+  (_,_,b)    -> b
diff --git a/src/Dustme/Search.hs b/src/Dustme/Search.hs
new file mode 100644
--- /dev/null
+++ b/src/Dustme/Search.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE TupleSections #-}
+module Dustme.Search where
+
+import           Control.Monad       (guard)
+import           Data.Char           (isSpace)
+import           Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HM
+import           Data.List           (sortBy)
+import           Data.Maybe          (mapMaybe)
+import           Data.Monoid         ((<>))
+import           Data.Ord            (comparing)
+import           Data.Text           (Text)
+import qualified Data.Text           as T
+import           Dustme.Score        (bestMatches, getIndices, matchComparison)
+import           Dustme.Types
+import           Safe                (headMay)
+
+-- Could be more sophisticated here
+isBoundaryChar = isSpace
+
+emptyCache = HM.empty
+
+getResults :: [Text]
+           -> SearchCache
+           -> Search
+           -> (SearchResult, SearchCache)
+getResults candidates cache search = case HM.lookup search cache of
+  Nothing -> let result = runSearch search candidates in
+               (result, HM.insert search result cache)
+  Just cached -> (cached, cache)
+
+runSearch :: Search -> [Text] -> SearchResult
+runSearch (Search st) candidates =
+  sortBy matchComparison $
+  mapMaybe (headMay .  bestMatches st) candidates
+
+
+applyOp ::  [Text] ->  SearchOp -> Search -> SearchResult -> (Search,[Text])
+applyOp candidates op (Search st) matches =
+  case op of
+    AddText t ->  ( Search (st <> t)
+                  , map matchText matches)
+    Backspace ->  ( Search (T.dropEnd 1 st)
+                  , candidates)
+    DeleteWord -> ( Search $ T.dropWhileEnd (not . isBoundaryChar) st
+                  , candidates)
diff --git a/src/Dustme/TTY.hs b/src/Dustme/TTY.hs
new file mode 100644
--- /dev/null
+++ b/src/Dustme/TTY.hs
@@ -0,0 +1,90 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Dustme.TTY
+  ( withTTY
+  , TermOutput(..)
+  , getWidth
+  , getHeight
+  )
+where
+import           Control.Applicative              ((<|>))
+import           Control.Concurrent               (newEmptyMVar, putMVar)
+import           Control.Concurrent.Async         (async, cancel)
+import           Control.Exception                (bracket)
+import           Control.Monad                    (forever)
+import           Data.Attoparsec.ByteString.Char8
+import qualified Data.ByteString.Char8            as BS8
+import           Data.IORef
+import           Data.Maybe                       (fromMaybe)
+import           Data.Text                        (Text)
+import qualified Data.Text                        as T
+import           Dustme.Types
+import           System.Console.Terminfo
+import           System.IO                        (BufferMode (NoBuffering),
+                                                   Handle (..), IOMode (..),
+                                                   hGetChar, hReady,
+                                                   hSetBuffering, openFile)
+import qualified Text.PrettyPrint.ANSI.Leijen     as PP
+
+withTTY :: FilePath -> (TTY -> IO ()) -> IO ()
+withTTY fp  = bracket setup teardown
+  where setup = do
+          t <- setupTermFromEnv
+          h <- openFile "/dev/tty" ReadWriteMode
+          hSetBuffering h NoBuffering
+          mv <- newEmptyMVar
+          p <- mkCommandReader h
+          reader <- async $ forever  (p >>= putMVar mv)
+          return (TTY h t mv reader)
+
+        teardown (TTY h t mv reader) = cancel reader
+
+mkCommandReader h = parser <$> newIORef ""
+  where
+    parser ref = do
+      leftovers <- atomicModifyIORef ref (\x -> (x,""))
+      res <- parseWith (getFromHandle h) commParser leftovers
+      case res of
+        Fail s a b -> error (show ("can't happen error", s, a, b))
+        Partial _ -> error "shouldn't happen - parseWith can resupply"
+        Done bs a -> do
+          writeIORef ref bs
+          return a
+
+commParser = choice
+  [ Edit . const Backspace <$> string "\DEL"
+  , Edit . const DeleteWord <$> string "\ETB"
+  , const Down <$> string "\SO"
+  , const Up <$> string "\DLE"
+  , const Accept <$> string "\n"
+  , Edit . AddText . T.pack . listify <$> anyChar
+  ]
+  where listify a = [a]
+-- still need to treat any of these specially?
+-- KEY_CTRL_C = ?\C-c
+-- KEY_CTRL_N = ?\C-n
+-- KEY_CTRL_P = ?\C-p
+-- KEY_CTRL_U = ?\C-u
+-- KEY_CTRL_H = ?\C-h
+-- KEY_CTRL_W = ?\C-w
+-- KEY_CTRL_J = ?\C-j
+-- KEY_CTRL_M = ?\C-m
+-- KEY_DELETE = 127.chr # Equivalent to ?\C-?
+
+dowhile :: IO Bool -> IO a -> IO [a]
+dowhile p f = (:) <$> f <*> while p f
+
+while :: IO Bool -> IO a -> IO [a]
+while p f = do
+  go <- p
+  if go
+    then (:) <$> f <*> while p f
+    else return []
+
+getFromHandle :: Handle -> IO BS8.ByteString
+getFromHandle h = BS8.pack <$> dowhile (hReady h) (hGetChar h)
+
+getWidth :: TTY -> Int
+getWidth t = fromMaybe (error "width not defined") (getCapability (ttyTerm t) termColumns)
+
+getHeight :: TTY -> Int
+getHeight t = fromMaybe (error "heighnot defined") (getCapability (ttyTerm t) termLines)
diff --git a/src/Dustme/Types.hs b/src/Dustme/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Dustme/Types.hs
@@ -0,0 +1,48 @@
+{-# LANGUAGE DeriveGeneric #-}
+module Dustme.Types where
+import           Control.Concurrent.Async (Async)
+import           Control.Concurrent.MVar  (MVar)
+import           Control.DeepSeq          (NFData)
+import           Data.Hashable            (Hashable)
+import           Data.HashMap.Strict      (HashMap)
+import           Data.Text                (Text)
+import           GHC.Generics
+import           System.Console.Terminfo
+import           System.IO                (Handle)
+
+data  TTY = TTY
+  { ttyHandle     :: Handle
+  , ttyTerm       :: Terminal
+  , ttyGetCommand :: MVar Command
+  , ttyProcess    :: Async ()
+  }
+
+data Match =
+  Match
+  { matchScore :: Int
+  , matchStart :: Int
+  , matchEnd   :: Int
+  , matchText  :: Text
+  } deriving (Show,Eq,Generic)
+
+instance NFData Match
+
+newtype Search = Search Text
+  deriving (Generic, Eq, Show)
+
+instance Hashable Search
+
+type SearchResult = [Match]
+
+data SearchOp = AddText Text
+              | Backspace
+              | DeleteWord
+  deriving (Eq,Show)
+
+data Command
+  = Accept
+  | Edit SearchOp
+  | Up
+  | Down
+
+type SearchCache = HashMap Search SearchResult
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE OverloadedStrings #-}
+import           Dustme
+import           Dustme.Score
+import           Dustme.Search
+import           Dustme.Types
+import           Test.Hspec
+
+main :: IO ()
+main = hspec spec
+
+spec = describe "applyOp" $ do
+  let applyOp' = applyOp ["a", "ab", "abc", "xyzzy"]
+  it "can apply an op" $ do
+    applyOp' (AddText "foo") (Search "") (map mkTrivialMatch ["a"])
+      `shouldBe` (Search "foo", ["a"])
+
+
+  it "can apply a complex op" $ do
+    applyOp' (AddText "b") (Search "a") (map mkTrivialMatch ["ab"])
+      `shouldBe` (Search "ab", ["ab"])
