diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/README.markdown b/README.markdown
new file mode 100644
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,34 @@
+The Haskell Robot
+=================
+
+**Robot** is a library for sending fake keyboard and mouse events using
+[Haskell][]. Applications will respond as though the user performed the
+actions themselves.
+
+[Haskell]: http://haskell.org/
+
+Only X11 systems are supported right now, but Windows and Mac can be
+added later if anyone cares.
+
+Here are some things you can do:
+
+* Test graphical interfaces and websites!
+
+* Play silly pranks on your friends!
+
+* Finally beat those [silly Flash games where you press the same button
+  over and over again really fast][blah blah blah]. I don't like those.
+
+[blah blah blah]: http://mixermike622.deviantart.com/art/GO-FAST-287788805
+
+
+Quick start
+-----------
+
+First, download and install the library:
+
+    git clone https://github.com/lfairy/robot.git
+    cd robot
+    cabal install
+
+Now try running some of the programs in the `examples` directory.
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/Test/Robot.hs b/Test/Robot.hs
new file mode 100644
--- /dev/null
+++ b/Test/Robot.hs
@@ -0,0 +1,89 @@
+-- | The main Robot interface.
+
+module Test.Robot
+    (
+      -- * Running your robot
+      Robot()  -- hide implementation
+    , runRobot
+    , runRobotWithConnection
+
+      -- * Key and button constants
+    , module Test.Robot.Types
+
+      -- * Doing things
+      -- ** Basic operations
+    , Pressable(press, release)
+    , moveBy
+    , moveTo
+      -- ** Derived operations
+    , hold
+    , tap
+
+      -- * Fancy utilities
+    , sleep
+
+    ) where
+
+
+import Control.Applicative
+import Control.Concurrent (threadDelay)
+import Control.Monad.IO.Class
+
+import Test.Robot.Internal
+import Test.Robot.Types
+
+
+-- | Represents things that can be pressed: either a key on a keyboard
+-- or a button on a mouse.
+class Pressable x where
+
+    -- | Press a key or button.
+    press :: x -> Robot ()
+
+    -- | Release a key or button.
+    release :: x -> Robot ()
+
+instance Pressable Key where
+    press = keyboard True
+    release = keyboard False
+
+instance Pressable Button where
+    press = button True
+    release = button False
+
+
+-- | Move the pointer by an offset.
+moveBy :: Int -> Int -> Robot ()
+moveBy = motion True
+
+-- | Move the pointer to a point on the screen.
+moveTo :: Int -> Int -> Robot ()
+moveTo = motion False
+
+
+-- | @hold x act@ holds down @x@ while executing @act@. It is equivalent
+-- to the code:
+--
+-- @
+-- press x
+-- act
+-- release x
+-- @
+--
+-- For example, you type some text in ALL CAPS using:
+--
+-- @
+-- hold _Shift $ mapM_ tap [_D, _U, _C, _K, _S]
+-- @
+--
+hold :: Pressable x => x -> Robot a -> Robot a
+hold = bracketRobot_ <$> press <*> release
+
+-- | Press the argument, then release it.
+tap :: Pressable x => x -> Robot ()
+tap = (`hold` return ())
+
+
+-- | Do nothing for the specified number of seconds.
+sleep :: Rational -> Robot ()
+sleep = liftIO . threadDelay . round . (* 1000000)
diff --git a/Test/Robot/Internal.hs b/Test/Robot/Internal.hs
new file mode 100644
--- /dev/null
+++ b/Test/Robot/Internal.hs
@@ -0,0 +1,101 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+-- | Miscellaneous low-level functions. You should rarely need to use
+-- these directly.
+
+module Test.Robot.Internal
+    (
+      -- * The Robot monad
+      Robot(..)
+    , runRobot
+    , runRobotWithConnection
+    , mkRobot
+    , mkRobot'
+
+      -- * Exception safety
+    , bracketRobot_
+
+      -- * Synthesizing events
+    , keyboard
+    , button
+    , motion
+    , releaseAll
+
+    ) where
+
+
+import Control.Applicative
+import Control.Exception (bracket_, finally)
+import Control.Monad.IO.Class
+import Control.Monad.Trans.Reader
+import Data.Map (Map)
+import qualified Data.Map as M
+
+import Graphics.XHB
+
+import qualified Test.Robot.Internal.XTest as X
+import Test.Robot.Types
+
+
+-- | The Robot monad: a reader monad over IO.
+newtype Robot a = Robot { unRobot :: ReaderT (Connection, Map KEYSYM KEYCODE) IO a }
+    deriving (Functor, Applicative, Monad, MonadIO)
+
+-- | Run the robot, connecting to the display automatically.
+runRobot :: Robot a -> IO a
+runRobot m = do
+    Just c <- connect
+    runRobotWithConnection m c
+
+-- | Run the robot using an existing connection.
+runRobotWithConnection :: Robot a -> Connection -> IO a
+runRobotWithConnection (Robot m) c = do
+    keymap <- X.getKeysymMap c
+    runReaderT m (c, keymap) `finally` X.releaseAll c
+
+mkRobot :: ((Connection, Map KEYSYM KEYCODE) -> IO a) -> Robot a
+mkRobot = Robot . ReaderT
+
+mkRobot' :: (Connection -> IO a) -> Robot a
+mkRobot' = mkRobot . (. fst)
+
+
+bracketRobot_ :: Robot a -> Robot z -> Robot r -> Robot r
+bracketRobot_ (Robot before) (Robot after) (Robot middle)
+    = mkRobot $ \env -> let run = flip runReaderT env
+                        in bracket_ (run before) (run after) (run middle)
+
+
+keyboard :: Bool -> Key -> Robot ()
+keyboard press key = mkRobot $ \(c, keymap) -> do
+    case M.lookup (rawKey key) keymap of
+        Nothing -> error $ "Unknown keysym: " ++ show (rawKey key)
+        Just keycode -> X.keyboard c press keycode
+
+button :: Bool -> Button -> Robot ()
+button press butt {- LOL -} = mkRobot' $ \c ->
+    X.button c press (rawButton butt)
+
+motion :: Bool -> Int -> Int -> Robot ()
+motion press x y = mkRobot' $ \c ->
+    X.motion c press (clipIntegral x, clipIntegral y)
+
+-- | @clipIntegral x :: T@ converts the value @x@ to type @T@.
+-- If the argument does not fit in @T@, it is clipped, rather than
+-- wrapping around as with 'fromIntegral'.
+--
+-- The range of the result type must be a subset of the argument type.
+-- If not, the behavior of this function is undefined.
+--
+clipIntegral :: (Integral a, Integral b, Ord a, Bounded b) => a -> b
+clipIntegral x = result
+  where
+    result
+      | x < fromIntegral (minBound `asTypeOf` result) = minBound
+      | x > fromIntegral (maxBound `asTypeOf` result) = maxBound
+      | otherwise = fromIntegral x
+
+-- | Release all the keys and buttons, in case some were left held down.
+-- This is called automatically before 'runRobot' returns.
+releaseAll :: Robot ()
+releaseAll = mkRobot' $ \c -> X.releaseAll c
diff --git a/Test/Robot/Internal/XTest.hs b/Test/Robot/Internal/XTest.hs
new file mode 100644
--- /dev/null
+++ b/Test/Robot/Internal/XTest.hs
@@ -0,0 +1,115 @@
+-- | Low-level wrappers around the XTest protocol.
+
+module Test.Robot.Internal.XTest
+    (
+    -- * XTest primitives
+      keyboard
+    , button
+    , motion
+    , releaseAll
+
+    -- * Utility functions
+    , getKeysymMap
+
+    ) where
+
+import Control.Arrow (second)
+import Control.Applicative (liftA2)
+import Control.Monad (forM_, (>=>))
+import Data.List (unfoldr)
+import Data.Map (Map)
+import qualified Data.Map as M
+import Data.Tuple (swap)
+import Data.Word (Word16)
+
+import Graphics.XHB
+import Graphics.XHB.Gen.Test
+
+
+keyboard :: Connection -> Bool -> KEYCODE -> IO ()
+keyboard c press keycode =
+    let eventType = if press then 2 else 3
+        req = MkFakeInput eventType keycode 0 noWindow 0 0 0
+    in fakeInput c req
+
+
+button :: Connection -> Bool -> BUTTON -> IO ()
+button c press butt =
+    let eventType = if press then 4 else 5
+        req = MkFakeInput eventType butt 0 noWindow 0 0 0
+    in fakeInput c req
+
+
+motion :: Connection -> Bool -> (Word16, Word16) -> IO ()
+motion c relative (x, y) =
+    let eventType = 6
+        isRelative = if relative then 1 else 0
+        req = MkFakeInput eventType isRelative 0 noWindow x y 0
+    in fakeInput c req
+
+
+noWindow :: WINDOW
+noWindow = fromXid xidNone
+
+
+-- | Release all the keys and buttons, in case some were left held down.
+releaseAll :: Connection -> IO ()
+releaseAll c = do
+    let (low, high) = keycodeRange c
+    forM_ [low..high] $ keyboard c False
+    -- There's no limit to the number of buttons, afaik. So hazard a guess.
+    forM_ [1..42] $ button c False
+
+
+-- | Return a structure mapping each keycode to its keysyms.
+getKeyboardMap :: Connection -> IO (Map KEYCODE [KEYSYM])
+getKeyboardMap c = do
+    let (low, high) = keycodeRange c
+    MkGetKeyboardMappingReply
+        { keysyms_per_keycode_GetKeyboardMappingReply = blockSize
+        , keysyms_GetKeyboardMappingReply = rawKeysyms }
+            <- getKeyboardMapping c low (high-low+1) >>= getReply'
+    return . M.fromList $ zip [low..high]
+                                (chunksOf (fromIntegral blockSize) rawKeysyms)
+
+
+-- | Like 'getReply', but calls @error@ instead of returning @Left@.
+getReply' :: Receipt a -> IO a
+getReply' = getReply >=> either (error . show) return
+
+
+-- | Split the input list into chunks of size @k@. The last chunk may be
+-- shorter than the other chunks, depending on the length of the input.
+-- Exammples:
+--
+-- > chunksOf 2 [1..6] == [[1, 2], [3, 4], [5, 6]]
+-- > chunksOf 3 "Hello, world!" == ["Hel", "lo,", " wo", "rld", "!"]
+--
+chunksOf :: Int -> [a] -> [[a]]
+chunksOf k = unfoldr $ \xs -> case xs of
+    [] -> Nothing
+    _  -> Just (splitAt k xs)
+
+
+-- | Return a structure mapping keysyms to their keycode.
+getKeysymMap :: Connection -> IO (Map KEYSYM KEYCODE)
+getKeysymMap = fmap flipTable . getKeyboardMap
+
+
+flipTable :: Map KEYCODE [KEYSYM] -> Map KEYSYM KEYCODE
+flipTable
+    = M.fromList
+    . map swap
+    . concatMap expandEntry
+    . removeZeroes
+    . M.toList
+  where
+    removeZeroes = map (second (filter (/= 0)))
+
+    expandEntry :: (a, [b]) -> [(a, b)]
+    expandEntry (x, ys) = zip (repeat x) ys
+
+
+keycodeRange :: Connection -> (KEYCODE, KEYCODE)
+keycodeRange
+    = liftA2 (,) min_keycode_Setup max_keycode_Setup . connectionSetup
diff --git a/Test/Robot/Types.hs b/Test/Robot/Types.hs
new file mode 100644
--- /dev/null
+++ b/Test/Robot/Types.hs
@@ -0,0 +1,61 @@
+-- | Defines the 'Key' and 'Button' types, along with a list of
+-- constants of those types.
+
+module Test.Robot.Types
+    (
+      -- * Types
+      Key()  -- Hide implementation
+    , Button()
+
+      -- ** Creating
+      -- $custom
+    , customKey
+    , customButton
+
+      -- ** Extracting
+    , rawKey
+    , rawButton
+
+      -- * Constants
+      -- ** Buttons
+    , leftButton, middleButton, rightButton
+    , scrollUp, scrollDown, scrollLeft, scrollRight
+    , backButton, forwardButton
+
+    , module Test.Robot.Types.Keys
+
+    ) where
+
+import Graphics.XHB
+
+import Test.Robot.Types.Core
+import Test.Robot.Types.Keys
+
+-- $custom
+-- These two functions let you define your own keys and buttons.
+--
+-- 'customKey' requires a magic number (/keysym/) as its argument. On
+-- most Unix systems, you can look up common keysyms in the file
+-- @\/usr\/include\/X11\/keysymdef.h@.
+--
+-- For example, the pound sign @£@ is listed as:
+--
+-- > #define XK_sterling                      0x00a3  /* U+00A3 POUND SIGN */
+--
+-- so to input this key, we use @customKey 0x00a3@.
+--
+-- Note: this feature only works if the key actually exists on your
+-- keyboard. If not, then using the result will raise an exception.
+
+customKey :: KEYSYM -> Key
+customKey = Key
+
+customButton :: BUTTON -> Button
+customButton = Button
+
+leftButton, middleButton, rightButton :: Button
+scrollUp, scrollDown, scrollLeft, scrollRight :: Button
+backButton, forwardButton :: Button
+[leftButton, middleButton, rightButton,
+    scrollUp, scrollDown, scrollLeft, scrollRight,
+    backButton, forwardButton] = map Button [1..9]
diff --git a/Test/Robot/Types/Core.hs b/Test/Robot/Types/Core.hs
new file mode 100644
--- /dev/null
+++ b/Test/Robot/Types/Core.hs
@@ -0,0 +1,18 @@
+module Test.Robot.Types.Core where
+
+import Graphics.XHB (BUTTON, KEYSYM)
+
+-- | A key on the keyboard.
+newtype Key = Key
+    { -- | Retrieve the internal identifier for this key.
+      rawKey :: KEYSYM }
+    deriving (Eq, Ord, Read, Show)
+
+-- | A mouse button or scroll wheel.
+--
+-- Note that the scroll wheel is treated as two separate buttons: one
+-- scrolling up, and one scrolling down.
+newtype Button = Button
+    { -- | Retrieve the internal identifier for this button.
+      rawButton :: BUTTON }
+    deriving (Eq, Ord, Read, Show)
diff --git a/Test/Robot/Types/Keys.hs b/Test/Robot/Types/Keys.hs
new file mode 100644
--- /dev/null
+++ b/Test/Robot/Types/Keys.hs
@@ -0,0 +1,569 @@
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+
+-- | This module is automatically generated from the file @keys.txt@,
+-- and should not be edited directly. If you want to add a keysym, refer
+-- to that file instead.
+
+module Test.Robot.Types.Keys
+    (
+      -- ** Control sequences
+      _Backspace
+    , _Tab
+    , _LineFeed
+    , _Clear
+    , _Return
+    , _Pause
+    , _ScrollLock
+    , _SysReq
+    , _Escape
+    , _Delete
+
+      -- ** Cursor control and motion
+    , _Home
+    , _Left
+    , _Up
+    , _Right
+    , _Down
+    , _Prior
+    , _PageUp
+    , _Next
+    , _PageDown
+    , _End
+    , _Begin
+
+      -- ** Miscellaneous functions
+    , _Select
+    , _Print
+    , _Execute
+    , _Insert
+    , _Undo
+    , _Redo
+    , _Menu
+    , _Find
+    , _Cancel
+    , _Help
+    , _Break
+    , _ModeSwitch
+    , _ScriptSwitch
+    , _NumLock
+
+      -- ** Keypad functions
+    , _KP_Space
+    , _KP_Tab
+    , _KP_Enter
+    , _KP_F1
+    , _KP_F2
+    , _KP_F3
+    , _KP_F4
+    , _KP_Home
+    , _KP_Left
+    , _KP_Up
+    , _KP_Right
+    , _KP_Down
+    , _KP_Prior
+    , _KP_PageUp
+    , _KP_Next
+    , _KP_PageDown
+    , _KP_End
+    , _KP_Begin
+    , _KP_Insert
+    , _KP_Delete
+    , _KP_Multiply
+    , _KP_Add
+    , _KP_Separator
+    , _KP_Subtract
+    , _KP_Decimal
+    , _KP_Divide
+    , _KP_Equal
+
+      -- ** Keypad numbers
+    , _KP_0
+    , _KP_1
+    , _KP_2
+    , _KP_3
+    , _KP_4
+    , _KP_5
+    , _KP_6
+    , _KP_7
+    , _KP_8
+    , _KP_9
+
+      -- ** Function keys
+    , _F1
+    , _F2
+    , _F3
+    , _F4
+    , _F5
+    , _F6
+    , _F7
+    , _F8
+    , _F9
+    , _F10
+    , _F11
+    , _F12
+    , _F13
+    , _F14
+    , _F15
+    , _F16
+    , _F17
+    , _F18
+    , _F19
+
+      -- ** Modifiers
+    , _Shift
+    , _Shift_L
+    , _Shift_R
+    , _Ctrl
+    , _Control
+    , _Control_L
+    , _Control_R
+    , _CapsLock
+    , _ShiftLock
+
+      -- ** More modifiers
+    , _Meta
+    , _Meta_L
+    , _Meta_R
+    , _Alt
+    , _Alt_L
+    , _Alt_R
+    , _Super
+    , _Super_L
+    , _Super_R
+    , _Hyper
+    , _Hyper_L
+    , _Hyper_R
+
+      -- ** Letters, numbers and symbols
+    , _Space
+    , _Exclamation
+    , _Quotation
+    , _NumberSign
+    , _Dollar
+    , _Percent
+    , _Ampersand
+    , _Apostrophe
+    , _ParenLeft
+    , _ParenRight
+    , _Asterisk
+    , _Plus
+    , _Comma
+    , _Minus
+    , _Period
+    , _Slash
+    , _0
+    , _1
+    , _2
+    , _3
+    , _4
+    , _5
+    , _6
+    , _7
+    , _8
+    , _9
+    , _Colon
+    , _Semicolon
+    , _Less
+    , _Equal
+    , _Greater
+    , _Question
+    , _At
+    , _A
+    , _B
+    , _C
+    , _D
+    , _E
+    , _F
+    , _G
+    , _H
+    , _I
+    , _J
+    , _K
+    , _L
+    , _M
+    , _N
+    , _O
+    , _P
+    , _Q
+    , _R
+    , _S
+    , _T
+    , _U
+    , _V
+    , _W
+    , _X
+    , _Y
+    , _Z
+    , _BracketLeft
+    , _Backslash
+    , _BracketRight
+    , _Circumflex
+    , _Underscore
+    , _Grave
+    , _BraceLeft
+    , _Bar
+    , _BraceRight
+    , _Tilde
+
+    ) where
+
+import Test.Robot.Types.Core
+
+_Backspace :: Key
+_Backspace                     = Key 0xff08
+_Tab :: Key
+_Tab                           = Key 0xff09
+_LineFeed :: Key
+_LineFeed                      = Key 0xff0a
+_Clear :: Key
+_Clear                         = Key 0xff0b
+_Return :: Key
+_Return                        = Key 0xff0d
+_Pause :: Key
+_Pause                         = Key 0xff13
+_ScrollLock :: Key
+_ScrollLock                    = Key 0xff14
+_SysReq :: Key
+_SysReq                        = Key 0xff15
+_Escape :: Key
+_Escape                        = Key 0xff1b
+_Delete :: Key
+_Delete                        = Key 0xffff
+
+_Home :: Key
+_Home                          = Key 0xff50
+_Left :: Key
+_Left                          = Key 0xff51
+_Up :: Key
+_Up                            = Key 0xff52
+_Right :: Key
+_Right                         = Key 0xff53
+_Down :: Key
+_Down                          = Key 0xff54
+_Prior, _PageUp :: Key
+_Prior                         = Key 0xff55
+_PageUp                        = Key 0xff55
+_Next, _PageDown :: Key
+_Next                          = Key 0xff56
+_PageDown                      = Key 0xff56
+_End :: Key
+_End                           = Key 0xff57
+_Begin :: Key
+_Begin                         = Key 0xff58
+
+_Select :: Key
+_Select                        = Key 0xff60
+_Print :: Key
+_Print                         = Key 0xff61
+_Execute :: Key
+_Execute                       = Key 0xff62
+_Insert :: Key
+_Insert                        = Key 0xff63
+_Undo :: Key
+_Undo                          = Key 0xff65
+_Redo :: Key
+_Redo                          = Key 0xff66
+_Menu :: Key
+_Menu                          = Key 0xff67
+_Find :: Key
+_Find                          = Key 0xff68
+_Cancel :: Key
+_Cancel                        = Key 0xff69
+_Help :: Key
+_Help                          = Key 0xff6a
+_Break :: Key
+_Break                         = Key 0xff6b
+_ModeSwitch, _ScriptSwitch :: Key
+_ModeSwitch                    = Key 0xff7e
+_ScriptSwitch                  = Key 0xff7e
+_NumLock :: Key
+_NumLock                       = Key 0xff7f
+
+_KP_Space :: Key
+_KP_Space                      = Key 0xff80
+_KP_Tab :: Key
+_KP_Tab                        = Key 0xff89
+_KP_Enter :: Key
+_KP_Enter                      = Key 0xff8d
+_KP_F1 :: Key
+_KP_F1                         = Key 0xff91
+_KP_F2 :: Key
+_KP_F2                         = Key 0xff92
+_KP_F3 :: Key
+_KP_F3                         = Key 0xff93
+_KP_F4 :: Key
+_KP_F4                         = Key 0xff94
+_KP_Home :: Key
+_KP_Home                       = Key 0xff95
+_KP_Left :: Key
+_KP_Left                       = Key 0xff96
+_KP_Up :: Key
+_KP_Up                         = Key 0xff97
+_KP_Right :: Key
+_KP_Right                      = Key 0xff98
+_KP_Down :: Key
+_KP_Down                       = Key 0xff99
+_KP_Prior, _KP_PageUp :: Key
+_KP_Prior                      = Key 0xff9a
+_KP_PageUp                     = Key 0xff9a
+_KP_Next, _KP_PageDown :: Key
+_KP_Next                       = Key 0xff9b
+_KP_PageDown                   = Key 0xff9b
+_KP_End :: Key
+_KP_End                        = Key 0xff9c
+_KP_Begin :: Key
+_KP_Begin                      = Key 0xff9d
+_KP_Insert :: Key
+_KP_Insert                     = Key 0xff9e
+_KP_Delete :: Key
+_KP_Delete                     = Key 0xff9f
+_KP_Multiply :: Key
+_KP_Multiply                   = Key 0xffaa
+_KP_Add :: Key
+_KP_Add                        = Key 0xffab
+_KP_Separator :: Key
+_KP_Separator                  = Key 0xffac
+_KP_Subtract :: Key
+_KP_Subtract                   = Key 0xffad
+_KP_Decimal :: Key
+_KP_Decimal                    = Key 0xffae
+_KP_Divide :: Key
+_KP_Divide                     = Key 0xffaf
+_KP_Equal :: Key
+_KP_Equal                      = Key 0xffbd
+
+_KP_0 :: Key
+_KP_0                          = Key 0xffb0
+_KP_1 :: Key
+_KP_1                          = Key 0xffb1
+_KP_2 :: Key
+_KP_2                          = Key 0xffb2
+_KP_3 :: Key
+_KP_3                          = Key 0xffb3
+_KP_4 :: Key
+_KP_4                          = Key 0xffb4
+_KP_5 :: Key
+_KP_5                          = Key 0xffb5
+_KP_6 :: Key
+_KP_6                          = Key 0xffb6
+_KP_7 :: Key
+_KP_7                          = Key 0xffb7
+_KP_8 :: Key
+_KP_8                          = Key 0xffb8
+_KP_9 :: Key
+_KP_9                          = Key 0xffb9
+
+_F1 :: Key
+_F1                            = Key 0xffbe
+_F2 :: Key
+_F2                            = Key 0xffbf
+_F3 :: Key
+_F3                            = Key 0xffc0
+_F4 :: Key
+_F4                            = Key 0xffc1
+_F5 :: Key
+_F5                            = Key 0xffc2
+_F6 :: Key
+_F6                            = Key 0xffc3
+_F7 :: Key
+_F7                            = Key 0xffc4
+_F8 :: Key
+_F8                            = Key 0xffc5
+_F9 :: Key
+_F9                            = Key 0xffc6
+_F10 :: Key
+_F10                           = Key 0xffc7
+_F11 :: Key
+_F11                           = Key 0xffc8
+_F12 :: Key
+_F12                           = Key 0xffc9
+_F13 :: Key
+_F13                           = Key 0xffca
+_F14 :: Key
+_F14                           = Key 0xffcb
+_F15 :: Key
+_F15                           = Key 0xffcc
+_F16 :: Key
+_F16                           = Key 0xffcd
+_F17 :: Key
+_F17                           = Key 0xffce
+_F18 :: Key
+_F18                           = Key 0xffcf
+_F19 :: Key
+_F19                           = Key 0xffd0
+
+_Shift, _Shift_L :: Key
+_Shift                         = Key 0xffe1
+_Shift_L                       = Key 0xffe1
+_Shift_R :: Key
+_Shift_R                       = Key 0xffe2
+_Ctrl, _Control, _Control_L :: Key
+_Ctrl                          = Key 0xffe3
+_Control                       = Key 0xffe3
+_Control_L                     = Key 0xffe3
+_Control_R :: Key
+_Control_R                     = Key 0xffe4
+_CapsLock :: Key
+_CapsLock                      = Key 0xffe5
+_ShiftLock :: Key
+_ShiftLock                     = Key 0xffe6
+
+_Meta, _Meta_L :: Key
+_Meta                          = Key 0xffe7
+_Meta_L                        = Key 0xffe7
+_Meta_R :: Key
+_Meta_R                        = Key 0xffe8
+_Alt, _Alt_L :: Key
+_Alt                           = Key 0xffe9
+_Alt_L                         = Key 0xffe9
+_Alt_R :: Key
+_Alt_R                         = Key 0xffea
+_Super, _Super_L :: Key
+_Super                         = Key 0xffeb
+_Super_L                       = Key 0xffeb
+_Super_R :: Key
+_Super_R                       = Key 0xffec
+_Hyper, _Hyper_L :: Key
+_Hyper                         = Key 0xffed
+_Hyper_L                       = Key 0xffed
+_Hyper_R :: Key
+_Hyper_R                       = Key 0xffee
+
+_Space :: Key
+_Space                         = Key 0x0020
+_Exclamation :: Key
+_Exclamation                   = Key 0x0021
+_Quotation :: Key
+_Quotation                     = Key 0x0022
+_NumberSign :: Key
+_NumberSign                    = Key 0x0023
+_Dollar :: Key
+_Dollar                        = Key 0x0024
+_Percent :: Key
+_Percent                       = Key 0x0025
+_Ampersand :: Key
+_Ampersand                     = Key 0x0026
+_Apostrophe :: Key
+_Apostrophe                    = Key 0x0027
+_ParenLeft :: Key
+_ParenLeft                     = Key 0x0028
+_ParenRight :: Key
+_ParenRight                    = Key 0x0029
+_Asterisk :: Key
+_Asterisk                      = Key 0x002a
+_Plus :: Key
+_Plus                          = Key 0x002b
+_Comma :: Key
+_Comma                         = Key 0x002c
+_Minus :: Key
+_Minus                         = Key 0x002d
+_Period :: Key
+_Period                        = Key 0x002e
+_Slash :: Key
+_Slash                         = Key 0x002f
+_0 :: Key
+_0                             = Key 0x0030
+_1 :: Key
+_1                             = Key 0x0031
+_2 :: Key
+_2                             = Key 0x0032
+_3 :: Key
+_3                             = Key 0x0033
+_4 :: Key
+_4                             = Key 0x0034
+_5 :: Key
+_5                             = Key 0x0035
+_6 :: Key
+_6                             = Key 0x0036
+_7 :: Key
+_7                             = Key 0x0037
+_8 :: Key
+_8                             = Key 0x0038
+_9 :: Key
+_9                             = Key 0x0039
+_Colon :: Key
+_Colon                         = Key 0x003a
+_Semicolon :: Key
+_Semicolon                     = Key 0x003b
+_Less :: Key
+_Less                          = Key 0x003c
+_Equal :: Key
+_Equal                         = Key 0x003d
+_Greater :: Key
+_Greater                       = Key 0x003e
+_Question :: Key
+_Question                      = Key 0x003f
+_At :: Key
+_At                            = Key 0x0040
+_A :: Key
+_A                             = Key 0x0041
+_B :: Key
+_B                             = Key 0x0042
+_C :: Key
+_C                             = Key 0x0043
+_D :: Key
+_D                             = Key 0x0044
+_E :: Key
+_E                             = Key 0x0045
+_F :: Key
+_F                             = Key 0x0046
+_G :: Key
+_G                             = Key 0x0047
+_H :: Key
+_H                             = Key 0x0048
+_I :: Key
+_I                             = Key 0x0049
+_J :: Key
+_J                             = Key 0x004a
+_K :: Key
+_K                             = Key 0x004b
+_L :: Key
+_L                             = Key 0x004c
+_M :: Key
+_M                             = Key 0x004d
+_N :: Key
+_N                             = Key 0x004e
+_O :: Key
+_O                             = Key 0x004f
+_P :: Key
+_P                             = Key 0x0050
+_Q :: Key
+_Q                             = Key 0x0051
+_R :: Key
+_R                             = Key 0x0052
+_S :: Key
+_S                             = Key 0x0053
+_T :: Key
+_T                             = Key 0x0054
+_U :: Key
+_U                             = Key 0x0055
+_V :: Key
+_V                             = Key 0x0056
+_W :: Key
+_W                             = Key 0x0057
+_X :: Key
+_X                             = Key 0x0058
+_Y :: Key
+_Y                             = Key 0x0059
+_Z :: Key
+_Z                             = Key 0x005a
+_BracketLeft :: Key
+_BracketLeft                   = Key 0x005b
+_Backslash :: Key
+_Backslash                     = Key 0x005c
+_BracketRight :: Key
+_BracketRight                  = Key 0x005d
+_Circumflex :: Key
+_Circumflex                    = Key 0x005e
+_Underscore :: Key
+_Underscore                    = Key 0x005f
+_Grave :: Key
+_Grave                         = Key 0x0060
+_BraceLeft :: Key
+_BraceLeft                     = Key 0x007b
+_Bar :: Key
+_Bar                           = Key 0x007c
+_BraceRight :: Key
+_BraceRight                    = Key 0x007d
+_Tilde :: Key
+_Tilde                         = Key 0x007e
diff --git a/examples/RightClick.hs b/examples/RightClick.hs
new file mode 100644
--- /dev/null
+++ b/examples/RightClick.hs
@@ -0,0 +1,10 @@
+-- | Open the right-click menu, then use the keyboard to select the
+-- first entry.
+
+import Test.Robot
+
+main = runRobot $ mapM_ (>> sleep 0.25)
+                        [tap rightButton, tap _Down, tap _Return]
+  where
+    _Down = customKey 0xff54
+    _Return = customKey 0xff0d
diff --git a/generate_keys.py b/generate_keys.py
new file mode 100644
--- /dev/null
+++ b/generate_keys.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+
+from collections import defaultdict
+from functools import partial
+
+HEADER = '''
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+
+-- | This module is automatically generated from the file @keys.txt@,
+-- and should not be edited directly. If you want to add a keysym, refer
+-- to that file instead.
+
+module Test.Robot.Types.Keys
+'''.strip()
+
+def read_blocks(raw):
+    for block in raw.split('\n\n'):
+        lines = block.strip().split('\n')
+        title = lines[0]
+
+        data = defaultdict(list)
+        for name, code in map(str.split, lines[1:]):
+            data[code].append(name)
+
+        yield title, data
+
+
+def dump_header(blocks, print=print):
+    print(HEADER)
+
+    print_i = partial(print, '   ')
+    print_i('(', end='')
+
+    first = True
+    for title, data in blocks:
+        print()
+        print_i('  -- **', title)
+        for code, names in sorted(data.items()):
+            for name in names:
+                print_i(' ' if first else ',', '_'+name)
+            first = False
+
+    print()
+    print_i(') where')
+    print()
+    print('import Test.Robot.Types.Core')
+
+
+def dump_definitions(blocks, print=print):
+    for title, data in blocks:
+        print()
+        for code, names in sorted(data.items()):
+            names = ['_' + name for name in names]
+            # Gather all the aliases into the same signature, so Haddock
+            # displays them on one line
+            print(', '.join(names), ':: Key')
+            for name in names:
+                print(name.ljust(30), '= Key', code)
+
+
+if __name__ == '__main__':
+    raw = open('keys.txt').read()
+    with open('Test/Robot/Types/Keys.hs', 'w', newline='\n') as out_f:
+        print = partial(print, file=out_f)
+        dump_header(read_blocks(raw), print)
+        dump_definitions(read_blocks(raw), print)
diff --git a/keys.txt b/keys.txt
new file mode 100644
--- /dev/null
+++ b/keys.txt
@@ -0,0 +1,198 @@
+Control sequences
+Backspace                     0xff08
+Tab                           0xff09
+LineFeed                      0xff0a
+Clear                         0xff0b
+Return                        0xff0d
+Pause                         0xff13
+ScrollLock                    0xff14
+SysReq                        0xff15
+Escape                        0xff1b
+Delete                        0xffff
+
+Cursor control and motion
+Home                          0xff50
+Left                          0xff51
+Up                            0xff52
+Right                         0xff53
+Down                          0xff54
+Prior                         0xff55
+PageUp                        0xff55
+Next                          0xff56
+PageDown                      0xff56
+End                           0xff57
+Begin                         0xff58
+
+Miscellaneous functions
+Select                        0xff60
+Print                         0xff61
+Execute                       0xff62
+Insert                        0xff63
+Undo                          0xff65
+Redo                          0xff66
+Menu                          0xff67
+Find                          0xff68
+Cancel                        0xff69
+Help                          0xff6a
+Break                         0xff6b
+ModeSwitch                    0xff7e
+ScriptSwitch                  0xff7e
+NumLock                       0xff7f
+
+Keypad functions
+KP_Space                      0xff80
+KP_Tab                        0xff89
+KP_Enter                      0xff8d
+KP_F1                         0xff91
+KP_F2                         0xff92
+KP_F3                         0xff93
+KP_F4                         0xff94
+KP_Home                       0xff95
+KP_Left                       0xff96
+KP_Up                         0xff97
+KP_Right                      0xff98
+KP_Down                       0xff99
+KP_Prior                      0xff9a
+KP_PageUp                     0xff9a
+KP_Next                       0xff9b
+KP_PageDown                   0xff9b
+KP_End                        0xff9c
+KP_Begin                      0xff9d
+KP_Insert                     0xff9e
+KP_Delete                     0xff9f
+KP_Equal                      0xffbd
+KP_Multiply                   0xffaa
+KP_Add                        0xffab
+KP_Separator                  0xffac
+KP_Subtract                   0xffad
+KP_Decimal                    0xffae
+KP_Divide                     0xffaf
+
+Keypad numbers
+KP_0                          0xffb0
+KP_1                          0xffb1
+KP_2                          0xffb2
+KP_3                          0xffb3
+KP_4                          0xffb4
+KP_5                          0xffb5
+KP_6                          0xffb6
+KP_7                          0xffb7
+KP_8                          0xffb8
+KP_9                          0xffb9
+
+Function keys
+F1                            0xffbe
+F2                            0xffbf
+F3                            0xffc0
+F4                            0xffc1
+F5                            0xffc2
+F6                            0xffc3
+F7                            0xffc4
+F8                            0xffc5
+F9                            0xffc6
+F10                           0xffc7
+F11                           0xffc8
+F12                           0xffc9
+F13                           0xffca
+F14                           0xffcb
+F15                           0xffcc
+F16                           0xffcd
+F17                           0xffce
+F18                           0xffcf
+F19                           0xffd0
+
+Modifiers
+Shift                         0xffe1
+Shift_L                       0xffe1
+Shift_R                       0xffe2
+Ctrl                          0xffe3
+Control                       0xffe3
+Control_L                     0xffe3
+Control_R                     0xffe4
+CapsLock                      0xffe5
+ShiftLock                     0xffe6
+
+More modifiers
+Meta                          0xffe7
+Meta_L                        0xffe7
+Meta_R                        0xffe8
+Alt                           0xffe9
+Alt_L                         0xffe9
+Alt_R                         0xffea
+Super                         0xffeb
+Super_L                       0xffeb
+Super_R                       0xffec
+Hyper                         0xffed
+Hyper_L                       0xffed
+Hyper_R                       0xffee
+
+Letters, numbers and symbols
+Space                         0x0020
+Exclamation                   0x0021
+Quotation                     0x0022
+NumberSign                    0x0023
+Dollar                        0x0024
+Percent                       0x0025
+Ampersand                     0x0026
+Apostrophe                    0x0027
+ParenLeft                     0x0028
+ParenRight                    0x0029
+Asterisk                      0x002a
+Plus                          0x002b
+Comma                         0x002c
+Minus                         0x002d
+Period                        0x002e
+Slash                         0x002f
+0                             0x0030
+1                             0x0031
+2                             0x0032
+3                             0x0033
+4                             0x0034
+5                             0x0035
+6                             0x0036
+7                             0x0037
+8                             0x0038
+9                             0x0039
+Colon                         0x003a
+Semicolon                     0x003b
+Less                          0x003c
+Equal                         0x003d
+Greater                       0x003e
+Question                      0x003f
+At                            0x0040
+A                             0x0041
+B                             0x0042
+C                             0x0043
+D                             0x0044
+E                             0x0045
+F                             0x0046
+G                             0x0047
+H                             0x0048
+I                             0x0049
+J                             0x004a
+K                             0x004b
+L                             0x004c
+M                             0x004d
+N                             0x004e
+O                             0x004f
+P                             0x0050
+Q                             0x0051
+R                             0x0052
+S                             0x0053
+T                             0x0054
+U                             0x0055
+V                             0x0056
+W                             0x0057
+X                             0x0058
+Y                             0x0059
+Z                             0x005a
+BracketLeft                   0x005b
+Backslash                     0x005c
+BracketRight                  0x005d
+Circumflex                    0x005e
+Underscore                    0x005f
+Grave                         0x0060
+BraceLeft                     0x007b
+Bar                           0x007c
+BraceRight                    0x007d
+Tilde                         0x007e
diff --git a/robot.cabal b/robot.cabal
new file mode 100644
--- /dev/null
+++ b/robot.cabal
@@ -0,0 +1,39 @@
+Name:                robot
+Version:             1.0
+Synopsis:            Simulate keyboard and mouse events
+Homepage:            https://github.com/lfairy/robot
+License:             OtherLicense
+License-File:        LICENSE
+Author:              Chris Wong
+Maintainer:          lambda.fairy@gmail.com
+Category:            Graphics
+Build-Type:          Simple
+Extra-Source-Files:  README.markdown, examples/*.hs, generate_keys.py, keys.txt
+Cabal-Version:       >= 1.6
+
+Description:
+    Generate native keyboard and mouse events. Using this library,
+    applications will respond as if the user performed the action
+    themselves.
+
+Library
+    Exposed-Modules:
+        Test.Robot
+        Test.Robot.Internal
+        Test.Robot.Internal.XTest
+        Test.Robot.Types
+
+    Other-Modules:
+        Test.Robot.Types.Core
+        Test.Robot.Types.Keys
+
+    Build-Depends:    base == 4.*
+                    , containers
+                    , transformers
+                    , xhb
+
+    GHC-Options: -Wall
+
+Source-Repository head
+    Type: git
+    Location: https://github.com/lfairy/robot.git
