diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,55 @@
-The MIT License (MIT)
+# Blue Oak Model License
 
-Copyright (c) 2018 Torsten Schmits
+Version 1.0.0
 
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
+## Purpose
 
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+This license gives everyone as much permission to work with
+this software as possible, while protecting contributors
+from liability.
 
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+## Acceptance
+
+In order to receive this license, you must agree to its
+rules.  The rules of this license are both obligations
+under that agreement and conditions to your license.
+You must not do anything with this software that triggers
+a rule that you cannot or will not follow.
+
+## Copyright
+
+Each contributor licenses you to do everything with this
+software that would otherwise infringe that contributor's
+copyright in it.
+
+## Notices
+
+You must ensure that everyone who gets a copy of
+any part of this software from you, with or without
+changes, also gets the text of this license or a link to
+<https://blueoakcouncil.org/license/1.0.0>.
+
+## Excuse
+
+If anyone notifies you in writing that you have not
+complied with [Notices](#notices), you can keep your
+license by taking all practical steps to comply within 30
+days after the notice.  If you do not do so, your license
+ends immediately.
+
+## Patent
+
+Each contributor licenses you to do everything with this
+software that would otherwise infringe any patent claims
+they can license or become able to license.
+
+## Reliability
+
+No contributor can revoke this license.
+
+## No Liability
+
+***As far as the law allows, this software comes as is,
+without any warranty or condition, and no contributor
+will be liable to anyone for any damages related to this
+software or this license, under any kind of legal claim.***
diff --git a/lib/Prelude.hs b/lib/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/lib/Prelude.hs
@@ -0,0 +1,5 @@
+module Prelude (
+  module Ribosome.Prelude
+) where
+
+import Ribosome.Prelude
diff --git a/lib/Ribosome/Api/Atomic.hs b/lib/Ribosome/Api/Atomic.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Api/Atomic.hs
@@ -0,0 +1,60 @@
+module Ribosome.Api.Atomic where
+
+import Data.MessagePack (Object(ObjectNil, ObjectArray, ObjectString))
+import Neovim.Plugin.Classes (FunctionName(F))
+import qualified Ribosome.Nvim.Api.RpcCall as RpcError (RpcError(Atomic))
+
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Msgpack.Decode (MsgpackDecode(..), fromMsgpack')
+import Ribosome.Msgpack.Encode (MsgpackEncode(toMsgpack))
+import Ribosome.Msgpack.Error (DecodeError)
+import qualified Ribosome.Msgpack.Util as Util (illegalType)
+import Ribosome.Nvim.Api.IO (nvimCallAtomic)
+import Ribosome.Nvim.Api.RpcCall (RpcCall(RpcCall))
+
+data AtomicStatus =
+  Failure Text
+  |
+  Success
+  deriving (Eq, Show)
+
+instance MsgpackDecode AtomicStatus where
+  fromMsgpack ObjectNil =
+    Right Success
+  fromMsgpack (ObjectArray [_, _, ObjectString msg]) =
+    Right (Failure (decodeUtf8 msg))
+  fromMsgpack o =
+    Util.illegalType "AtomicStatus" o
+
+-- |Bundle a list of 'RpcCall's into a single call to nvim_call_atomic.
+-- The result is checked for an error message, and if it is present, the call will fail.
+atomic ::
+  MonadDeepError e DecodeError m =>
+  NvimE e m =>
+  [RpcCall] ->
+  m [Object]
+atomic calls = do
+  (results, statusObject) <- unpack =<< nvimCallAtomic (call <$> calls)
+  status <- fromMsgpack' statusObject
+  check results status
+  where
+    call (RpcCall (F name) args) =
+      toMsgpack [toMsgpack name, toMsgpack args]
+    unpack [ObjectArray results, status] =
+      return (results, status)
+    unpack o =
+      throwHoist (RpcError.Atomic ("unexpected result structure: " <> show o))
+    check _ (Failure err) =
+      throwHoist (RpcError.Atomic err)
+    check results Success =
+      return results
+
+-- |Bundle calls into one and decode all results to the same type.
+atomicAs ::
+  MonadDeepError e DecodeError m =>
+  MsgpackDecode a =>
+  NvimE e m =>
+  [RpcCall] ->
+  m [a]
+atomicAs =
+  traverse fromMsgpack' <=< atomic
diff --git a/lib/Ribosome/Api/Autocmd.hs b/lib/Ribosome/Api/Autocmd.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Api/Autocmd.hs
@@ -0,0 +1,11 @@
+module Ribosome.Api.Autocmd where
+
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Nvim.Api.IO (vimCommand)
+
+doautocmd ::
+  NvimE e m =>
+  Text ->
+  m ()
+doautocmd name =
+  vimCommand $ "doautocmd " <> name
diff --git a/lib/Ribosome/Api/Buffer.hs b/lib/Ribosome/Api/Buffer.hs
--- a/lib/Ribosome/Api/Buffer.hs
+++ b/lib/Ribosome/Api/Buffer.hs
@@ -1,53 +1,104 @@
-module Ribosome.Api.Buffer(
-  edit,
-  buflisted,
-  setBufferContent,
-  bufferContent,
-  currentBufferContent,
-  setCurrentBufferContent,
-) where
+module Ribosome.Api.Buffer where
 
-import Neovim (
-  Neovim,
-  Buffer,
-  NvimObject(..),
-  Object,
-  toObject,
-  vim_command',
-  vim_call_function',
-  buffer_get_number',
-  buffer_get_lines',
-  buffer_set_lines',
-  vim_get_current_buffer',
+import Control.Monad (when)
+import Data.Bifunctor (second)
+import Data.MessagePack (Object)
+import System.FilePath ((</>))
+
+import Ribosome.Api.Atomic (atomicAs)
+import Ribosome.Api.Path (nvimCwd)
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Msgpack.Encode (toMsgpack)
+import Ribosome.Msgpack.Error (DecodeError)
+import Ribosome.Nvim.Api.Data (Buffer, bufferGetName)
+import Ribosome.Nvim.Api.IO (
+  bufferGetLines,
+  bufferGetNumber,
+  bufferIsValid,
+  bufferSetLines,
+  vimCallFunction,
+  vimCommand,
+  vimGetBuffers,
+  vimGetCurrentBuffer,
   )
-import Ribosome.Control.Ribo (Ribo)
+import Ribosome.Nvim.Api.RpcCall (syncRpcCall)
 
-edit :: FilePath -> Ribo e ()
-edit path = vim_command' $ "silent! edit " ++ path
+edit :: NvimE e m => FilePath -> m ()
+edit path = vimCommand $ "silent! edit " <> toText path
 
-nvimCallBool :: String -> [Object] -> Neovim e Bool
-nvimCallBool fun args =
-  vim_call_function' fun args >>= fromObject'
+nvimCallBool :: NvimE e m => Text -> [Object] -> m Bool
+nvimCallBool =
+  vimCallFunction
 
-buflisted :: Buffer -> Neovim e Bool
+buflisted :: NvimE e m => Buffer -> m Bool
 buflisted buf = do
-  num <- buffer_get_number' buf
-  nvimCallBool "buflisted" [toObject num]
+  num <- bufferGetNumber buf
+  nvimCallBool "buflisted" [toMsgpack num]
 
-bufferContent :: Buffer -> Neovim e [String]
+bufferContent :: NvimE e m => Buffer -> m [Text]
 bufferContent buffer =
-  buffer_get_lines' buffer 0 (-1) False
+  bufferGetLines buffer 0 (-1) False
 
-currentBufferContent :: Neovim e [String]
-currentBufferContent = do
-  buffer <- vim_get_current_buffer'
-  bufferContent buffer
+currentBufferContent :: NvimE e m => m [Text]
+currentBufferContent =
+  bufferContent =<< vimGetCurrentBuffer
 
-setBufferContent :: Buffer -> [String] -> Neovim e ()
+setBufferContent :: NvimE e m => Buffer -> [Text] -> m ()
 setBufferContent buffer =
-  buffer_set_lines' buffer 0 (-1) False
+  bufferSetLines buffer 0 (-1) False
 
-setCurrentBufferContent :: [String] -> Neovim e ()
+setCurrentBufferContent ::
+  NvimE e m =>
+  [Text] ->
+  m ()
 setCurrentBufferContent content = do
-  buffer <- vim_get_current_buffer'
+  buffer <- vimGetCurrentBuffer
   setBufferContent buffer content
+
+closeBuffer ::
+  NvimE e m =>
+  Buffer ->
+  m ()
+closeBuffer buffer = do
+  valid <- bufferIsValid buffer
+  when valid $ do
+    number <- bufferGetNumber buffer
+    vimCommand ("silent! bdelete! " <> show number)
+
+wipeBuffer ::
+  NvimE e m =>
+  Buffer ->
+  m ()
+wipeBuffer buffer = do
+  valid <- bufferIsValid buffer
+  when valid $ do
+    number <- bufferGetNumber buffer
+    vimCommand ("silent! bwipeout! " <> show number)
+
+buffersAndNames ::
+  MonadIO m =>
+  MonadDeepError e DecodeError m =>
+  NvimE e m =>
+  m [(Buffer, Text)]
+buffersAndNames = do
+  buffers <- vimGetBuffers
+  names <- atomicAs (syncRpcCall . bufferGetName <$> buffers)
+  return (zip buffers names)
+
+bufferForFile ::
+  MonadIO m =>
+  MonadDeepError e DecodeError m =>
+  NvimE e m =>
+  Text ->
+  m (Maybe Buffer)
+bufferForFile target = do
+  cwd <- nvimCwd
+  fmap fst . find sameBuffer . fmap (second (toText . absolute cwd . toString)) <$> buffersAndNames
+  where
+    sameBuffer (_, name) = name == target
+    absolute dir ('.' : '/' : rest) =
+      dir </> rest
+    absolute _ p@('/' : _) =
+      p
+    absolute dir path =
+      dir </> path
diff --git a/lib/Ribosome/Api/Echo.hs b/lib/Ribosome/Api/Echo.hs
--- a/lib/Ribosome/Api/Echo.hs
+++ b/lib/Ribosome/Api/Echo.hs
@@ -1,20 +1,46 @@
-module Ribosome.Api.Echo(
-  echom,
-  echomS,
-) where
+module Ribosome.Api.Echo where
 
-import Neovim (vim_command')
-import Ribosome.Control.Ribo (Ribo)
-import qualified Ribosome.Control.Ribo as Ribo (name)
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE, pluginName)
+import Ribosome.Data.Text (escapeQuotes)
+import Ribosome.Nvim.Api.IO (vimCommand)
 
-escapeQuotes :: Char -> String
-escapeQuotes '\'' = "''"
-escapeQuotes a = [a]
+echoWith :: NvimE e m => Text -> Text -> m ()
+echoWith cmd msg =
+  vimCommand $ cmd <> " '" <> escapeQuotes msg <> "'"
 
-echom :: String -> Ribo e ()
-echom msg = do
-  name <- Ribo.name
-  vim_command' $ "echom '" ++ name ++ ": " ++ concatMap escapeQuotes msg ++ "'"
+echoWithName :: MonadRibo m => NvimE e m => Text -> Text -> m ()
+echoWithName cmd msg = do
+  name <- pluginName
+  echoWith cmd $ name <> ": " <> msg
 
-echomS :: Show a => a -> Ribo e ()
+echo' :: NvimE e m => Text -> m ()
+echo' =
+  echoWith "echo"
+
+echo :: MonadRibo m => NvimE e m => Text -> m ()
+echo =
+  echoWithName "echo"
+
+echom' :: NvimE e m => Text -> m ()
+echom' =
+  echoWith "echom"
+
+echom :: MonadRibo m => NvimE e m => Text -> m ()
+echom =
+  echoWithName "echom"
+
+echomS ::
+  MonadRibo m =>
+  NvimE e m =>
+  Show a =>
+  a ->
+  m ()
 echomS = echom . show
+
+echon :: MonadRibo m => NvimE e m => Text -> m ()
+echon =
+  echoWithName "echom"
+
+echohl :: MonadRibo m => NvimE e m => Text -> m ()
+echohl =
+  vimCommand . ("echohl " <>)
diff --git a/lib/Ribosome/Api/Exists.hs b/lib/Ribosome/Api/Exists.hs
--- a/lib/Ribosome/Api/Exists.hs
+++ b/lib/Ribosome/Api/Exists.hs
@@ -1,26 +1,21 @@
-module Ribosome.Api.Exists(
-  retry,
-  waitForFunction,
-  vimDoesExist,
-  function,
-) where
+module Ribosome.Api.Exists where
 
 import Control.Monad.IO.Class (MonadIO)
 import Data.Default (Default(def))
 import Data.Either (isRight)
-import Data.Text.Prettyprint.Doc ((<+>), viaShow, prettyList)
+import Data.Text.Prettyprint.Doc (viaShow, (<+>))
 import Neovim (
-  Neovim,
+  AnsiStyle,
+  Doc,
   NvimObject,
   Object(ObjectInt),
-  Doc,
-  AnsiStyle,
-  toObject,
-  fromObject,
-  vim_call_function',
   )
 
-import Ribosome.Data.Time (epochSeconds, sleep)
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Msgpack.Decode (MsgpackDecode, fromMsgpack)
+import Ribosome.Msgpack.Encode (toMsgpack)
+import Ribosome.Nvim.Api.IO (vimCallFunction)
+import Ribosome.System.Time (epochSeconds, sleep)
 
 data Retry =
   Retry Int Double
@@ -29,7 +24,12 @@
 instance Default Retry where
   def = Retry 3 0.1
 
-retry :: MonadIO f => f a -> (a -> f (Either c b)) -> Retry -> f (Either c b)
+retry ::
+  MonadIO f =>
+  f a ->
+  (a -> f (Either c b)) ->
+  Retry ->
+  f (Either c b)
 retry thunk check (Retry timeout interval) = do
   start <- epochSeconds
   step start
@@ -47,37 +47,78 @@
         step start
       else return $ Left e
 
-waitFor :: NvimObject b =>
-  Neovim e Object ->
-  (Object -> Neovim e (Either (Doc AnsiStyle) b)) ->
+waitFor ::
+  NvimE e m =>
+  MonadIO m =>
+  NvimObject b =>
+  m Object ->
+  (Object -> m (Either (Doc AnsiStyle) b)) ->
   Retry ->
-  Neovim e (Either (Doc AnsiStyle) b)
+  m (Either (Doc AnsiStyle) b)
 waitFor thunk check' =
   retry thunk check
   where
     check result =
-      case fromObject result of
+      case fromMsgpack result of
         Right a -> check' a
         Left e -> return $ Left e
 
 existsResult :: Object -> Either (Doc AnsiStyle) ()
 existsResult (ObjectInt 1) = Right ()
-existsResult a = Left $ prettyList "weird return type " <+> viaShow a
+existsResult a =
+  Left $ "weird return type " <+> viaShow a
 
-vimExists :: String -> Neovim e Object
+vimExists ::
+  NvimE e m =>
+  Text ->
+  m Object
 vimExists entity =
-  vim_call_function' "exists" [toObject entity]
+  vimCallFunction "exists" [toMsgpack entity]
 
-vimDoesExist :: String -> Neovim e Bool
+vimDoesExist ::
+  NvimE e m =>
+  Text ->
+  m Bool
 vimDoesExist entity =
   fmap (isRight . existsResult) (vimExists entity)
 
-function :: String -> Neovim e Bool
+function ::
+  NvimE e m =>
+  Text ->
+  m Bool
 function name =
-  vimDoesExist ("*" ++ name)
+  vimDoesExist ("*" <> name)
 
-waitForFunction :: String -> Retry -> Neovim e (Either (Doc AnsiStyle) ())
+waitForFunction ::
+  NvimE e m =>
+  MonadIO m =>
+  Text ->
+  Retry ->
+  m (Either (Doc AnsiStyle) ())
 waitForFunction name =
   waitFor thunk (return . existsResult)
   where
-    thunk = vimExists ("*" ++ name)
+    thunk = vimExists ("*" <> name)
+
+waitForFunctionResult ::
+  NvimE e m =>
+  MonadIO m =>
+  Eq a =>
+  Show a =>
+  MsgpackDecode a =>
+  Text ->
+  a ->
+  Retry ->
+  m (Either (Doc AnsiStyle) ())
+waitForFunctionResult name a retry' =
+  waitForFunction name retry' >>= \case
+    Right _ -> waitFor thunk (return . check . fromMsgpack) retry'
+    Left e -> return (Left e)
+  where
+    thunk = vimCallFunction name []
+    check (Right a') | a == a' =
+      Right ()
+    check (Right a') =
+      Left $ "results differ:" <+> show a <+> "/" <+> show a'
+    check (Left e) =
+      Left $ "weird return type: " <+> e
diff --git a/lib/Ribosome/Api/Function.hs b/lib/Ribosome/Api/Function.hs
--- a/lib/Ribosome/Api/Function.hs
+++ b/lib/Ribosome/Api/Function.hs
@@ -1,8 +1,18 @@
-module Ribosome.Api.Function(
-  callFunction,
-) where
+module Ribosome.Api.Function where
 
-import Neovim (Neovim, fromObject', NvimObject, Object, vim_call_function')
+import qualified Data.Text as Text (intercalate)
 
-callFunction :: NvimObject a => String -> [Object] -> Neovim e a
-callFunction name args = vim_call_function' name args >>= fromObject'
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Nvim.Api.IO (vimCommand)
+
+defineFunction ::
+  NvimE e m =>
+  Text ->
+  [Text] ->
+  [Text] ->
+  m ()
+defineFunction name params body =
+  vimCommand $ unlines $ sig : body ++ ["endfunction"]
+  where
+    sig =
+      "function! " <> name <> "(" <> Text.intercalate ", " params <> ")"
diff --git a/lib/Ribosome/Api/Input.hs b/lib/Ribosome/Api/Input.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Api/Input.hs
@@ -0,0 +1,17 @@
+module Ribosome.Api.Input where
+
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Nvim.Api.IO (vimInput)
+import Ribosome.System.Time (sleep)
+
+syntheticInput ::
+  MonadIO m =>
+  NvimE e m =>
+  Maybe Double ->
+  [Text] ->
+  m ()
+syntheticInput interval =
+  traverse_ send
+  where
+    send c =
+      traverse_ sleep interval *> vimInput c
diff --git a/lib/Ribosome/Api/Option.hs b/lib/Ribosome/Api/Option.hs
--- a/lib/Ribosome/Api/Option.hs
+++ b/lib/Ribosome/Api/Option.hs
@@ -1,30 +1,22 @@
-module Ribosome.Api.Option(
-  optionCat,
-  rtpCat,
-  optionList,
-  option,
-  optionString,
-) where
+module Ribosome.Api.Option where
 
-import Control.Monad ((>=>))
-import Data.List.Split (splitOn)
-import Neovim (Neovim, vim_get_option', fromObject', vim_set_option', toObject, NvimObject)
+import Data.Text (splitOn)
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Msgpack.Encode (toMsgpack)
+import Ribosome.Nvim.Api.IO (vimGetOption, vimSetOption)
 
-optionCat :: String -> String -> Neovim e ()
+optionCat :: NvimE e m => Text -> Text -> m ()
 optionCat name extra = do
-  current <- vim_get_option' name >>= fromObject'
-  vim_set_option' name $ toObject $ current ++ "," ++ extra
+  current <- vimGetOption name
+  vimSetOption name $ toMsgpack $ current <> "," <> extra
 
-rtpCat :: String -> Neovim e ()
+rtpCat :: NvimE e m => Text -> m ()
 rtpCat = optionCat "runtimepath"
 
-option :: NvimObject a => String -> Neovim e a
-option = vim_get_option' >=> fromObject'
-
-optionString :: String -> Neovim e String
-optionString = option
+optionString :: NvimE e m => Text -> m Text
+optionString = vimGetOption
 
-optionList :: String -> Neovim e [String]
+optionList :: NvimE e m => Text -> m [Text]
 optionList name = do
-  s <- option name
+  s <- vimGetOption name
   return $ splitOn "," s
diff --git a/lib/Ribosome/Api/Path.hs b/lib/Ribosome/Api/Path.hs
--- a/lib/Ribosome/Api/Path.hs
+++ b/lib/Ribosome/Api/Path.hs
@@ -1,8 +1,7 @@
-module Ribosome.Api.Path(
-  nvimCwd,
-) where
+module Ribosome.Api.Path where
 
-import Neovim (Neovim, vim_call_function', fromObject')
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Nvim.Api.IO (vimCallFunction)
 
-nvimCwd :: Neovim e FilePath
-nvimCwd = vim_call_function' "getcwd" [] >>= fromObject'
+nvimCwd :: NvimE e m => m FilePath
+nvimCwd = vimCallFunction "getcwd" []
diff --git a/lib/Ribosome/Api/Process.hs b/lib/Ribosome/Api/Process.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Api/Process.hs
@@ -0,0 +1,11 @@
+module Ribosome.Api.Process where
+
+import Control.Monad.DeepError (MonadDeepError)
+
+import Ribosome.Control.Monad.Ribo (Nvim)
+import Ribosome.Nvim.Api.IO (vimCallFunction)
+import Ribosome.Nvim.Api.RpcCall (RpcError)
+
+vimPid :: (MonadDeepError e RpcError m, Nvim m) => m Int
+vimPid =
+  vimCallFunction "getpid" []
diff --git a/lib/Ribosome/Api/Response.hs b/lib/Ribosome/Api/Response.hs
--- a/lib/Ribosome/Api/Response.hs
+++ b/lib/Ribosome/Api/Response.hs
@@ -6,20 +6,19 @@
 )
 where
 
-import Data.ByteString.UTF8
 import Neovim
 
 nvimFatal :: Either NeovimException a -> Neovim env a
 nvimFatal (Right a) = return a
 nvimFatal (Left e) = (liftIO . fail . show) e
 
-nvimResponseString :: Object -> Neovim env String
-nvimResponseString (ObjectString a) = return $ toString a
-nvimResponseString a = liftIO . fail $ "invalid nvim type for String: " ++ show a
+nvimResponseString :: Object -> Neovim env Text
+nvimResponseString (ObjectString a) = return $ decodeUtf8 a
+nvimResponseString a = liftIO . fail $ "invalid nvim type for Text: " <> show a
 
-nvimResponseStringArray :: Object -> Neovim env [String]
+nvimResponseStringArray :: Object -> Neovim env [Text]
 nvimResponseStringArray (ObjectArray a) = traverse nvimResponseString a
-nvimResponseStringArray a = liftIO . fail $ "invalid nvim type for Array: " ++ show a
+nvimResponseStringArray a = liftIO . fail $ "invalid nvim type for Array: " <> show a
 
 nvimValidateFatal :: (Object -> Neovim env a) -> Either NeovimException Object -> Neovim env a
 nvimValidateFatal validate response = do
diff --git a/lib/Ribosome/Api/Sleep.hs b/lib/Ribosome/Api/Sleep.hs
--- a/lib/Ribosome/Api/Sleep.hs
+++ b/lib/Ribosome/Api/Sleep.hs
@@ -1,12 +1,18 @@
-module Ribosome.Api.Sleep(
-  nvimSleep,
-  nvimMSleep,
-) where
+module Ribosome.Api.Sleep where
 
-import Neovim (Neovim, vim_command')
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Nvim.Api.IO (vimCommand)
 
-nvimSleep :: Int -> Neovim e ()
-nvimSleep interval = vim_command' $ "sleep " ++ show interval
+nvimSleep ::
+  NvimE e m =>
+  Int ->
+  m ()
+nvimSleep interval =
+  vimCommand $ "sleep " <> show interval
 
-nvimMSleep :: Int -> Neovim e ()
-nvimMSleep interval = vim_command' $ "sleep " ++ show interval ++ "m"
+nvimMSleep ::
+  NvimE e m =>
+  Int ->
+  m ()
+nvimMSleep interval =
+  vimCommand $ "sleep " <> show interval <> "m"
diff --git a/lib/Ribosome/Api/Syntax.hs b/lib/Ribosome/Api/Syntax.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Api/Syntax.hs
@@ -0,0 +1,89 @@
+module Ribosome.Api.Syntax where
+
+import Data.Functor.Syntax ((<$$>))
+import Data.Map (Map)
+import qualified Data.Map as Map (toList)
+import Data.MessagePack (Object)
+import Neovim.Plugin.Classes (FunctionName(F))
+
+import Ribosome.Api.Atomic (atomic)
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Data.Syntax (
+  HiLink(HiLink),
+  Highlight(Highlight),
+  Syntax(Syntax),
+  SyntaxItem(SyntaxItem),
+  SyntaxItemDetail(Keyword, Match, Region, Verbatim),
+  )
+import Ribosome.Msgpack.Encode (MsgpackEncode(toMsgpack))
+import Ribosome.Msgpack.Error (DecodeError)
+import Ribosome.Nvim.Api.Data (Window)
+import Ribosome.Nvim.Api.IO (nvimWinGetNumber)
+import Ribosome.Nvim.Api.RpcCall (RpcCall(RpcCall))
+
+joinEquals :: Map Text Text -> Text
+joinEquals =
+  unwords . (equals <$$> Map.toList)
+  where
+    equals (a, b) = a <> "=" <> b
+
+rpcCommand :: [Text] -> RpcCall
+rpcCommand cmd =
+  RpcCall (F "nvim_command") [toMsgpack $ unwords cmd]
+
+synPattern :: Text -> Text
+synPattern text =
+  "/" <> text <> "/"
+
+namedPattern :: Text -> Text -> Text -> Text
+namedPattern param text offset =
+  param <> "=" <> synPattern text <> offset
+
+syntaxItemDetailCmd :: SyntaxItemDetail -> [Text]
+syntaxItemDetailCmd (Keyword group' keyword keywords) =
+  ["syntax", "keyword", group', keyword, unwords keywords]
+syntaxItemDetailCmd (Match group' pat) =
+  ["syntax", "match", group', synPattern pat]
+syntaxItemDetailCmd (Region group' start end skip ms me) =
+  ["syntax", "region", group', namedPattern "start" start ms] <> foldMap skipArg skip <> [namedPattern "end" end me]
+  where
+    skipArg a = [namedPattern "skip" a ""]
+syntaxItemDetailCmd (Verbatim cmd) =
+  [cmd]
+
+syntaxItemCmd :: SyntaxItem -> [Text]
+syntaxItemCmd (SyntaxItem detail options params) =
+  syntaxItemDetailCmd detail <> [unwords options, joinEquals params]
+
+highlightCmd :: Highlight -> [Text]
+highlightCmd (Highlight group' values) =
+  ["highlight", group', joinEquals values]
+
+hilinkCmd :: HiLink -> [Text]
+hilinkCmd (HiLink group' target) =
+  ["highlight", "link", group', target]
+
+syntaxCmds :: Syntax -> [[Text]]
+syntaxCmds (Syntax items highlights hilinks) =
+  (syntaxItemCmd <$> items) <> (highlightCmd <$> highlights) <> (hilinkCmd <$> hilinks)
+
+executeSyntax ::
+  MonadDeepError e DecodeError m =>
+  NvimE e m =>
+  Syntax ->
+  m [Object]
+executeSyntax =
+  atomic . (rpcCommand <$$> syntaxCmds)
+
+executeWindowSyntax ::
+  MonadDeepError e DecodeError m =>
+  NvimE e m =>
+  Window ->
+  Syntax ->
+  m [Object]
+executeWindowSyntax win syntax = do
+  number <- nvimWinGetNumber win
+  atomic $ wrapCmd (show number <> "windo") <$> syntaxCmds syntax
+  where
+    wrapCmd wrap cmd =
+      rpcCommand (wrap : cmd)
diff --git a/lib/Ribosome/Api/Tabpage.hs b/lib/Ribosome/Api/Tabpage.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Api/Tabpage.hs
@@ -0,0 +1,17 @@
+module Ribosome.Api.Tabpage where
+
+import Control.Monad (when)
+
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Nvim.Api.Data (Tabpage)
+import Ribosome.Nvim.Api.IO (nvimTabpageGetNumber, tabpageIsValid, vimCommand)
+
+closeTabpage ::
+  NvimE e m =>
+  Tabpage ->
+  m ()
+closeTabpage tabpage = do
+  valid <- tabpageIsValid tabpage
+  when valid $ do
+    number <- nvimTabpageGetNumber tabpage
+    vimCommand ("silent! tabclose! " <> show number)
diff --git a/lib/Ribosome/Api/Variable.hs b/lib/Ribosome/Api/Variable.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Api/Variable.hs
@@ -0,0 +1,25 @@
+module Ribosome.Api.Variable where
+
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE, pluginName)
+import Ribosome.Msgpack.Encode (MsgpackEncode(toMsgpack))
+import Ribosome.Nvim.Api.IO (vimSetVar)
+
+setVar ::
+  NvimE e m =>
+  MsgpackEncode a =>
+  Text ->
+  a ->
+  m ()
+setVar name =
+  vimSetVar name . toMsgpack
+
+setPVar ::
+  MonadRibo m =>
+  NvimE e m =>
+  MsgpackEncode a =>
+  Text ->
+  a ->
+  m ()
+setPVar name a = do
+  pn <- pluginName
+  setVar (pn <> "_" <> name) a
diff --git a/lib/Ribosome/Api/Window.hs b/lib/Ribosome/Api/Window.hs
--- a/lib/Ribosome/Api/Window.hs
+++ b/lib/Ribosome/Api/Window.hs
@@ -1,3 +1,80 @@
-module Ribosome.Api.Window(
-) where
+module Ribosome.Api.Window where
 
+import Control.Monad (when)
+
+import Ribosome.Control.Monad.Ribo (NvimE)
+import Ribosome.Nvim.Api.Data (Window)
+import Ribosome.Nvim.Api.IO (
+  nvimGetCurrentWin,
+  nvimWinClose,
+  nvimWinGetCursor,
+  nvimWinSetCursor,
+  vimCommand,
+  windowIsValid,
+  )
+
+closeWindow ::
+  NvimE e m =>
+  Window ->
+  m ()
+closeWindow window = do
+  valid <- windowIsValid window
+  when valid $ nvimWinClose window True
+
+cursor ::
+  NvimE e m =>
+  Window ->
+  m (Int, Int)
+cursor window = do
+  (line, col) <- nvimWinGetCursor window
+  return (fromIntegral line - 1, fromIntegral col)
+
+currentCursor ::
+  NvimE e m =>
+  m (Int, Int)
+currentCursor =
+  cursor =<< nvimGetCurrentWin
+
+windowLine ::
+  NvimE e m =>
+  Window ->
+  m Int
+windowLine window =
+  fst <$> cursor window
+
+currentLine ::
+  NvimE e m =>
+  m Int
+currentLine =
+  windowLine =<< nvimGetCurrentWin
+
+setCursor ::
+  NvimE e m =>
+  Window ->
+  Int ->
+  Int ->
+  m ()
+setCursor window line col =
+  nvimWinSetCursor window (line + 1, col)
+
+setLine ::
+  NvimE e m =>
+  Window ->
+  Int ->
+  m ()
+setLine window line =
+  setCursor window line 0
+
+setCurrentLine ::
+  NvimE e m =>
+  Int ->
+  m ()
+setCurrentLine line = do
+  window <- nvimGetCurrentWin
+  setLine window line
+
+redraw ::
+  NvimE e m =>
+  m ()
+redraw =
+  vimCommand "silent! redraw!"
diff --git a/lib/Ribosome/Config/Setting.hs b/lib/Ribosome/Config/Setting.hs
--- a/lib/Ribosome/Config/Setting.hs
+++ b/lib/Ribosome/Config/Setting.hs
@@ -1,101 +1,80 @@
-module Ribosome.Config.Setting(
-  Setting (..),
-  setting,
-  settingE,
-  updateSetting,
-  settingVariableName,
-  settingOr,
-  settingMaybe,
-  settingR,
-  SettingError(..),
-  updateSettingR,
-) where
-
-import Data.Either (fromRight)
-import Data.Text.Prettyprint.Doc (Doc)
-import Data.Text.Prettyprint.Doc.Render.Terminal (AnsiStyle)
-import Neovim
-import System.Log (Priority(NOTICE))
-
-import Ribosome.Control.Monad.RiboE (RiboE, liftRibo, riboE, mapE)
-import Ribosome.Control.Ribo (Ribo)
-import qualified Ribosome.Control.Ribosome as R (name)
-import Ribosome.Data.ErrorReport (ErrorReport(..))
-import Ribosome.Error.Report (ReportError(..))
-import Ribosome.Msgpack.Decode (MsgpackDecode(fromMsgpack))
-import Ribosome.Msgpack.Encode (MsgpackEncode(toMsgpack))
+{-# LANGUAGE TemplateHaskell #-}
 
-data SettingError =
-  Other String String
-  |
-  Decode String (Doc AnsiStyle)
-  |
-  Unset String
-  deriving Show
+module Ribosome.Config.Setting where
 
-instance ReportError SettingError where
-  errorReport (Other name message) =
-    ErrorReport ("weird setting: " ++ name) ["failed to read setting `" ++ name ++ "`", message] NOTICE
-  errorReport (Decode name message) =
-    ErrorReport ("invalid setting: " ++ name) ["failed to decode setting `" ++ name ++ "`", show message] NOTICE
-  errorReport (Unset name) =
-    ErrorReport ("required setting unset: " ++ name) ["unset setting: `" ++ name ++ "`"] NOTICE
+import Control.Monad.DeepError (MonadDeepError(throwHoist), catchAt)
+import Control.Monad.IO.Class (MonadIO)
+import Control.Monad.Trans.Except (runExceptT)
+import Data.DeepPrisms (deepPrisms)
 
-data Setting a =
-  Setting {
-    name :: String,
-    prefix :: Bool,
-    fallback :: Maybe a
-  }
+import Ribosome.Control.Monad.Ribo (MonadRibo, Nvim, NvimE, pluginName)
+import Ribosome.Data.Setting (Setting(Setting))
+import Ribosome.Data.SettingError (SettingError)
+import qualified Ribosome.Data.SettingError as SettingError (SettingError(..))
+import Ribosome.Msgpack.Decode (MsgpackDecode)
+import Ribosome.Msgpack.Encode (MsgpackEncode(toMsgpack))
+import Ribosome.Nvim.Api.IO
+import Ribosome.Nvim.Api.RpcCall (RpcError)
+import qualified Ribosome.Nvim.Api.RpcCall as RpcError (RpcError(..))
 
-settingVariableName :: Setting a -> Ribo e String
-settingVariableName (Setting n False _) = return n
-settingVariableName (Setting n True _) = do
-  pluginName <- R.name <$> ask
-  return $ pluginName ++ "_" ++ n
+settingVariableName ::
+  (MonadRibo m) =>
+  Setting a ->
+  m Text
+settingVariableName (Setting settingName False _) =
+  return settingName
+settingVariableName (Setting settingName True _) = do
+  name <- pluginName
+  return $ name <> "_" <> settingName
 
-settingE :: NvimObject a => Setting a -> Ribo e (Either String a)
-settingE s@(Setting _ _ fallback') = do
-  varName <- settingVariableName s
-  raw <- vim_get_var varName
-  case raw of
-    Right o -> fromObject' o
-    Left a -> return $ case fallback' of
-      Just fb -> Right fb
-      Nothing -> Left $ show a
+settingRaw :: (MonadRibo m, Nvim m, MsgpackDecode a, MonadDeepError e RpcError m) => Setting a -> m a
+settingRaw s =
+  vimGetVar =<< settingVariableName s
 
-setting :: NvimObject a => Setting a -> Ribo e a
-setting s = do
-  raw <- settingE s
-  either fail return raw
+setting ::
+  ∀ e m a.
+  NvimE e m =>
+  MonadRibo m =>
+  MonadDeepError e SettingError m =>
+  MsgpackDecode a =>
+  Setting a ->
+  m a
+setting s@(Setting n _ fallback') =
+  catchAt handleError $ settingRaw s
+  where
+    handleError (RpcError.Nvim _ _) =
+      case fallback' of
+        (Just fb) -> return fb
+        Nothing -> throwHoist $ SettingError.Unset n
+    handleError a =
+      throwHoist a
 
-settingOr :: NvimObject a => a -> Setting a -> Ribo e a
-settingOr a s = do
-  raw <- settingE s
-  return $ fromRight a raw
+data SettingOrError =
+  Sett SettingError
+  |
+  Rpc RpcError
 
-settingMaybe :: NvimObject a => Setting a -> Ribo e (Maybe a)
-settingMaybe s = do
-  raw <- settingE s
-  return $ either (const Nothing) Just raw
+deepPrisms ''SettingOrError
 
-updateSetting :: NvimObject a => Setting a -> a -> Ribo e ()
-updateSetting s a = do
-  varName <- settingVariableName s
-  _ <- vim_set_var' varName (toObject a)
-  return ()
+settingOr ::
+  (MonadIO m, Nvim m, MonadRibo m, MsgpackDecode a) =>
+  a ->
+  Setting a ->
+  m a
+settingOr a =
+  (fromRight a <$>) . runExceptT . setting @SettingOrError
 
-settingR :: MsgpackDecode a => Setting a -> RiboE s SettingError a
-settingR s@(Setting n _ fallback') = do
-  varName <- liftRibo $ settingVariableName s
-  raw <- liftRibo $ vim_get_var varName
-  case raw of
-    Right o -> mapE (Decode n) $ riboE $ pure $ fromMsgpack o
-    Left _ -> riboE $ return $ case fallback' of
-      Just fb -> Right fb
-      Nothing -> Left $ Unset n
+settingMaybe ::
+  (MonadIO m, Nvim m, MonadRibo m, MsgpackDecode a) =>
+  Setting a ->
+  m (Maybe a)
+settingMaybe =
+  (rightToMaybe <$>) . runExceptT . setting @SettingOrError
 
-updateSettingR :: MsgpackEncode a => Setting a -> a -> Ribo e ()
-updateSettingR s a = do
-  varName <- settingVariableName s
-  void $ vim_set_var' varName (toMsgpack a)
+updateSetting ::
+  (MonadRibo m, MonadIO m, Nvim m, MonadDeepError e RpcError m, MsgpackEncode a) =>
+  Setting a ->
+  a ->
+  m ()
+updateSetting s a =
+  (`vimSetVar` toMsgpack a) =<< settingVariableName s
diff --git a/lib/Ribosome/Config/Settings.hs b/lib/Ribosome/Config/Settings.hs
--- a/lib/Ribosome/Config/Settings.hs
+++ b/lib/Ribosome/Config/Settings.hs
@@ -1,8 +1,9 @@
-module Ribosome.Config.Settings(
-  persistenceDir,
-) where
+module Ribosome.Config.Settings where
 
-import Ribosome.Config.Setting (Setting(Setting))
+import Ribosome.Data.Setting (Setting(Setting))
 
 persistenceDir :: Setting FilePath
 persistenceDir = Setting "ribosome_persistence_dir" False Nothing
+
+tmuxSocket :: Setting FilePath
+tmuxSocket = Setting "tmux_socket" True Nothing
diff --git a/lib/Ribosome/Control/Concurrent/Wait.hs b/lib/Ribosome/Control/Concurrent/Wait.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Control/Concurrent/Wait.hs
@@ -0,0 +1,103 @@
+module Ribosome.Control.Concurrent.Wait where
+
+import Control.Exception.Lifted (Exception, SomeException(..), try)
+import Control.Monad.IO.Class (MonadIO)
+import Control.Monad.Trans.Control (MonadBaseControl)
+import Data.Default (Default(def))
+import Data.Functor ((<&>))
+import qualified Text.Show
+
+import Ribosome.System.Time (sleep)
+
+-- |Specifies the maximum number of retries and the interval in seconds for 'waitIO'.
+data Retry =
+  Retry Int Double
+  deriving Show
+
+instance Default Retry where
+  def = Retry 30 0.1
+
+-- |Error description for 'waitIO'
+data WaitError =
+  NotStarted
+  |
+  ConditionUnmet Text
+  |
+  ∀ e. Exception e => Thrown e
+
+instance Text.Show.Show WaitError where
+  show NotStarted =
+    "NotStarted"
+  show (ConditionUnmet reason) =
+    toString $ "ConditionUnmet(" <> reason <> ")"
+  show (Thrown _) =
+    "Thrown"
+
+-- |Execute an IO thunk repeatedly until either the supplied condition produces a 'Right' or the maximum number of
+-- retries specified in the `Retry` parameter has been reached.
+-- Returns the value produced by the condition.
+waitIO ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  Retry ->
+  m a ->
+  (a -> m (Either Text b)) ->
+  m (Either WaitError b)
+waitIO (Retry maxRetry interval) thunk cond =
+  wait maxRetry (Left NotStarted)
+  where
+    wait 0 reason = return reason
+    wait count _ = do
+      ea <- try thunk
+      result <- try $ check ea
+      case result of
+        Right (Right a) ->
+          return $ Right a
+        Right (Left reason) ->
+          recurse reason count
+        Left (SomeException e) ->
+          recurse (Thrown e) count
+    recurse reason count = do
+      sleep interval
+      wait (count - 1) (Left reason)
+    check (Right a) =
+      cond a <&> \case
+        Right b -> Right b
+        Left reason -> Left (ConditionUnmet reason)
+    check (Left (SomeException e)) =
+      return $ Left (Thrown e)
+
+-- |Calls 'waitIO' with the default configuration of 30 retries every 100ms.
+waitIODef ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  m a ->
+  (a -> m (Either Text b)) ->
+  m (Either WaitError b)
+waitIODef =
+  waitIO def
+
+-- |Same as 'waitIO', but the condition returns 'Bool' and the result is the result of the thunk.
+waitIOPred ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  Retry ->
+  m a ->
+  (a -> m Bool) ->
+  m (Either WaitError a)
+waitIOPred retry thunk pred' =
+  waitIO retry thunk cond
+  where
+    cond a = pred' a <&> \case
+      True -> Right a
+      False -> Left "predicate returned False"
+
+-- |Calls 'waitIOPred' with the default configuration of 30 retries every 100ms.
+waitIOPredDef ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  m a ->
+  (a -> m Bool) ->
+  m (Either WaitError a)
+waitIOPredDef =
+  waitIOPred def
diff --git a/lib/Ribosome/Control/Exception.hs b/lib/Ribosome/Control/Exception.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Control/Exception.hs
@@ -0,0 +1,18 @@
+module Ribosome.Control.Exception where
+
+import Control.Exception.Lifted (IOException, try)
+import Control.Monad.Trans.Control (MonadBaseControl)
+
+tryIO ::
+  MonadBaseControl IO m =>
+  m a ->
+  m (Either IOException a)
+tryIO =
+  try
+
+tryAny ::
+  MonadBaseControl IO m =>
+  m a ->
+  m (Either SomeException a)
+tryAny =
+  try
diff --git a/lib/Ribosome/Control/Lock.hs b/lib/Ribosome/Control/Lock.hs
--- a/lib/Ribosome/Control/Lock.hs
+++ b/lib/Ribosome/Control/Lock.hs
@@ -1,32 +1,29 @@
-module Ribosome.Control.Lock(
-  getOrCreateLock,
-  lockOrSkip,
-) where
+module Ribosome.Control.Lock where
 
-import qualified Control.Lens as Lens (view, over, at)
-import qualified Data.Map.Strict as Map (insert)
-import Neovim (ask)
-import UnliftIO (finally)
-import UnliftIO.STM (TMVar, atomically, newTMVarIO, tryTakeTMVar, tryPutTMVar, modifyTVar)
+import Control.Exception.Lifted (finally)
+import qualified Control.Lens as Lens (at, view)
+import Control.Monad.IO.Class (MonadIO)
+import Control.Monad.Trans.Control (MonadBaseControl)
+import qualified Data.Map as Map (insert)
+import UnliftIO.STM (TMVar, newTMVarIO, tryPutTMVar, tryTakeTMVar)
 
-import Ribosome.Control.Ribo (Ribo, riboInternal)
-import Ribosome.Control.Ribosome (Ribosome(Ribosome), Locks)
-import qualified Ribosome.Control.Ribosome as Ribosome (_locks, locks)
-import qualified Ribosome.Log as Log (debugR)
+import Ribosome.Control.Monad.Ribo (MonadRibo, pluginInternalL, pluginInternalModifyL)
+import Ribosome.Control.Ribosome (Locks)
+import qualified Ribosome.Control.Ribosome as Ribosome (locks)
+import qualified Ribosome.Log as Log (debug)
 
-getLocks :: Ribo e Locks
+getLocks :: (MonadRibo m, MonadIO m) => m Locks
 getLocks =
-  Ribosome.locks <$> riboInternal
+  pluginInternalL Ribosome.locks
 
-inspectLocks :: (Locks -> a) -> Ribo e a
-inspectLocks f = fmap f getLocks
+inspectLocks :: (MonadRibo m, MonadIO m) => (Locks -> a) -> m a
+inspectLocks = (<$> getLocks)
 
-modifyLocks :: (Locks -> Locks) -> Ribo e ()
-modifyLocks f = do
-  Ribosome _ intTv _ <- ask
-  atomically $ modifyTVar intTv $ Lens.over Ribosome._locks f
+modifyLocks :: MonadRibo m => (Locks -> Locks) -> m ()
+modifyLocks =
+  pluginInternalModifyL Ribosome.locks
 
-getOrCreateLock :: String -> Ribo e (TMVar ())
+getOrCreateLock :: (MonadRibo m, MonadIO m) => Text -> m (TMVar ())
 getOrCreateLock key = do
   currentLock <- inspectLocks $ Lens.view $ Lens.at key
   case currentLock of
@@ -36,13 +33,19 @@
       modifyLocks $ Map.insert key tv
       getOrCreateLock key
 
-lockOrSkip :: String -> Ribo e () -> Ribo e ()
+lockOrSkip ::
+  MonadRibo m =>
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  Text ->
+  m () ->
+  m ()
 lockOrSkip key thunk = do
   currentLock <- getOrCreateLock key
   currentState <- atomically $ tryTakeTMVar currentLock
   case currentState of
     Just _ -> do
-      Log.debugR $ "locking MVar `" ++ key ++ "`"
+      Log.debug $ "locking MVar `" <> key <> "`"
       finally thunk $ atomically $ tryPutTMVar currentLock ()
-      Log.debugR $ "unlocking MVar `" ++ key ++ "`"
+      Log.debug $ "unlocking MVar `" <> key <> "`"
     Nothing -> return ()
diff --git a/lib/Ribosome/Control/Monad/Error.hs b/lib/Ribosome/Control/Monad/Error.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Control/Monad/Error.hs
@@ -0,0 +1,12 @@
+module Ribosome.Control.Monad.Error(
+  recoverAs,
+  recoveryFor,
+) where
+
+import Control.Monad.Error.Class (MonadError(catchError))
+
+recoveryFor :: MonadError e m => m a -> m a -> m a
+recoveryFor = flip catchError . const
+
+recoverAs :: MonadError e m => m a -> m a -> m a
+recoverAs = flip recoveryFor
diff --git a/lib/Ribosome/Control/Monad/Ribo.hs b/lib/Ribosome/Control/Monad/Ribo.hs
--- a/lib/Ribosome/Control/Monad/Ribo.hs
+++ b/lib/Ribosome/Control/Monad/Ribo.hs
@@ -1,80 +1,230 @@
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
 {-# LANGUAGE UndecidableInstances #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE FunctionalDependencies #-}
 
-module Ribosome.Control.Monad.Ribo(
-  Ribo,
-  RiboT(..),
-  MonadRibo(..),
-  MonadRiboError(..),
-  unsafeToNeovim,
-) where
+module Ribosome.Control.Monad.Ribo where
 
-import Control.Concurrent.STM.TVar (swapTVar)
+import Control.Concurrent.STM.TMVar (putTMVar, readTMVar, takeTMVar)
+import Control.Exception.Lifted (onException)
+import Control.Lens (Lens')
+import qualified Control.Lens as Lens (mapMOf, over, view)
+import Control.Monad.Base (MonadBase(..))
+import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
 import Control.Monad.Error.Class (MonadError(..))
-import Control.Monad.State (MonadState(..))
-import Control.Monad.Trans.Except (ExceptT(ExceptT), runExceptT, mapExceptT)
-import qualified Control.Monad.Trans.Except as Except (catchE)
-import Data.Bifunctor (Bifunctor(..))
-import Data.Either.Combinators (mapLeft)
-import Data.Functor (void)
-import UnliftIO.Exception (throwString)
-import UnliftIO.STM (TVar, atomically, readTVarIO)
-import Neovim (ask)
-import Neovim.Context.Internal (Neovim(Neovim))
-import Ribosome.Control.Ribosome (Ribosome)
-import qualified Ribosome.Control.Ribosome as Ribosome (env)
+import Control.Monad.Fail (MonadFail)
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Reader.Class (MonadReader, asks)
+import Control.Monad.Trans.Control (MonadBaseControl(..))
+import Control.Monad.Trans.Except (ExceptT(ExceptT), runExceptT)
+import Control.Monad.Trans.Free (FreeT)
+import Control.Monad.Trans.Reader (ReaderT(ReaderT), runReaderT)
+import Control.Monad.Trans.Resource (runResourceT)
+import qualified Control.Monad.Trans.State.Strict as StateT (gets, modify)
+import Data.DeepLenses (DeepLenses(deepLens))
+import Data.DeepPrisms (DeepPrisms)
+import Neovim.Context.Internal (Neovim(..))
+import Ribosome.Plugin.RpcHandler (RpcHandler(..))
+import UnliftIO.STM (TMVar)
 
-type Ribo e = Neovim (Ribosome e)
+import Ribosome.Control.Ribosome (Ribosome, RibosomeInternal, RibosomeState)
+import qualified Ribosome.Control.Ribosome as Ribosome (_errors, errors, name, state)
+import qualified Ribosome.Control.Ribosome as RibosomeState (internal, public)
+import Ribosome.Control.StrictRibosome (StrictRibosome)
+import qualified Ribosome.Control.StrictRibosome as StrictRibosome (name, state)
+import Ribosome.Data.Errors (Errors)
+import Ribosome.Nvim.Api.RpcCall (Rpc, RpcError)
+import qualified Ribosome.Nvim.Api.RpcCall as Rpc (Rpc(..))
+import Ribosome.Orphans ()
 
-newtype RiboT s e a =
-  RiboT { unRiboT :: ExceptT e (Neovim (Ribosome (TVar s))) a }
-  deriving (Functor, Applicative, Monad)
+type RNeovim s = Neovim (Ribosome s)
 
-instance Bifunctor (RiboT s) where
-  first f (RiboT r) =
-    RiboT $ mapExceptT (fmap $ mapLeft f) r
+instance MonadBase IO (Neovim e) where
+  liftBase = liftIO
 
-  second = fmap
+instance MonadBaseControl IO (Neovim e) where
+  type StM (Neovim e) a = a
+  liftBaseWith f =
+    Neovim (lift $ ReaderT $ \r -> f (peel r))
+    where
+      peel r ma =
+        runReaderT (runResourceT (unNeovim ma)) r
+  restoreM = return
 
-class MonadRibo s e m | m -> s, m -> e where
-  nvim :: Neovim (Ribosome (TVar s)) a -> m a
-  asNeovim :: m a -> Neovim (Ribosome (TVar s)) (Either e a)
+newtype Ribo s e a =
+  Ribo { unRibo :: ExceptT e (RNeovim s) a }
+  deriving (Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask, MonadFail, MonadBase IO)
 
-class Monad (t e) => MonadRiboError e t where
-  liftEither :: Either e a -> t e a
-  mapE :: (e -> e') -> t e a -> t e' a
-  catchE :: (e -> t e' a) -> t e a -> t e' a
+deriving instance MonadError e (Ribo s e)
 
-instance MonadRibo s e (RiboT s e) where
-  nvim = RiboT . ExceptT . fmap Right
-  asNeovim = runExceptT . unRiboT
+modifyTMVar ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  (a -> a) ->
+  TMVar a ->
+  m a
+modifyTMVar f tmvar = do
+  a <- f <$> atomically (takeTMVar tmvar)
+  atomically $ putTMVar tmvar a
+  return a
 
-instance MonadRiboError e (RiboT s) where
-  liftEither = RiboT . ExceptT . return
-  mapE f = RiboT . mapExceptT (fmap $ mapLeft f) . unRiboT
-  catchE f = RiboT . flip Except.catchE (unRiboT . f) . unRiboT
+safeModifyTMVarM ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  (a -> m a) ->
+  TMVar a ->
+  m a
+safeModifyTMVarM f tmvar =
+  process =<< atomically (takeTMVar tmvar)
+  where
+    process a =
+      onException (restore =<< f a) (restore a)
+    restore a =
+      a <$ (atomically . putTMVar tmvar $ a)
 
-instance MonadError e (RiboT s e) where
-  throwError = liftEither . Left
-  catchError = flip catchE
+deriving instance MonadReader (Ribosome s) (Ribo s e)
 
-stateTVar :: (Functor m, MonadRibo s e m) => m (TVar s)
-stateTVar =
-  Ribosome.env <$> nvim ask
+riboStateVar ::
+  MonadReader (Ribosome s) m =>
+  m (TMVar (RibosomeState s))
+riboStateVar =
+  asks (Lens.view Ribosome.state)
 
-instance MonadState s (RiboT s e) where
-  get = do
-    t <- stateTVar
-    nvim $ readTVarIO t
-  put newState = do
-    t <- stateTVar
-    void $ nvim $ atomically $ swapTVar t newState
+public ::
+  DeepLenses s s' =>
+  Lens' (RibosomeState s) s'
+public =
+  RibosomeState.public . deepLens
 
-unsafeToNeovim :: (MonadRibo s e m, Show e) => m a -> Neovim (Ribosome (TVar s)) a
-unsafeToNeovim ra = do
-  r <- asNeovim ra
-  either (throwString . show) return r
+instance DeepLenses s s' => MonadDeepState s s' (Ribo s e) where
+  get =
+    Lens.view public <$> (atomically . readTMVar =<< riboStateVar)
+
+  modifyM' f =
+    Lens.view public <$> (safeModifyTMVarM trans =<< riboStateVar)
+    where
+      trans = Lens.mapMOf public f
+
+  put =
+    modify . const
+
+class Monad m => Nvim m where
+  call :: Rpc c a => c -> m (Either RpcError a)
+
+instance Nvim (Neovim e) where
+  call = Rpc.call
+
+instance (MonadTrans t, Monad (t m), Nvim m) => Nvim (t m) where
+  call = lift . call
+
+instance Nvim (Ribo s e) where
+  call = Ribo . call
+
+class (Nvim m, MonadDeepError e RpcError m) => NvimE e m where
+
+instance DeepPrisms e RpcError => NvimE e (Ribo s e) where
+
+instance (DeepPrisms e RpcError, Nvim m, Monad m) => NvimE e (ExceptT e m) where
+
+instance (Functor f, MonadDeepError e RpcError m, Nvim m, Monad m) => NvimE e (FreeT f m) where
+
+instance MonadBaseControl IO (Ribo s e) where
+    type StM (Ribo s e) a = Either e a
+
+    liftBaseWith f =
+      Ribo $ liftBaseWith $ \ q -> f (q . unRibo)
+
+    restoreM =
+      Ribo . restoreM
+
+    {-# INLINABLE liftBaseWith #-}
+    {-# INLINABLE restoreM #-}
+
+instance RpcHandler e (Ribosome s) (Ribo s e) where
+  native = runRiboE
+
+acall :: (Monad m, Nvim m, Rpc c ()) => c -> m ()
+acall c = fromRight () <$> call c
+
+readTv :: Lens' (RibosomeState s) s' -> TMVar (RibosomeState s) -> IO s'
+readTv l t = Lens.view l <$> atomically (readTMVar t)
+
+runRibo :: Ribo s e a -> RNeovim s (Either e a)
+runRibo =
+  runExceptT . unRibo
+
+runRiboE :: Ribo s e a -> ExceptT e (RNeovim s) a
+runRiboE =
+  unRibo
+
+class MonadIO m => MonadRibo m where
+  pluginName :: m Text
+  pluginInternal :: m RibosomeInternal
+  pluginInternalModify :: (RibosomeInternal -> RibosomeInternal) -> m ()
+
+pluginInternals :: MonadRibo m => (RibosomeInternal -> a) -> m a
+pluginInternals = (<$> pluginInternal)
+
+pluginInternalL :: MonadRibo m => Lens' RibosomeInternal a -> m a
+pluginInternalL = pluginInternals . Lens.view
+
+pluginInternalPut' :: MonadRibo m => RibosomeInternal -> m ()
+pluginInternalPut' s =
+  pluginInternalModify (const s)
+
+pluginInternalModifyL :: MonadRibo m => Lens' RibosomeInternal a -> (a -> a) -> m ()
+pluginInternalModifyL l f =
+  pluginInternalModify $ Lens.over l f
+
+instance MonadRibo (RNeovim s) where
+  pluginName =
+    asks (Lens.view Ribosome.name)
+
+  pluginInternal =
+    Lens.view RibosomeState.internal <$$> atomically . readTMVar =<< asks (Lens.view Ribosome.state)
+
+  pluginInternalModify f =
+    void . modifyTMVar (Lens.over RibosomeState.internal f) =<< riboStateVar
+
+instance MonadRibo (Ribo s e) where
+  pluginName = Ribo pluginName
+  pluginInternal = Ribo pluginInternal
+  pluginInternalModify = Ribo . pluginInternalModify
+
+instance {-# OVERLAPPABLE #-} (MonadTrans t, MonadIO (t m), MonadRibo m) => MonadRibo (t m) where
+  pluginName = lift pluginName
+  pluginInternal = lift pluginInternal
+  pluginInternalModify = lift . pluginInternalModify
+
+instance {-# OVERLAPPING #-} MonadIO m => MonadRibo (StateT (StrictRibosome s) m) where
+  pluginName =
+    StateT.gets (Lens.view StrictRibosome.name)
+  pluginInternal =
+    StateT.gets (Lens.view $ StrictRibosome.state . RibosomeState.internal)
+  pluginInternalModify =
+    StateT.modify . Lens.over (StrictRibosome.state . RibosomeState.internal)
+
+getErrors :: MonadRibo m => m Errors
+getErrors =
+  pluginInternals Ribosome._errors
+
+inspectErrors :: MonadRibo m => (Errors -> a) -> m a
+inspectErrors = (<$> getErrors)
+
+modifyErrors :: MonadRibo m => (Errors -> Errors) -> m ()
+modifyErrors =
+  pluginInternalModifyL Ribosome.errors
+
+prepend :: ∀s' s m a. MonadDeepState s s' m => Lens' s' [a] -> a -> m ()
+prepend lens a =
+  modify $ Lens.over lens (a:)
+
+inspectHeadE ::
+  ∀ s' s e e' m a .
+  (MonadDeepState s s' m, MonadDeepError e e' m) =>
+  e' ->
+  Lens' s' [a] ->
+  m a
+inspectHeadE err lens = do
+  as <- gets $ Lens.view lens
+  case as of
+    (a : _) -> return a
+    _ -> throwHoist err
diff --git a/lib/Ribosome/Control/Monad/RiboE.hs b/lib/Ribosome/Control/Monad/RiboE.hs
deleted file mode 100644
--- a/lib/Ribosome/Control/Monad/RiboE.hs
+++ /dev/null
@@ -1,67 +0,0 @@
-{-# LANGUAGE MultiParamTypeClasses #-}
-
-module Ribosome.Control.Monad.RiboE(
-  Ribo,
-  RiboE(..),
-  riboE,
-  liftRibo,
-  runRiboE,
-  mapE,
-  anaE,
-  cataE,
-  runRiboReport,
-) where
-
-import Control.Monad (join)
-import Control.Monad.Error.Class (MonadError(..))
-import Control.Monad.IO.Class (MonadIO)
-import Control.Monad.Trans.Class
-import Control.Monad.Trans.Except (ExceptT(ExceptT), mapExceptT, runExceptT)
-import Data.Either.Combinators (mapLeft)
-import Neovim.Context.Internal (Neovim)
-import UnliftIO.STM (TVar)
-
-import Ribosome.Control.Ribosome (Ribosome)
-import Ribosome.Error.Report (ReportError, reportError)
-
-type Ribo e = Neovim (Ribosome (TVar e))
-
-newtype RiboE s e a =
-  RiboE { unRiboE :: ExceptT e (Ribo s) a }
-  deriving (Functor, Applicative, Monad, MonadIO)
-
-riboE :: (Ribo s) (Either e a) -> RiboE s e a
-riboE = RiboE . ExceptT
-
-liftRibo :: Ribo s a -> RiboE s e a
-liftRibo = RiboE . lift
-
-runRiboE :: RiboE s e a -> Ribo s (Either e a)
-runRiboE = runExceptT . unRiboE
-
-runRiboReport :: ReportError e => String -> RiboE s e () -> Ribo s ()
-runRiboReport componentName ma = do
-  result <- runRiboE ma
-  case result of
-    Right _ -> return ()
-    Left e -> reportError componentName e
-
-mapE :: (e -> e') -> RiboE s e a -> RiboE s e' a
-mapE f =
-  RiboE . trans . unRiboE
-  where
-    trans = mapExceptT (fmap $ mapLeft f)
-
-anaE :: (e -> e') -> RiboE s e (Either e' a) -> RiboE s e' a
-anaE f =
-  riboE . fmap (join . mapLeft f) . runRiboE
-
-cataE :: (e' -> e) -> RiboE s e (Either e' a) -> RiboE s e a
-cataE f =
-  riboE . fmap join . runRiboE . fmap (mapLeft f)
-
-instance MonadError e (RiboE s e) where
-  throwError =
-    RiboE . throwError
-  catchError ma f =
-    RiboE $ catchError (unRiboE ma) (unRiboE . f)
diff --git a/lib/Ribosome/Control/Monad/State.hs b/lib/Ribosome/Control/Monad/State.hs
deleted file mode 100644
--- a/lib/Ribosome/Control/Monad/State.hs
+++ /dev/null
@@ -1,91 +0,0 @@
-module Ribosome.Control.Monad.State(
-  riboStateLocalT,
-  riboStateT,
-  riboStateLocal,
-  riboState,
-  modifyL,
-  prepend,
-  riboStateLocalE,
-  riboStateE,
-  runRiboStateE,
-) where
-
-import Control.Lens (Lens')
-import qualified Control.Lens as Lens (over, view, set)
-import Control.Monad.State.Class (MonadState, modify)
-import Control.Monad.Trans.Class (MonadTrans(lift))
-import Control.Monad.Trans.Except (ExceptT)
-import Control.Monad.Trans.State (StateT, runStateT)
-import Ribosome.Control.Monad.RiboE (Ribo, RiboE(..), runRiboE)
-import qualified Ribosome.Control.Ribo as Ribo (inspect, modify)
-
-riboStateLocalT ::
-  (Monad (t (Ribo s)), MonadTrans t) =>
-  Lens' s s' ->
-  StateT s' (ExceptT e (t (Ribo s))) a ->
-  ExceptT e (t (Ribo s)) a
-riboStateLocalT zoom ma = do
-  state <- lift $ lift $ Ribo.inspect $ Lens.view zoom
-  (output, newState) <- runStateT ma state
-  lift $ lift $ Ribo.modify $ Lens.set zoom newState
-  return output
-
-riboStateT ::
-  (Monad (t (Ribo s)), MonadTrans t) =>
-  StateT s (ExceptT e (t (Ribo s))) a ->
-  ExceptT e (t (Ribo s)) a
-riboStateT =
-  riboStateLocalT id
-
-riboStateLocalE ::
-  Lens' s s' ->
-  StateT s' (ExceptT e (Ribo s)) a ->
-  RiboE s e a
-riboStateLocalE zoom ma = RiboE $ do
-  state <- lift $ Ribo.inspect $ Lens.view zoom
-  (output, newState) <- runStateT ma state
-  lift $ Ribo.modify $ Lens.set zoom newState
-  return output
-
-riboStateE ::
-  StateT s (ExceptT e (Ribo s)) a ->
-  RiboE s e a
-riboStateE =
-  riboStateLocalE id
-
-runRiboStateE ::
-  StateT s (ExceptT e (Ribo s)) a ->
-  Ribo s (Either e a)
-runRiboStateE = runRiboE . riboStateE
-
-riboStateLocal ::
-  Lens' s s' ->
-  StateT s' (Ribo s) a ->
-  Ribo s a
-riboStateLocal zoom ma = do
-  state <- Ribo.inspect $ Lens.view zoom
-  (output, newState) <- runStateT ma state
-  Ribo.modify $ Lens.set zoom newState
-  return output
-
-riboState ::
-  StateT s (Ribo s) a ->
-  Ribo s a
-riboState =
-  riboStateLocal id
-
-modifyL ::
-  (MonadState s m) =>
-  Lens' s a ->
-  (a -> a) ->
-  m ()
-modifyL lens f =
-  modify $ Lens.over lens f
-
-prepend ::
-  (MonadState s m) =>
-  Lens' s [a] ->
-  a ->
-  m ()
-prepend lens a =
-  modifyL lens (a :)
diff --git a/lib/Ribosome/Control/Monad/Trans/Ribo.hs b/lib/Ribosome/Control/Monad/Trans/Ribo.hs
deleted file mode 100644
--- a/lib/Ribosome/Control/Monad/Trans/Ribo.hs
+++ /dev/null
@@ -1,85 +0,0 @@
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-{-# LANGUAGE UndecidableInstances #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE StandaloneDeriving #-}
-
-module Ribosome.Control.Monad.Trans.Ribo where
-
-import Control.Concurrent.STM.TVar (swapTVar)
-import Control.Monad.Error.Class (MonadError(..))
-import Control.Monad.State (MonadState(..))
-import Control.Monad.Trans.Except (ExceptT(ExceptT), runExceptT, mapExceptT)
-import Control.Monad.Trans.Class
-import Control.Monad.IO.Class (MonadIO)
-import qualified Control.Monad.Trans.Except as Except (catchE)
-import Data.Bifunctor (Bifunctor(..))
-import Data.Either.Combinators (mapLeft)
-import Data.Functor (void)
-import UnliftIO.Exception (throwString)
-import UnliftIO.STM (TVar, atomically, readTVarIO)
-import Neovim (ask)
-import Neovim.Context.Internal (Neovim)
-import Ribosome.Control.Ribosome (Ribosome)
-import qualified Ribosome.Control.Ribosome as Ribosome (env)
-
-type Ribo e = Neovim (Ribosome (TVar e))
-
-newtype RiboT t s e a =
-  RiboT { unRiboT :: ExceptT e (t (Ribo s)) a }
-
-deriving instance Functor (t (Ribo s)) => Functor (RiboT t s e)
-deriving instance Monad (t (Ribo s)) => Applicative (RiboT t s e)
-deriving instance Monad (t (Ribo s)) => Monad (RiboT t s e)
-deriving instance MonadIO (t (Ribo s)) => MonadIO (RiboT t s e)
-deriving instance Monad (t (Ribo s)) => MonadError e (RiboT t s e)
-
-nvim :: MonadTrans t => Neovim (Ribosome (TVar s)) a -> RiboT t s e a
-nvim = RiboT . ExceptT . lift . fmap Right
-
-asNeovim :: RiboT t s e a -> t (Ribo s) (Either e a)
-asNeovim = runExceptT . unRiboT
-
-asNeovimWith :: (∀ b. t (Ribo s) b -> Ribo s b) -> RiboT t s e a -> Ribo s (Either e a)
-asNeovimWith run = run . runExceptT . unRiboT
-
-mapE :: (Functor (t (Ribo s))) => (e -> e') -> RiboT t s e a -> RiboT t s e' a
-mapE f =
-  RiboT . trans . unRiboT
-  where
-    trans = mapExceptT (fmap $ mapLeft f)
-
-liftEither :: (Monad (t (Ribo s))) => Either e a -> RiboT t s e a
-liftEither = RiboT . ExceptT . return
-
-catchE :: (Monad (t (Ribo s))) => (e -> RiboT t s e' a) -> RiboT t s e a -> RiboT t s e' a
-catchE f = RiboT . flip Except.catchE (unRiboT . f) . unRiboT
-
-stateTVar :: (MonadTrans t, Functor (t (Ribo s))) => RiboT t s e (TVar s)
-stateTVar =
-  Ribosome.env <$> nvim ask
-
-toException :: Show e => Ribo s (Either e a) -> Ribo s a
-toException ma = ma >>= either (throwString . show) return
-
-unsafeToNeovim :: (MonadTrans t, Monad (t (Ribo s)), Show e) => RiboT t s e a -> t (Ribo s) a
-unsafeToNeovim ra = do
-  r <- asNeovim ra
-  lift $ either (throwString . show) return r
-
-unsafeToNeovimWith :: Show e => (∀ b. t (Ribo s) b -> Ribo s b) -> RiboT t s e a -> Ribo s a
-unsafeToNeovimWith run =
-  toException . run . asNeovim
-
-instance (MonadTrans t, Monad (t (Ribo s))) => MonadState s (RiboT t s e) where
-  get = do
-    t <- stateTVar
-    nvim $ readTVarIO t
-  put newState = do
-    t <- stateTVar
-    void $ nvim $ atomically $ swapTVar t newState
-
-instance (Functor (t (Ribo s))) => Bifunctor (RiboT t s) where
-  first = mapE
-  second = fmap
diff --git a/lib/Ribosome/Control/Monad/Trans/Unlift.hs b/lib/Ribosome/Control/Monad/Trans/Unlift.hs
deleted file mode 100644
--- a/lib/Ribosome/Control/Monad/Trans/Unlift.hs
+++ /dev/null
@@ -1,15 +0,0 @@
-module Ribosome.Control.Monad.Trans.Unlift(
-  MonadUnlift(..),
-) where
-
-import Control.Monad.Trans.Except (ExceptT, mapExceptT)
-
-class MonadUnlift t where
-  unlift :: Monad m => (m a -> m b) -> t m a -> t m b
-
-instance MonadUnlift (ExceptT e) where
-  unlift f =
-    mapExceptT (>>= trans)
-    where
-      trans (Left e) = return $ Left e
-      trans (Right a) = Right <$> f (return a)
diff --git a/lib/Ribosome/Control/Ribo.hs b/lib/Ribosome/Control/Ribo.hs
deleted file mode 100644
--- a/lib/Ribosome/Control/Ribo.hs
+++ /dev/null
@@ -1,79 +0,0 @@
-module Ribosome.Control.Ribo(
-  Ribo,
-  state,
-  swap,
-  put,
-  inspect,
-  modify,
-  name,
-  prepend,
-  modifyL,
-  riboInternal,
-  getErrors,
-  inspectErrors,
-  modifyErrors,
-) where
-
-import Control.Concurrent.STM.TVar (modifyTVar, swapTVar)
-import Control.Lens (Lens')
-import qualified Control.Lens as Lens (over)
-import Data.Functor (void)
-import Neovim (Neovim, ask)
-import UnliftIO.STM (TVar, atomically, readTVarIO)
-
-import Ribosome.Control.Ribosome (Ribosome(Ribosome), RibosomeInternal)
-import qualified Ribosome.Control.Ribosome as Ribosome (errors, _errors)
-import Ribosome.Data.Errors (Errors)
-
-type Ribo e = Neovim (Ribosome e)
-
-state :: Ribo (TVar e) e
-state = do
-  Ribosome _ _ t <- ask
-  readTVarIO t
-
-swap :: e -> Ribo (TVar e) e
-swap newState = do
-  Ribosome _ _ t <- ask
-  atomically $ swapTVar t newState
-
-put :: e -> Ribo (TVar e) ()
-put = void . swap
-
-inspect :: (e -> a) -> Ribo (TVar e) a
-inspect f = fmap f state
-
-modify :: (e -> e) -> Ribo (TVar e) ()
-modify f = do
-  Ribosome _ _ t <- ask
-  atomically $ modifyTVar t f
-
-modifyL :: Lens' s a -> (a -> a) -> Ribo (TVar s) ()
-modifyL lens f =
-  modify $ Lens.over lens f
-
-prepend :: Lens' s [a] -> a -> Ribo (TVar s) ()
-prepend lens a =
-  modifyL lens (a :)
-
-name :: Ribo e String
-name = do
-  Ribosome n _ _ <- ask
-  return n
-
-riboInternal :: Ribo d RibosomeInternal
-riboInternal = do
-  Ribosome _ intTv _ <- ask
-  readTVarIO intTv
-
-getErrors :: Ribo e Errors
-getErrors =
-  Ribosome.errors <$> riboInternal
-
-inspectErrors :: (Errors -> a) -> Ribo e a
-inspectErrors f = fmap f getErrors
-
-modifyErrors :: (Errors -> Errors) -> Ribo e ()
-modifyErrors f = do
-  Ribosome _ intTv _ <- ask
-  atomically $ modifyTVar intTv $ Lens.over Ribosome._errors f
diff --git a/lib/Ribosome/Control/Ribosome.hs b/lib/Ribosome/Control/Ribosome.hs
--- a/lib/Ribosome/Control/Ribosome.hs
+++ b/lib/Ribosome/Control/Ribosome.hs
@@ -1,50 +1,56 @@
 {-# LANGUAGE TemplateHaskell #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-{-# LANGUAGE FunctionalDependencies #-}
-{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE DeriveAnyClass #-}
 
-module Ribosome.Control.Ribosome(
-  Ribosome (..),
-  RibosomeInternal (..),
-  Locks,
-  newRibosome,
-  _locks,
-  _errors,
-  newInternalTVar,
-  _internal,
-) where
+module Ribosome.Control.Ribosome where
 
-import Control.Lens (makeClassy_)
+import Control.Lens (makeClassy)
 import Control.Monad.IO.Class (MonadIO)
-import Data.Default (def)
-import Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map (empty)
-import UnliftIO.STM (TVar, newTVarIO, TMVar)
+import Data.Default (Default(def))
+import Data.Functor.Syntax ((<$$>))
+import Data.Map (Map)
+import Data.MessagePack (Object)
+import GHC.Generics (Generic)
+import UnliftIO.STM (TMVar, TMVar, newTMVarIO)
 
+import Path (Abs, Dir, Path)
 import Ribosome.Data.Errors (Errors)
+import Ribosome.Data.Scratch (Scratch)
 
-type Locks = Map String (TMVar ())
+type Locks = Map Text (TMVar ())
 
 data RibosomeInternal =
   RibosomeInternal {
-    locks :: Locks,
-    errors :: Errors
+    _locks :: Locks,
+    _errors :: Errors,
+    _scratch :: Map Text Scratch,
+    _watchedVariables :: Map Text Object,
+    _projectDir :: Maybe (Path Abs Dir)
   }
-makeClassy_ ''RibosomeInternal
+  deriving (Generic, Default)
 
-data Ribosome e =
+makeClassy ''RibosomeInternal
+
+data RibosomeState s =
+  RibosomeState {
+    _internal :: RibosomeInternal,
+    _public :: s
+  }
+  deriving (Generic, Default)
+
+makeClassy ''RibosomeState
+
+data Ribosome s =
   Ribosome {
-    name :: String,
-    internal :: TVar RibosomeInternal,
-    env :: e
+    _name :: Text,
+    _state :: TMVar (RibosomeState s)
   }
-makeClassy_ ''Ribosome
 
-newInternalTVar :: MonadIO m => m (TVar RibosomeInternal)
-newInternalTVar = newTVarIO (RibosomeInternal Map.empty def)
+makeClassy ''Ribosome
 
-newRibosome :: MonadIO m => String -> e -> m (Ribosome (TVar e))
-newRibosome name' env' = do
-  envTv <- newTVarIO env'
-  intTv <- newInternalTVar
-  return $ Ribosome name' intTv envTv
+newRibosomeTMVar :: MonadIO m => s -> m (TMVar (RibosomeState s))
+newRibosomeTMVar s =
+  newTMVarIO (RibosomeState def s)
+
+newRibosome :: MonadIO m => Text -> s -> m (Ribosome s)
+newRibosome name' =
+  Ribosome name' <$$> newRibosomeTMVar
diff --git a/lib/Ribosome/Control/StrictRibosome.hs b/lib/Ribosome/Control/StrictRibosome.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Control/StrictRibosome.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Control.StrictRibosome where
+
+import Control.Lens (makeClassy)
+
+import Ribosome.Control.Ribosome (RibosomeState)
+
+data StrictRibosome s =
+  StrictRibosome {
+    _name :: Text,
+    _state :: RibosomeState s
+    }
+
+makeClassy ''StrictRibosome
+
+instance Default s => Default (StrictRibosome s) where
+  def = StrictRibosome "default" def
diff --git a/lib/Ribosome/Data/Conduit.hs b/lib/Ribosome/Data/Conduit.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/Conduit.hs
@@ -0,0 +1,105 @@
+module Ribosome.Data.Conduit where
+
+import Conduit (ConduitT, runConduit, yield, (.|))
+import Control.Concurrent.Lifted (fork, killThread)
+import Control.Concurrent.STM.TBMChan (TBMChan, closeTBMChan, newTBMChan, readTBMChan, writeTBMChan)
+import Control.Concurrent.STM.TMVar (TMVar, newTMVar)
+import Control.Exception.Lifted (bracket, finally)
+import Control.Monad.Trans.Control (MonadBaseControl)
+import qualified Data.Conduit.Combinators as Conduit (mapM_)
+
+import Ribosome.Control.Monad.Ribo (modifyTMVar)
+
+withTBMChan ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  Int ->
+  (TBMChan a -> m b) ->
+  m b
+withTBMChan bound =
+  bracket acquire release
+  where
+    acquire =
+      atomically (newTBMChan bound)
+    release =
+      atomically . closeTBMChan
+
+sourceChan ::
+  MonadIO m =>
+  TBMChan a ->
+  ConduitT () a m ()
+sourceChan chan =
+  loop
+  where
+    loop =
+      traverse_ recurse =<< atomically (readTBMChan chan)
+    recurse a =
+      yield a *> loop
+
+sourceTerminated ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  TMVar Int ->
+  TBMChan a ->
+  m ()
+sourceTerminated var chan = do
+  n <- modifyTMVar (subtract 1) var
+  when (n == 0) (atomically $ closeTBMChan chan)
+
+withSourcesInChanAs ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  (ConduitT () a m () -> m b) ->
+  [ConduitT () a m ()] ->
+  TBMChan a ->
+  m b
+withSourcesInChanAs executor sources chan = do
+  activeSources <- atomically $ newTMVar (length sources)
+  threadIds <- traverse (fork . start activeSources) sources
+  finally listen (release threadIds)
+  where
+    release =
+      traverse_ killThread
+    listen =
+      executor $ sourceChan chan
+    start activeSources source = do
+      runConduit (source .| Conduit.mapM_ (atomically . writeTBMChan chan))
+      sourceTerminated activeSources chan
+
+simpleExecutor ::
+  Monad m =>
+  ConduitT a Void m b ->
+  ConduitT () a m () ->
+  m b
+simpleExecutor consumer s =
+  runConduit $ s .| consumer
+
+withSourcesInChan ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  ConduitT a Void m b ->
+  [ConduitT () a m ()] ->
+  TBMChan a ->
+  m b
+withSourcesInChan =
+  withSourcesInChanAs . simpleExecutor
+
+withMergedSourcesAs ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  (ConduitT () a m () -> m b) ->
+  Int ->
+  [ConduitT () a m ()] ->
+  m b
+withMergedSourcesAs executor bound sources =
+  withTBMChan bound (withSourcesInChanAs executor sources)
+
+withMergedSources ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  ConduitT a Void m b ->
+  Int ->
+  [ConduitT () a m ()] ->
+  m b
+withMergedSources =
+  withMergedSourcesAs . simpleExecutor
diff --git a/lib/Ribosome/Data/Conduit/Composition.hs b/lib/Ribosome/Data/Conduit/Composition.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/Conduit/Composition.hs
@@ -0,0 +1,418 @@
+{-# LANGUAGE CPP #-}
+
+module Ribosome.Data.Conduit.Composition where
+
+import Conduit
+import qualified Control.Concurrent.Async.Lifted as A
+import Control.Concurrent.Async.Lifted hiding (link2)
+import Control.Concurrent.STM hiding (atomically, newTVarIO)
+import Control.Exception.Lifted (finally)
+import Control.Monad hiding (forM_)
+import Control.Monad.Base (MonadBase, liftBase)
+import Control.Monad.Loops
+import Control.Monad.Trans.Resource
+import qualified Data.Conduit.Binary as CB
+import qualified Data.Conduit.Cereal as C
+import qualified Data.Conduit.List as CL
+import Data.Foldable (forM_)
+import Data.Serialize
+import GHC.Exts (Constraint)
+import Prelude hiding (get, put)
+import System.Directory (removeFile)
+import System.IO
+
+-- | Concurrently join the producer and consumer, using a bounded queue of the
+-- given size. The producer will block when the queue is full, if it is
+-- producing faster than the consumers is taking from it. Likewise, if the
+-- consumer races ahead, it will block until more input is available.
+--
+-- Exceptions are properly managed and propagated between the two sides, so
+-- the net effect should be equivalent to not using buffer at all, save for
+-- the concurrent interleaving of effects.
+--
+-- The underlying monad must always be an instance of
+-- 'MonadBaseControl IO'.  If at least one of the two conduits is a
+-- 'CFConduit', it must additionally be a in instance of
+-- 'MonadResource'.
+--
+-- This function is similar to '$$'; for one more like '=$=', see
+-- 'buffer''.
+--
+-- >>> buffer 1 (CL.sourceList [1,2,3]) CL.consume
+-- [1,2,3]
+buffer :: (CCatable c1 c2 c3, CRunnable c3, RunConstraints c3 m)
+          => Int -- ^ Size of the bounded queue in memory.
+          -> c1 () x m ()
+          -> c2 x Void m r
+          -> m r
+buffer i c1 c2 = runCConduit (buffer' i c1 c2)
+
+-- | An operator form of 'buffer'.  In general you should be able to replace
+-- any use of '$$' with '$$&' and suddenly reap the benefit of
+-- concurrency, if your conduits were spending time waiting on each other.
+--
+-- The underlying monad must always be an instance of
+-- 'MonadBaseControl IO'.  If at least one of the two conduits is a
+-- 'CFConduit', it must additionally be a in instance of
+-- 'MonadResource'.
+--
+-- >>> CL.sourceList [1,2,3] $$& CL.consume
+-- [1,2,3]
+--
+-- It can be combined with '$=&' and '$='.  This creates two threads;
+-- the first thread produces the list and the second thread does the
+-- map and the consume:
+--
+-- >>> CL.sourceList [1,2,3] $$& mapC (*2) $= CL.consume
+-- [2,4,6]
+--
+-- This creates three threads.  The three conduits all run in their
+-- own threads:
+--
+-- >>> CL.sourceList [1,2,3] $$& mapC (*2) $=& CL.consume
+-- [2,4,6]
+--
+-- >>> CL.sourceList [1,2,3] $$& (mapC (*2) $= mapC (+1)) $=& CL.consume
+-- [3,5,7]
+($$&) :: (CCatable c1 c2 c3, CRunnable c3, RunConstraints c3 m) => c1 () x m () -> c2 x Void m r -> m r
+a $$& b = runCConduit (a =$=& b)
+infixr 0 $$&
+
+-- | An operator form of 'buffer''.  In general you should be able to replace
+-- any use of '=$=' with '=$=&' and '$$' either with '$$&' or '=$='
+-- and 'runCConduit' and suddenly reap the benefit of concurrency, if
+-- your conduits were spending time waiting on each other.
+--
+-- >>> runCConduit $ CL.sourceList [1,2,3] =$=& CL.consume
+-- [1,2,3]
+(=$=&) :: (CCatable c1 c2 c3) => c1 i x m () -> c2 x o m r -> c3 i o m r
+a =$=& b = buffer' 64 a b
+infixr 2 =$=&
+
+-- | An alias for '=$=&' by analogy with '=$=' and '$='.
+($=&) :: (CCatable c1 c2 c3) => c1 i x m () -> c2 x o m r -> c3 i o m r
+($=&) = (=$=&)
+infixl 1 $=&
+
+-- | An alias for '=$=&' by analogy with '=$=' and '=$'.
+(=$&) :: (CCatable c1 c2 c3) => c1 i x m () -> c2 x o m r -> c3 i o m r
+(=$&) = (=$=&)
+infixr 2 =$&
+
+-- | Conduits are concatenable; this class describes how.
+-- class CCatable (c1 :: * -> * -> (* -> *) -> * -> *) (c2 :: * -> * -> (* -> *) -> * -> *) (c3 :: * -> * -> (* -> *) -> * -> *) | c1 c2 -> c3 where
+class CCatable c1 c2 (c3 :: * -> * -> (* -> *) -> * -> *) | c1 c2 -> c3 where
+  -- | Concurrently join the producer and consumer, using a bounded queue of the
+  -- given size. The producer will block when the queue is full, if it is
+  -- producing faster than the consumers is taking from it. Likewise, if the
+  -- consumer races ahead, it will block until more input is available.
+  --
+  -- Exceptions are properly managed and propagated between the two sides, so
+  -- the net effect should be equivalent to not using buffer at all, save for
+  -- the concurrent interleaving of effects.
+  --
+  -- This function is similar to '=$='; for one more like '$$', see
+  -- 'buffer'.
+  --
+  -- >>> runCConduit $ buffer' 1 (CL.sourceList [1,2,3]) CL.consume
+  -- [1,2,3]
+  buffer' :: Int -- ^ Size of the bounded queue in memory
+             -> c1 i x m ()
+             -> c2 x o m r
+             -> c3 i o m r
+
+-- | Like 'buffer', except that when the bounded queue is overflowed, the
+-- excess is cached in a local file so that consumption from upstream may
+-- continue. When the queue becomes exhausted by yielding, it is filled
+-- from the cache until all elements have been yielded.
+--
+-- Note that the maximum amount of memory consumed is equal to (2 *
+-- memorySize + 1), so take this into account when picking a chunking size.
+--
+-- This function is similar to '$$'; for one more like '=$=', see
+-- 'bufferToFile''.
+--
+-- >>> runResourceT $ bufferToFile 1 Nothing "/tmp" (CL.sourceList [1,2,3]) CL.consume
+-- [1,2,3]
+bufferToFile :: (CFConduitLike c1, CFConduitLike c2, Serialize x, MonadBaseControl IO m, MonadIO m, MonadResource m, MonadThrow m)
+                => Int -- ^ Size of the bounded queue in memory
+                -> Maybe Int -- ^ Max elements to keep on disk at one time
+                -> FilePath -- ^ Directory to write temp files to
+                -> c1 () x m ()
+                -> c2 x Void m r
+                -> m r
+bufferToFile bufsz dsksz tmpDir c1 c2 = runCConduit (bufferToFile' bufsz dsksz tmpDir c1 c2)
+
+-- | Like 'buffer'', except that when the bounded queue is overflowed, the
+-- excess is cached in a local file so that consumption from upstream may
+-- continue. When the queue becomes exhausted by yielding, it is filled
+-- from the cache until all elements have been yielded.
+--
+-- Note that the maximum amount of memory consumed is equal to (2 *
+-- memorySize + 1), so take this into account when picking a chunking size.
+--
+-- This function is similar to '=$='; for one more like '$$', see
+-- 'bufferToFile'.
+--
+-- >>> runResourceT $ runCConduit $ bufferToFile' 1 Nothing "/tmp" (CL.sourceList [1,2,3]) CL.consume
+-- [1,2,3]
+--
+-- It is frequently convenient to define local function to use this in operator form:
+--
+-- >>> :{
+-- runResourceT $ do
+--   let buf c = bufferToFile' 10 Nothing "/tmp" c -- eta-conversion to avoid monomorphism restriction
+--   runCConduit $ CL.sourceList [0x30, 0x31, 0x32] `buf` mapC (toEnum :: Int -> Char) `buf` CL.consume
+-- :}
+-- "012"
+bufferToFile' :: (CFConduitLike c1, CFConduitLike c2, Serialize x)
+                 => Int -- ^ Size of the bounded queue in memory
+                 -> Maybe Int -- ^ Max elements to keep on disk at one time
+                 -> FilePath -- ^ Directory to write temp files to
+                 -> c1 i x m ()
+                 -> c2 x o m r
+                 -> CFConduit i o m r
+bufferToFile' bufsz dsksz tmpDir c1 c2 = combine (asCFConduit c1) (asCFConduit c2)
+  where combine (FSingle a) b = FMultipleF bufsz dsksz tmpDir a b
+        combine (FMultiple i a as) b = FMultiple i a (bufferToFile' bufsz dsksz tmpDir as b)
+        combine (FMultipleF bufsz' dsksz' tmpDir' a as) b = FMultipleF bufsz' dsksz' tmpDir' a (bufferToFile' bufsz dsksz tmpDir as b)
+
+-- | Conduits are, once there's a producer on one end and a consumer
+-- on the other, runnable.
+class CRunnable c where
+  type RunConstraints c (m :: * -> *) :: Constraint
+  -- | Execute a conduit concurrently.  This is the concurrent
+  -- equivalent of 'runConduit'.
+  --
+  -- The underlying monad must always be an instance of
+  -- 'MonadBaseControl IO'.  If the conduits is a 'CFConduit', it must
+  -- additionally be a in instance of 'MonadResource'.
+  runCConduit :: (RunConstraints c m) => c () Void m r -> m r
+
+instance CCatable ConduitT ConduitT CConduit where
+  buffer' i a b = buffer' i (Single a) (Single b)
+
+instance CCatable ConduitT CConduit CConduit where
+  buffer' i a b = buffer' i (Single a) b
+
+instance CCatable ConduitT CFConduit CFConduit where
+  buffer' i a b = buffer' i (asCFConduit a) b
+
+instance CCatable CConduit ConduitT CConduit where
+  buffer' i a b = buffer' i a (Single b)
+
+instance CCatable CConduit CConduit CConduit where
+  buffer' i (Single a) b = Multiple i a b
+  buffer' i (Multiple i' a as) b = Multiple i' a (buffer' i as b)
+
+instance CCatable CConduit CFConduit CFConduit where
+  buffer' i a b = buffer' i (asCFConduit a) b
+
+instance CCatable CFConduit ConduitT CFConduit where
+  buffer' i a b = buffer' i a (asCFConduit b)
+
+instance CCatable CFConduit CConduit CFConduit where
+  buffer' i a b = buffer' i a (asCFConduit b)
+
+instance CCatable CFConduit CFConduit CFConduit where
+  buffer' i (FSingle a) b = FMultiple i a b
+  buffer' i (FMultiple i' a as) b = FMultiple i' a (buffer' i as b)
+  buffer' i (FMultipleF bufsz dsksz tmpDir a as) b = FMultipleF bufsz dsksz tmpDir a (buffer' i as b)
+
+instance CRunnable ConduitT where
+  type RunConstraints ConduitT m = (Monad m)
+  runCConduit = runConduit
+
+instance CRunnable CConduit where
+  type RunConstraints CConduit m = (MonadBaseControl IO m, MonadIO m)
+  runCConduit (Single c) = runConduit c
+  runCConduit (Multiple bufsz c cs) = do
+    chan <- liftIO $ newTBQueueIO bufsz
+    withAsync (sender chan c) $ \c' ->
+      stage chan c' cs
+
+instance CRunnable CFConduit where
+  type RunConstraints CFConduit m = (MonadBaseControl IO m, MonadIO m, MonadResource m, MonadThrow m)
+  runCConduit (FSingle c) = runConduit c
+  runCConduit (FMultiple bufsz c cs) = do
+    chan <- liftIO $ newTBQueueIO bufsz
+    withAsync (sender chan c) $ \c' ->
+      fstage (receiver chan) c' cs
+  runCConduit (FMultipleF bufsz filemax tempDir c cs) = do
+    context <- liftIO $ BufferContext <$> newTBQueueIO bufsz
+                                      <*> newTQueueIO
+                                      <*> newTVarIO filemax
+                                      <*> newTVarIO False
+                                      <*> pure tempDir
+    withAsync (fsender context c) $ \c' ->
+      fstage (freceiver context) c' cs
+
+-- | A "concurrent conduit", in which the stages run in parallel with
+-- a buffering queue between them.
+data CConduit i o m r where
+  Single :: ConduitT i o m r -> CConduit i o m r
+  Multiple :: Int -> ConduitT i x m () -> CConduit x o m r -> CConduit i o m r
+
+-- C.C.A.L's link2 has the wrong type:  https://github.com/maoe/lifted-async/issues/16
+link2 :: MonadBase IO m => Async a -> Async b -> m ()
+link2 = (liftBase .) . A.link2
+
+-- Combines a producer with a queue, sending it everything the
+-- producer produces.
+sender :: (MonadIO m) => TBQueue (Maybe o) -> ConduitT () o m () -> m ()
+sender chan input = do
+  runConduit $ input .| mapM_C (send chan . Just)
+  send chan Nothing
+
+-- One "layer" of withAsync in a CConduit run.
+stage :: (MonadBaseControl IO m, MonadIO m) => TBQueue (Maybe i) -> Async x -> CConduit i Void m r -> m r
+stage chan prevAsync (Single c) =
+  -- The last layer; feed the output of "chan" into the conduit and
+  -- wait for the result.
+  withAsync (runConduit $ receiver chan .| c) $ \c' -> do
+    link2 prevAsync c'
+    wait c'
+stage chan prevAsync (Multiple bufsz c cs) = do
+  -- not the last layer, so take the input from "chan", have this
+  -- layer's conduit process it, and send the conduit's output to the
+  -- next layer.
+  chan' <- liftIO $ newTBQueueIO bufsz
+  withAsync (sender chan' $ receiver chan .| c) $ \c' -> do
+    link2 prevAsync c'
+    stage chan' c' cs
+
+-- A Producer which produces the values of the given channel until
+-- Nothing is received.  This is the other half of "sender".
+receiver :: (MonadIO m) => TBQueue (Maybe o) -> ConduitT () o m ()
+receiver chan = do
+  mx <- recv chan
+  case mx of
+   Nothing -> return ()
+   Just x -> yield x >> receiver chan
+
+-- | A "concurrent conduit", in which the stages run in parallel with
+-- a buffering queue and possibly a disk file between them.
+data CFConduit i o m r where
+  FSingle :: ConduitT i o m r -> CFConduit i o m r
+  FMultiple :: Int -> ConduitT i x m () -> CFConduit x o m r -> CFConduit i o m r
+  FMultipleF :: (Serialize x) => Int -> Maybe Int -> FilePath -> ConduitT i x m () -> CFConduit x o m r -> CFConduit i o m r
+
+class CFConduitLike a where
+  asCFConduit :: a i o m r -> CFConduit i o m r
+
+instance CFConduitLike ConduitT where
+  asCFConduit = FSingle
+
+instance CFConduitLike CConduit where
+  asCFConduit (Single c) = FSingle c
+  asCFConduit (Multiple i c cs) = FMultiple i c (asCFConduit cs)
+
+instance CFConduitLike CFConduit where
+  asCFConduit = id
+
+data BufferContext m a = BufferContext { chan :: TBQueue a
+                                       , restore :: TQueue (ConduitT () a m ())
+                                       , slotsFree :: TVar (Maybe Int)
+                                       , done :: TVar Bool
+                                       , tempDir :: FilePath
+                                       }
+
+-- The file-backed equivlent of "sender".  This sends the values
+-- generated by "input" to the "chan" in the BufferContext until it
+-- gets full, then flushes it to disk via "persistChan".
+fsender :: (MonadIO m, MonadResource m, Serialize x, MonadThrow m) => BufferContext m x -> ConduitT () x m () -> m ()
+fsender bc@BufferContext{..} input = do
+  runConduit $ input .| mapM_C f
+  liftIO $ atomically $ writeTVar done True
+  where
+    f x = join $ liftIO $ atomically $
+      (writeTBQueue chan x >> return (return ())) `orElse` do
+        action <- persistChan bc
+        writeTBQueue chan x
+        return action
+
+-- Connect a stage to another stage via either an in-memory queue or a
+-- disk buffer.  This is the file-backed equivalent of "stage".
+fstage :: (MonadBaseControl IO m, MonadIO m, MonadResource m, MonadThrow m) => ConduitT () i m () -> Async x -> CFConduit i Void m r -> m r
+fstage prevStage prevAsync (FSingle c) =
+  -- The final conduit in the chain; just accept everything from
+  -- the previous stage and wait for the result.
+  withAsync (runConduit $ prevStage .| c) $ \c' -> do
+    link2 prevAsync c'
+    wait c'
+fstage prevStage prevAsync (FMultiple bufsz c cs) = do
+  -- This stage is connected to the next via a non-file-backed
+  -- channel, so it just uses "sender" and "reciever" in the same way
+  -- "stage" does.
+  chan' <- liftIO $ newTBQueueIO bufsz
+  withAsync (sender chan' $ prevStage .| c) $ \c' -> do
+    link2 prevAsync c'
+    fstage (receiver chan') c' cs
+fstage prevStage prevAsync (FMultipleF bufsz dsksz tempDir c cs) = do
+  -- This potentially needs to write its output to a file, so it uses
+  -- "fsender" send and tells the next stage to use "freceiver" to read.
+  bc <- liftIO $ BufferContext <$> newTBQueueIO bufsz
+                               <*> newTQueueIO
+                               <*> newTVarIO dsksz
+                               <*> newTVarIO False
+                               <*> pure tempDir
+  withAsync (fsender bc $ prevStage .| c) $ \c' -> do
+    link2 prevAsync c'
+    fstage (freceiver bc) c' cs
+
+-- Receives from disk files or the in-memory queue if no spill-to-disk
+-- has occurred.
+freceiver :: (MonadIO m) => BufferContext m o -> ConduitT () o m ()
+freceiver BufferContext{..} = loop where
+  loop = do
+    (src, exit) <- liftIO $ atomically $ do
+      (readTQueue restore >>= (\action -> return (action, False))) `orElse` do
+        xs <- exhaust chan
+        isDone <- readTVar done
+        return (CL.sourceList xs, isDone)
+    src
+    unless exit loop
+
+-- The channel is full, so (return an action which will) spill it to disk, unless too
+-- many items are there already.
+persistChan :: (MonadIO m, MonadResource m, Serialize o, MonadThrow m) => BufferContext m o -> STM (m ())
+persistChan BufferContext{..} = do
+  xs <- exhaust chan
+  mslots <- readTVar slotsFree
+  let len = length xs
+  forM_ mslots $ \slots -> check (len < slots)
+  filePath <- newEmptyTMVar
+  writeTQueue restore $ do
+    (path, key) <- liftIO $ atomically $ takeTMVar filePath
+    CB.sourceFile path .| do
+      C.conduitGet2 get
+      liftIO $ atomically $ modifyTVar slotsFree (fmap (+ len))
+      release key
+  case xs of
+   [] -> return (return ())
+   _ -> do
+     modifyTVar slotsFree (fmap (subtract len))
+     return $ do
+       (key, (path, h)) <- allocate (openBinaryTempFile tempDir "conduit.bin") (\(path, h) -> hClose h `finally` removeFile path)
+       liftIO $ do
+         runConduit $ CL.sourceList xs .| C.conduitPut put .| CB.sinkHandle h
+         hClose h
+         atomically $ putTMVar filePath (path, key)
+
+exhaust :: TBQueue a -> STM [a]
+exhaust chan = whileM (not <$> isEmptyTBQueue chan) (readTBQueue chan)
+
+recv :: (MonadIO m) => TBQueue a -> m a
+recv c = liftIO . atomically $ readTBQueue c
+
+send :: (MonadIO m) => TBQueue a -> a -> m ()
+send c = liftIO . atomically . writeTBQueue c
+
+ccMap :: (∀ i1 . ConduitT i1 o1 m a -> ConduitT i1 o2 m a) -> CConduit i o1 m a -> CConduit i o2 m a
+ccMap f =
+  go
+  where
+    go (Single c) =
+      Single (f c)
+    go (Multiple bufsz l r) =
+      Multiple bufsz l (ccMap f r)
diff --git a/lib/Ribosome/Data/ErrorReport.hs b/lib/Ribosome/Data/ErrorReport.hs
--- a/lib/Ribosome/Data/ErrorReport.hs
+++ b/lib/Ribosome/Data/ErrorReport.hs
@@ -8,13 +8,12 @@
 ) where
 
 import Control.Lens (makeClassy)
-import Prelude hiding (log)
 import System.Log (Priority)
 
 data ErrorReport =
   ErrorReport {
-    _user :: String,
-    _log :: [String],
+    _user :: Text,
+    _log :: [Text],
     _priority :: Priority
   }
   deriving (Eq, Show)
diff --git a/lib/Ribosome/Data/Errors.hs b/lib/Ribosome/Data/Errors.hs
--- a/lib/Ribosome/Data/Errors.hs
+++ b/lib/Ribosome/Data/Errors.hs
@@ -13,13 +13,13 @@
 import Data.Default (Default)
 import Data.Map (Map)
 import qualified Data.Map as Map (toList)
-import Data.Text.Prettyprint.Doc (Doc, Pretty(..), align, vsep, line, (<+>), (<>))
+import Data.Text.Prettyprint.Doc (Doc, Pretty(..), align, line, vsep, (<+>), (<>))
 import Prelude hiding (error)
 
 import Ribosome.Data.ErrorReport (ErrorReport(ErrorReport))
 
 newtype ComponentName =
-  ComponentName String
+  ComponentName Text
   deriving (Eq, Ord, Show)
 
 data Error =
@@ -49,4 +49,4 @@
 
 instance Pretty Errors where
   pretty (Errors errors') =
-    pretty ("Errors:" :: String) <> vsep (uncurry prettyComponentErrors <$> Map.toList errors')
+    vsep (uncurry prettyComponentErrors <$> Map.toList errors')
diff --git a/lib/Ribosome/Data/Mapping.hs b/lib/Ribosome/Data/Mapping.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/Mapping.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Data.Mapping where
+
+import Data.DeepPrisms (deepPrisms)
+import Data.MessagePack (Object)
+import Ribosome.Data.ErrorReport (ErrorReport(ErrorReport))
+import Ribosome.Error.Report.Class (ReportError(..))
+import System.Log (Priority(NOTICE, ERROR))
+
+newtype MappingIdent =
+  MappingIdent Text
+  deriving (Eq, Show)
+
+data Mapping =
+  Mapping {
+    mappingIdent :: MappingIdent,
+    mappingLhs :: Text,
+    mappingMode :: Text,
+    mappingRemap :: Bool,
+    mappingBuffer :: Bool
+  }
+  deriving (Eq, Show)
+
+data MappingError =
+  NoSuchMapping MappingIdent
+  |
+  InvalidArgs [Object]
+  deriving (Eq, Show)
+
+deepPrisms ''MappingError
+
+instance ReportError MappingError where
+  errorReport (NoSuchMapping (MappingIdent ident)) =
+    ErrorReport ("no mapping defined for `" <> ident <>"`") ["no such mapping: " <> ident] NOTICE
+  errorReport (InvalidArgs args) =
+    ErrorReport "internal error while executing mapping" ["invalid mapping args:", show args] ERROR
diff --git a/lib/Ribosome/Data/PersistError.hs b/lib/Ribosome/Data/PersistError.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/PersistError.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Data.PersistError where
+
+import System.Log (Priority(INFO, NOTICE, ERROR))
+
+import Ribosome.Data.ErrorReport (ErrorReport(ErrorReport))
+import Ribosome.Error.Report.Class (ReportError(..))
+
+-- TODO use Path
+data PersistError =
+  FileNotReadable FilePath
+  |
+  NoSuchFile FilePath
+  |
+  Decode FilePath Text
+  deriving (Eq, Show)
+
+deepPrisms ''PersistError
+
+instance ReportError PersistError where
+  errorReport (FileNotReadable path) =
+    ErrorReport msg ["PersistError.FileNotReadable:", toText path] NOTICE
+    where
+      msg = "persistence file not readable: " <> toText path
+  errorReport (NoSuchFile path) =
+    ErrorReport msg ["PersistError.SoSuchFile:", toText path] INFO
+    where
+      msg = "no persistence file present at " <> toText path
+  errorReport (Decode path err) =
+    ErrorReport msg ["PersistError.Decode:", toText path, err] ERROR
+    where
+      msg = "invalid data in persistence file, please delete it: " <> toText path
diff --git a/lib/Ribosome/Data/Scratch.hs b/lib/Ribosome/Data/Scratch.hs
--- a/lib/Ribosome/Data/Scratch.hs
+++ b/lib/Ribosome/Data/Scratch.hs
@@ -1,13 +1,13 @@
-module Ribosome.Data.Scratch(
-  Scratch(..),
-) where
+module Ribosome.Data.Scratch where
 
-import Neovim (Buffer, Window, Tabpage)
+import Ribosome.Nvim.Api.Data (Buffer, Tabpage, Window)
 
 data Scratch =
   Scratch {
+    scratchName :: Text,
     scratchBuffer :: Buffer,
     scratchWindow :: Window,
+    scratchPrevious :: Window,
     scratchTab :: Maybe Tabpage
   }
   deriving (Eq, Show)
diff --git a/lib/Ribosome/Data/ScratchOptions.hs b/lib/Ribosome/Data/ScratchOptions.hs
--- a/lib/Ribosome/Data/ScratchOptions.hs
+++ b/lib/Ribosome/Data/ScratchOptions.hs
@@ -1,21 +1,39 @@
-module Ribosome.Data.ScratchOptions(
-  ScratchOptions(..),
-  defaultScratchOptions,
-) where
+module Ribosome.Data.ScratchOptions where
 
 import Data.Default (Default(def))
 
+import Ribosome.Data.Mapping (Mapping)
+import Ribosome.Data.Syntax (Syntax)
+
 data ScratchOptions =
   ScratchOptions {
     tab :: Bool,
     vertical :: Bool,
-    size :: Maybe Int,
     wrap :: Bool,
-    name :: String
+    focus :: Bool,
+    resize :: Bool,
+    bottom :: Bool,
+    size :: Maybe Int,
+    maxSize :: Maybe Int,
+    syntax :: [Syntax],
+    mappings :: [Mapping],
+    name :: Text
   }
 
-defaultScratchOptions :: String -> ScratchOptions
-defaultScratchOptions = ScratchOptions False False Nothing False
+defaultScratchOptions :: Text -> ScratchOptions
+defaultScratchOptions = ScratchOptions False False False False True True Nothing Nothing [] []
 
 instance Default ScratchOptions where
   def = defaultScratchOptions "scratch"
+
+scratchFocus :: ScratchOptions -> ScratchOptions
+scratchFocus so =
+  so { focus = True }
+
+scratchSyntax :: [Syntax] -> ScratchOptions -> ScratchOptions
+scratchSyntax syn so =
+  so { syntax = syn }
+
+scratchMappings :: [Mapping] -> ScratchOptions -> ScratchOptions
+scratchMappings maps so =
+  so { mappings = maps }
diff --git a/lib/Ribosome/Data/Setting.hs b/lib/Ribosome/Data/Setting.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/Setting.hs
@@ -0,0 +1,8 @@
+module Ribosome.Data.Setting where
+
+data Setting a =
+  Setting {
+    name :: Text,
+    prefix :: Bool,
+    fallback :: Maybe a
+  }
diff --git a/lib/Ribosome/Data/SettingError.hs b/lib/Ribosome/Data/SettingError.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/SettingError.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Data.SettingError where
+
+import Data.DeepPrisms (deepPrisms)
+import Data.Text.Prettyprint.Doc (Doc)
+import Data.Text.Prettyprint.Doc.Render.Terminal (AnsiStyle)
+import System.Log (Priority(NOTICE))
+
+import Ribosome.Data.ErrorReport (ErrorReport(..))
+import Ribosome.Error.Report.Class (ReportError(..))
+
+data SettingError =
+  Decode Text (Doc AnsiStyle)
+  |
+  Unset Text
+  deriving Show
+
+deepPrisms ''SettingError
+
+instance ReportError SettingError where
+  errorReport (Decode name message) =
+    ErrorReport ("invalid setting: " <> name) ["failed to decode setting `" <> name <> "`", show message] NOTICE
+  errorReport (Unset name) =
+    ErrorReport ("required setting unset: " <> name) ["unset setting: `" <> name <> "`"] NOTICE
diff --git a/lib/Ribosome/Data/String.hs b/lib/Ribosome/Data/String.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/String.hs
@@ -0,0 +1,11 @@
+module Ribosome.Data.String where
+
+import Data.Char (toUpper)
+
+escapeQuotes :: Char -> String
+escapeQuotes '\'' = "''"
+escapeQuotes a = [a]
+
+capitalize :: String -> String
+capitalize [] = []
+capitalize (head' : tail') = toUpper head' : tail'
diff --git a/lib/Ribosome/Data/Syntax.hs b/lib/Ribosome/Data/Syntax.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/Syntax.hs
@@ -0,0 +1,91 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Ribosome.Data.Syntax where
+
+import Data.Default (Default(def))
+import Data.Map (Map)
+import qualified Data.Map as Map (fromList)
+
+data SyntaxItemDetail =
+  Keyword {
+    kwGroup :: Text,
+    kwKeyword :: Text,
+    kwOptions :: [Text]
+  }
+  |
+  Match {
+    matchGroup :: Text,
+    matchPattern :: Text
+  }
+  |
+  Region {
+    regionGroup :: Text,
+    regionStart :: Text,
+    regionEnd :: Text,
+    regionSkip :: Maybe Text,
+    regionStartOffset :: Text,
+    regionEndOffset :: Text
+  }
+  |
+  Verbatim {
+    verbatimCommand :: Text
+  }
+  deriving (Eq, Show)
+
+data SyntaxItem =
+  SyntaxItem {
+    siDetail :: SyntaxItemDetail,
+    siOptions :: [Text],
+    siParams :: Map Text Text
+  }
+  deriving (Eq, Show)
+
+syntaxItem :: SyntaxItemDetail -> SyntaxItem
+syntaxItem detail =
+  SyntaxItem detail def def
+
+syntaxKeyword :: Text -> Text -> SyntaxItem
+syntaxKeyword group' keyword =
+  syntaxItem $ Keyword group' keyword def
+
+syntaxMatch :: Text -> Text -> SyntaxItem
+syntaxMatch group' pat =
+  syntaxItem $ Match group' pat
+
+syntaxRegionOffset :: Text -> Text -> Text -> Maybe Text -> Text -> Text -> SyntaxItem
+syntaxRegionOffset group' start end skip ms me =
+  syntaxItem $ Region group' start end skip ms me
+
+syntaxRegion :: Text -> Text -> Text -> Maybe Text -> SyntaxItem
+syntaxRegion group' start end skip =
+  syntaxRegionOffset group' start end skip "" ""
+
+syntaxVerbatim :: Text -> SyntaxItem
+syntaxVerbatim =
+  syntaxItem . Verbatim
+
+data Highlight =
+  Highlight {
+    hiGroup :: Text,
+    hiValues :: Map Text Text
+  }
+  deriving (Eq, Show)
+
+syntaxHighlight :: Text -> [(Text, Text)] -> Highlight
+syntaxHighlight group' =
+  Highlight group' . Map.fromList
+
+data HiLink =
+  HiLink {
+    hlGroup :: Text,
+    hlTarget :: Text
+  }
+  deriving (Eq, Show)
+
+data Syntax =
+  Syntax {
+     syntaxItems :: [SyntaxItem],
+     syntaxHighlights :: [Highlight],
+     syntaxHiLinks :: [HiLink]
+  }
+  deriving (Eq, Show, Generic, Default)
diff --git a/lib/Ribosome/Data/Text.hs b/lib/Ribosome/Data/Text.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Data/Text.hs
@@ -0,0 +1,17 @@
+module Ribosome.Data.Text where
+
+import Data.Char (toUpper)
+import qualified Data.Text as Text (concatMap, cons, singleton, uncons)
+
+escapeQuote :: Char -> Text
+escapeQuote '\'' = "''"
+escapeQuote a = Text.singleton a
+
+escapeQuotes :: Text -> Text
+escapeQuotes = Text.concatMap escapeQuote
+
+capitalize :: Text -> Text
+capitalize a =
+  maybe "" run (Text.uncons a)
+  where
+    run (h, t) = Text.cons (toUpper h) t
diff --git a/lib/Ribosome/Data/Time.hs b/lib/Ribosome/Data/Time.hs
deleted file mode 100644
--- a/lib/Ribosome/Data/Time.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-module Ribosome.Data.Time(
-  epochSeconds,
-  usleep,
-  sleep,
-  sleepW,
-) where
-
-import Control.Concurrent (threadDelay)
-import Control.Monad.IO.Class (MonadIO, liftIO)
-import Data.Time.Clock.POSIX (getPOSIXTime)
-import GHC.Float (word2Double)
-
-epochSeconds :: MonadIO m => m Int
-epochSeconds = liftIO $ fmap round getPOSIXTime
-
-usleep :: MonadIO m => Double -> m ()
-usleep =
-  liftIO . threadDelay . round
-
-sleep :: MonadIO m => Double -> m ()
-sleep seconds =
-  usleep $ seconds * 1e6
-
-sleepW :: MonadIO m => Word -> m ()
-sleepW = sleep . word2Double
diff --git a/lib/Ribosome/Error/Report.hs b/lib/Ribosome/Error/Report.hs
--- a/lib/Ribosome/Error/Report.hs
+++ b/lib/Ribosome/Error/Report.hs
@@ -1,39 +1,27 @@
-module Ribosome.Error.Report(
-  ErrorReport(..),
-  ReportError(..),
-  logErrorReport,
-  reportErrorWith,
-  reportError,
-  reportErrorOr,
-  reportErrorOr_,
-  printAllErrors,
-) where
+module Ribosome.Error.Report where
 
-import Control.Monad.IO.Class (liftIO)
+import Control.Monad ((<=<))
+import Control.Monad.Error.Class (MonadError)
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Trans.Except (runExceptT)
 import Data.Foldable (traverse_)
+import Data.Functor (void)
 import qualified Data.Map as Map (alter)
-import Data.Text.Prettyprint.Doc (pretty, line, (<>))
+import Data.Text.Prettyprint.Doc (line, pretty, (<>))
 import Data.Text.Prettyprint.Doc.Render.Terminal (putDoc)
-import System.Log.Logger (logM, Priority(NOTICE, DEBUG))
+import System.Log (Priority(NOTICE))
 
 import Ribosome.Api.Echo (echom)
-import Ribosome.Control.Monad.Ribo (Ribo)
-import qualified Ribosome.Control.Ribo as Ribo (name, modifyErrors, getErrors)
+import Ribosome.Control.Monad.Ribo (MonadRibo, Nvim, NvimE, RNeovim, Ribo, runRibo)
+import qualified Ribosome.Control.Monad.Ribo as Ribo (getErrors, modifyErrors, pluginName)
 import Ribosome.Data.ErrorReport (ErrorReport(ErrorReport))
-import Ribosome.Data.Errors (Errors(Errors), ComponentName(ComponentName), Error(Error))
-import Ribosome.Data.Time (epochSeconds)
-
-class ReportError a where
-  errorReport :: a -> ErrorReport
-
-instance ReportError [Char] where
-  errorReport msg = ErrorReport msg [msg] NOTICE
-
-instance ReportError [[Char]] where
-  errorReport (msg:extra) = ErrorReport msg (msg:extra) NOTICE
-  errorReport [] = ErrorReport "empty error" ["empty error"] DEBUG
+import Ribosome.Data.Errors (ComponentName(ComponentName), Error(Error), Errors(Errors))
+import Ribosome.Error.Report.Class (ReportError(..))
+import Ribosome.Log (logAs)
+import Ribosome.Nvim.Api.RpcCall (RpcError)
+import Ribosome.System.Time (epochSeconds)
 
-storeError' :: Int -> String -> ErrorReport -> Errors -> Errors
+storeError' :: Int -> Text -> ErrorReport -> Errors -> Errors
 storeError' time name report (Errors errors) =
   Errors (Map.alter alter (ComponentName name) errors)
   where
@@ -41,37 +29,96 @@
     alter Nothing = Just [err]
     alter (Just current) = Just (err:current)
 
-storeError :: String -> ErrorReport -> Ribo d ()
+storeError :: (MonadRibo m, MonadIO m) => Text -> ErrorReport -> m ()
 storeError name e = do
   time <- epochSeconds
   Ribo.modifyErrors $ storeError' time name e
 
-logErrorReport :: ErrorReport -> Ribo d ()
+logErrorReport ::
+  (MonadRibo m, NvimE e m, MonadIO m) =>
+  ErrorReport ->
+  m ()
 logErrorReport (ErrorReport user logMsgs prio) = do
-  name <- Ribo.name
-  liftIO $ traverse_ (logM name prio) logMsgs
-  echom user
+  name <- Ribo.pluginName
+  liftIO $ traverse_ (logAs prio name) logMsgs
+  when (prio >= NOTICE) (echom user)
 
-reportErrorWith :: String -> (a -> ErrorReport) -> a -> Ribo d ()
-reportErrorWith name cons err = do
+processErrorReport ::
+  (MonadRibo m, NvimE e m, MonadIO m) =>
+  Text ->
+  ErrorReport ->
+  m ()
+processErrorReport name report = do
   storeError name report
   logErrorReport report
-  where
-    report = cons err
 
-reportError :: ReportError a => String -> a -> Ribo d ()
+processErrorReport' ::
+  (MonadRibo m, Nvim m, MonadIO m) =>
+  Text ->
+  ErrorReport ->
+  m ()
+processErrorReport' name =
+  void . runExceptT @RpcError . processErrorReport name
+
+reportErrorWith ::
+  (MonadRibo m, NvimE e m, MonadIO m) =>
+  Text ->
+  (a -> ErrorReport) ->
+  a ->
+  m ()
+reportErrorWith name cons err =
+  processErrorReport name (cons err)
+
+reportError ::
+  MonadRibo m =>
+  NvimE e m =>
+  MonadIO m =>
+  ReportError a =>
+  Text ->
+  a ->
+  m ()
 reportError name =
   reportErrorWith name errorReport
 
-reportErrorOr :: ReportError e => String -> (a -> Ribo d ()) -> Either e a -> Ribo d ()
+reportErrorOr ::
+  (MonadRibo m, NvimE e m, MonadIO m, ReportError e) =>
+  Text ->
+  (a -> m ()) ->
+  Either e a ->
+  m ()
 reportErrorOr name =
   either $ reportError name
 
-reportErrorOr_ :: ReportError e => String -> Ribo d () -> Either e a -> Ribo d ()
+reportErrorOr_ ::
+  (MonadError RpcError m, MonadRibo m, NvimE e m, MonadIO m, ReportError e) =>
+  Text ->
+  m () ->
+  Either e a ->
+  m ()
 reportErrorOr_ name =
   reportErrorOr name . const
 
-printAllErrors :: Ribo e ()
+reportError' ::
+  ∀ e m a .
+  (MonadRibo m, Nvim m, MonadIO m, ReportError e) =>
+  Text ->
+  Either e a ->
+  m ()
+reportError' _ (Right _) =
+  return ()
+reportError' componentName (Left e) =
+  void $ runExceptT @RpcError $ reportError componentName e
+
+printAllErrors :: (MonadRibo m, NvimE e m, MonadIO m) => m ()
 printAllErrors = do
   errors <- Ribo.getErrors
-  liftIO $ putDoc $ (pretty errors <> line)
+  liftIO $ putDoc (pretty errors <> line)
+
+runRiboReport ::
+  ∀ e s.
+  ReportError e =>
+  Text ->
+  Ribo s e () ->
+  RNeovim s ()
+runRiboReport componentName =
+  reportError' componentName <=< runRibo
diff --git a/lib/Ribosome/Error/Report/Class.hs b/lib/Ribosome/Error/Report/Class.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Error/Report/Class.hs
@@ -0,0 +1,59 @@
+module Ribosome.Error.Report.Class where
+
+import qualified Data.Text as Text (pack)
+import GHC.Generics (
+  Generic,
+  K1(..),
+  M1(..),
+  Rep,
+  from,
+  (:+:)(..),
+  )
+import System.Log.Logger (Priority(NOTICE, DEBUG))
+
+import Ribosome.Data.ErrorReport (ErrorReport(ErrorReport))
+
+class ReportError a where
+  errorReport :: a -> ErrorReport
+  default errorReport ::
+    Generic a =>
+    GenReportError (Rep a) =>
+    a ->
+    ErrorReport
+  errorReport =
+    genErrorReport . from
+
+class GenReportError f where
+  genErrorReport :: f a -> ErrorReport
+
+instance GenReportError f => GenReportError (M1 i c f) where
+  genErrorReport =
+    genErrorReport . unM1
+
+instance (GenReportError f, GenReportError g) => GenReportError (f :+: g) where
+  genErrorReport (L1 a) =
+    genErrorReport a
+  genErrorReport (R1 a) =
+    genErrorReport a
+
+instance ReportError a => GenReportError (K1 i a) where
+  genErrorReport =
+    errorReport . unK1
+
+instance ReportError [Char] where
+  errorReport msg = ErrorReport (Text.pack msg) (Text.pack <$> [msg]) NOTICE
+
+instance ReportError [[Char]] where
+  errorReport (msg : extra) = ErrorReport (Text.pack msg) (Text.pack <$> (msg : extra)) NOTICE
+  errorReport [] = ErrorReport "empty error" ["empty error"] DEBUG
+
+instance ReportError Text where
+  errorReport msg = ErrorReport msg [msg] NOTICE
+
+instance ReportError [Text] where
+  errorReport (msg : extra) =
+    ErrorReport msg (msg : extra) NOTICE
+  errorReport [] = ErrorReport "empty error" ["empty error"] DEBUG
+
+instance ReportError () where
+  errorReport _ = ErrorReport "" [] DEBUG
diff --git a/lib/Ribosome/File.hs b/lib/Ribosome/File.hs
--- a/lib/Ribosome/File.hs
+++ b/lib/Ribosome/File.hs
@@ -6,7 +6,7 @@
 
 import Control.Monad.IO.Class (MonadIO, liftIO)
 import Data.String.Utils (replace)
-import System.Directory (getHomeDirectory, canonicalizePath)
+import System.Directory (canonicalizePath, getHomeDirectory)
 
 canonicalPathWithHome :: FilePath -> FilePath -> FilePath
 canonicalPathWithHome = replace "~"
diff --git a/lib/Ribosome/Internal/IO.hs b/lib/Ribosome/Internal/IO.hs
--- a/lib/Ribosome/Internal/IO.hs
+++ b/lib/Ribosome/Internal/IO.hs
@@ -3,14 +3,13 @@
   forkNeovim,
 ) where
 
-import GHC.Conc.Sync (forkIO)
+import Control.Concurrent (forkIO)
 import Control.Monad.IO.Class (liftIO)
 import Control.Monad.Reader (ask, runReaderT, withReaderT)
 import Control.Monad.Trans.Resource (runResourceT)
 import Data.Functor (void)
-import Neovim.Context.Internal (Neovim(..), Config(..), retypeConfig, runNeovim)
+import Neovim.Context.Internal (Config(..), Neovim(..), retypeConfig, runNeovim)
 
--- try using Contravariant with this
 retypeNeovim :: (e0 -> e1) -> Neovim e1 a -> Neovim e0 a
 retypeNeovim transform thunk = do
   env <- Neovim ask
diff --git a/lib/Ribosome/Internal/NvimObject.hs b/lib/Ribosome/Internal/NvimObject.hs
--- a/lib/Ribosome/Internal/NvimObject.hs
+++ b/lib/Ribosome/Internal/NvimObject.hs
@@ -1,27 +1,22 @@
-module Ribosome.Internal.NvimObject(
-  deriveString,
-  extractObject,
-  extractObjectOr,
-) where
+module Ribosome.Internal.NvimObject where
 
-import qualified Data.ByteString.UTF8 as UTF8 (fromString)
-import Data.Map.Strict (Map, (!?))
-import Data.Text.Prettyprint.Doc ((<+>), pretty)
-import Neovim (Object(ObjectString), Doc, AnsiStyle, fromObject, NvimObject)
+import Data.Map (Map, (!?))
+import Data.Text.Prettyprint.Doc (pretty, (<+>))
+import Neovim (AnsiStyle, Doc, NvimObject, Object(ObjectString), fromObject)
 
-deriveString :: (String -> a) -> Object -> Either (Doc AnsiStyle) a
-deriveString cons o = fmap cons (fromObject o :: Either (Doc AnsiStyle) String)
+deriveString :: (Text -> a) -> Object -> Either (Doc AnsiStyle) a
+deriveString cons o = fmap cons (fromObject o :: Either (Doc AnsiStyle) Text)
 
-objectKeyMissing :: String -> Maybe Object -> Either (Doc AnsiStyle) Object
+objectKeyMissing :: Text -> Maybe Object -> Either (Doc AnsiStyle) Object
 objectKeyMissing _ (Just o) = Right o
-objectKeyMissing key Nothing = Left (pretty ("missing key in nvim data:" :: String) <+> pretty key)
+objectKeyMissing key Nothing = Left (pretty ("missing key in nvim data:" :: Text) <+> pretty key)
 
-extractObject :: NvimObject o => String -> Map Object Object -> Either (Doc AnsiStyle) o
+extractObject :: NvimObject o => Text -> Map Object Object -> Either (Doc AnsiStyle) o
 extractObject key data' = do
-  value <- objectKeyMissing key $ data' !? (ObjectString . UTF8.fromString) key
+  value <- objectKeyMissing key $ data' !? (ObjectString . encodeUtf8) key
   fromObject value
 
-extractObjectOr :: NvimObject o => String -> o -> Map Object Object -> o
+extractObjectOr :: NvimObject o => Text -> o -> Map Object Object -> o
 extractObjectOr key default' data' =
   case extractObject key data' of
     Right a -> a
diff --git a/lib/Ribosome/Log.hs b/lib/Ribosome/Log.hs
--- a/lib/Ribosome/Log.hs
+++ b/lib/Ribosome/Log.hs
@@ -1,35 +1,150 @@
-module Ribosome.Log(
-  debug,
-  info,
-  err,
-  p,
-  prefixed,
-  debugR,
-) where
+module Ribosome.Log where
 
 import Control.Monad.IO.Class (MonadIO, liftIO)
-import Neovim (ask)
-import Neovim.Log (debugM, infoM, errorM)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString.UTF8 as ByteString (toString)
+import Data.Foldable (traverse_)
+import Data.Text (Text)
+import qualified Data.Text as Text (unpack)
+import System.Log.Logger (Priority(DEBUG, ERROR, INFO), logM)
 
-import Ribosome.Control.Ribo (Ribo)
-import qualified Ribosome.Control.Ribosome as R (name)
+import Ribosome.Control.Monad.Ribo (MonadRibo, pluginName)
 
-debug :: (MonadIO m, Show a) => String -> a -> m ()
-debug name message = liftIO $ debugM name $ show message
+class Loggable a where
+  logLines :: a -> [String]
 
-info :: (MonadIO m, Show a) => String -> a -> m ()
-info name message = liftIO $ infoM name $ show message
+instance {-# OVERLAPPABLE #-} Loggable a => Loggable [a] where
+  logLines =
+    (>>= logLines)
 
-err :: (MonadIO m, Show a) => String -> a -> m ()
-err name message = liftIO $ errorM name $ show message
+instance {-# OVERLAPPING #-} Loggable String where
+  logLines = pure
 
-p :: (MonadIO m, Show a) => a -> m ()
-p = liftIO . print
+instance Loggable ByteString where
+  logLines = pure . ByteString.toString
 
-prefixed :: (MonadIO m, Show a) => String -> a -> m ()
-prefixed prefix a = liftIO $ putStrLn $ prefix ++ ": " ++ show a
+instance Loggable Text where
+  logLines = pure . Text.unpack
 
-debugR :: String -> Ribo e ()
-debugR a = do
-  pluginName <- R.name <$> ask
-  liftIO $ debugM pluginName a
+logAs ::
+  Loggable a =>
+  MonadIO m =>
+  Priority ->
+  Text ->
+  a ->
+  m ()
+logAs prio name = liftIO . traverse_ (logM (toString name) prio) . logLines
+
+debugAs ::
+  Loggable a =>
+  MonadIO m =>
+  Text ->
+  a ->
+  m ()
+debugAs =
+  logAs DEBUG
+
+infoAs ::
+  Loggable a =>
+  MonadIO m =>
+  Text ->
+  a ->
+  m ()
+infoAs =
+  logAs INFO
+
+errAs ::
+  Loggable a =>
+  MonadIO m =>
+  Text ->
+  a ->
+  m ()
+errAs =
+  logAs ERROR
+
+prefixed :: (MonadIO m, Show a) => Text -> a -> m ()
+prefixed prefix a = liftIO $ putStrLn $ prefix <> ": " <> show a
+
+logR ::
+  Loggable a =>
+  MonadRibo m =>
+  Priority ->
+  a ->
+  m ()
+logR prio message = do
+  n <- pluginName
+  logAs prio n message
+
+debug ::
+  Loggable a =>
+  MonadRibo m =>
+  a ->
+  m ()
+debug =
+  logR DEBUG
+
+logDebug ::
+  Loggable a =>
+  MonadRibo m =>
+  a ->
+  m ()
+logDebug = debug
+
+showDebug ::
+  Show a =>
+  MonadRibo m =>
+  Text ->
+  a ->
+  m ()
+showDebug prefix a =
+  logDebug @Text (prefix <> " " <> show a)
+
+showDebugM ::
+  Show a =>
+  MonadRibo m =>
+  Text ->
+  m a ->
+  m a
+showDebugM prefix ma = do
+  a <- ma
+  logDebug @Text (prefix <> " " <> show a)
+  return a
+
+info ::
+  Loggable a =>
+  MonadRibo m =>
+  a ->
+  m ()
+info =
+  logR INFO
+
+logInfo ::
+  Loggable a =>
+  MonadRibo m =>
+  a ->
+  m ()
+logInfo = info
+
+err ::
+  Loggable a =>
+  MonadRibo m =>
+  a ->
+  m ()
+err =
+  logR ERROR
+
+logError ::
+  Loggable a =>
+  MonadRibo m =>
+  a ->
+  m ()
+logError = err
+
+showError ::
+  Show a =>
+  MonadRibo m =>
+  Text ->
+  a ->
+  m ()
+showError prefix a =
+  logError @Text (prefix <> " " <> show a)
diff --git a/lib/Ribosome/Mapping.hs b/lib/Ribosome/Mapping.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Mapping.hs
@@ -0,0 +1,29 @@
+module Ribosome.Mapping where
+
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE, pluginName)
+import Ribosome.Data.Mapping (Mapping(Mapping), MappingIdent(MappingIdent))
+import Ribosome.Data.Text (capitalize)
+import Ribosome.Nvim.Api.Data (Buffer)
+import Ribosome.Nvim.Api.IO (vimCommand)
+
+activateMapping :: Mapping -> m ()
+activateMapping _ =
+  undefined
+
+mapCommand :: Text -> Bool -> Text
+mapCommand mode remap =
+  mode <> (if remap then "map" else "noremap")
+
+activateBufferMapping ::
+  MonadRibo m =>
+  NvimE e m =>
+  Buffer ->
+  Mapping ->
+  m ()
+activateBufferMapping _ (Mapping (MappingIdent ident) lhs mode remap _) = do
+  name <- pluginName
+  vimCommand (unwords (cmdline name))
+  where
+    cmdline name = [cmd, "<buffer>", lhs, ":call", func name <> "('" <> ident <> "')<cr>"]
+    cmd = mapCommand mode remap
+    func name = capitalize name <> "Mapping"
diff --git a/lib/Ribosome/Menu/Data/Menu.hs b/lib/Ribosome/Menu/Data/Menu.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Data/Menu.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Menu.Data.Menu where
+
+import Ribosome.Menu.Data.MenuItem (MenuItem)
+
+newtype MenuFilter =
+  MenuFilter Text
+  deriving (Eq, Show)
+
+instance Default MenuFilter where
+  def = MenuFilter ""
+
+data Menu =
+  Menu {
+    _items :: [MenuItem],
+    _filtered :: [MenuItem],
+    _stack :: [(Text, [MenuItem])],
+    _selected :: Int,
+    _currentFilter :: MenuFilter
+  }
+  deriving (Eq, Show, Generic, Default)
+
+deepLenses ''Menu
diff --git a/lib/Ribosome/Menu/Data/MenuAction.hs b/lib/Ribosome/Menu/Data/MenuAction.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Data/MenuAction.hs
@@ -0,0 +1,15 @@
+module Ribosome.Menu.Data.MenuAction where
+
+import Ribosome.Menu.Data.MenuEvent (QuitReason)
+
+data MenuAction m a =
+  Quit (QuitReason m a)
+  |
+  Continue
+  -- |
+  -- Cursor Int
+  |
+  Render Bool
+  -- |
+  -- Submenu Text
+  deriving Show
diff --git a/lib/Ribosome/Menu/Data/MenuConfig.hs b/lib/Ribosome/Menu/Data/MenuConfig.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Data/MenuConfig.hs
@@ -0,0 +1,16 @@
+module Ribosome.Menu.Data.MenuConfig where
+
+import Conduit (ConduitT)
+
+import Ribosome.Menu.Data.MenuConsumer (MenuConsumer)
+import Ribosome.Menu.Data.MenuItem (MenuItem)
+import Ribosome.Menu.Data.MenuRenderEvent (MenuRenderEvent)
+import Ribosome.Menu.Prompt.Data.PromptConfig (PromptConfig)
+
+data MenuConfig m a =
+  MenuConfig {
+    _items :: ConduitT () MenuItem m (),
+    _handle :: MenuConsumer m a,
+    _render :: MenuRenderEvent m a -> m (),
+    _prompt :: PromptConfig m
+  }
diff --git a/lib/Ribosome/Menu/Data/MenuConsumer.hs b/lib/Ribosome/Menu/Data/MenuConsumer.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Data/MenuConsumer.hs
@@ -0,0 +1,8 @@
+module Ribosome.Menu.Data.MenuConsumer where
+
+import Ribosome.Menu.Data.Menu (Menu)
+import Ribosome.Menu.Data.MenuAction (MenuAction)
+import Ribosome.Menu.Data.MenuUpdate (MenuUpdate)
+
+newtype MenuConsumer m a =
+  MenuConsumer { unMenuConsumer :: MenuUpdate m a -> m (MenuAction m a, Menu) }
diff --git a/lib/Ribosome/Menu/Data/MenuConsumerAction.hs b/lib/Ribosome/Menu/Data/MenuConsumerAction.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Data/MenuConsumerAction.hs
@@ -0,0 +1,12 @@
+module Ribosome.Menu.Data.MenuConsumerAction where
+
+data MenuConsumerAction m a =
+  Quit
+  |
+  QuitWith (m a)
+  |
+  Continue
+  |
+  Render Bool
+  |
+  Return a
diff --git a/lib/Ribosome/Menu/Data/MenuConsumerUpdate.hs b/lib/Ribosome/Menu/Data/MenuConsumerUpdate.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Data/MenuConsumerUpdate.hs
@@ -0,0 +1,9 @@
+module Ribosome.Menu.Data.MenuConsumerUpdate where
+
+import Ribosome.Menu.Data.MenuUpdate (MenuUpdate)
+
+data MenuConsumerUpdate m a =
+  MenuConsumerUpdate {
+    _changed :: Bool,
+    _menu :: MenuUpdate m a
+  }
diff --git a/lib/Ribosome/Menu/Data/MenuEvent.hs b/lib/Ribosome/Menu/Data/MenuEvent.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Data/MenuEvent.hs
@@ -0,0 +1,41 @@
+module Ribosome.Menu.Data.MenuEvent where
+
+import qualified Text.Show
+
+import Ribosome.Menu.Data.MenuItem (MenuItem)
+import Ribosome.Menu.Prompt.Data.Prompt (Prompt)
+
+data QuitReason m a =
+  Aborted
+  |
+  PromptError Text
+  |
+  NoOutput
+  |
+  Return a
+  |
+  Execute (m a)
+
+instance Show (QuitReason m a) where
+  show Aborted =
+    "Aborted"
+  show (PromptError err) =
+    "PromptError(" <> toString err <> ")"
+  show NoOutput =
+    "NoOutput"
+  show (Return _) =
+    "Return"
+  show (Execute _) =
+    "Execute"
+
+data MenuEvent m a =
+  Init Prompt
+  |
+  PromptChange Text Prompt
+  |
+  Mapping Text Prompt
+  |
+  NewItems MenuItem
+  |
+  Quit (QuitReason m a)
+  deriving Show
diff --git a/lib/Ribosome/Menu/Data/MenuItem.hs b/lib/Ribosome/Menu/Data/MenuItem.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Data/MenuItem.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Menu.Data.MenuItem where
+
+import Control.Lens (makeClassy)
+
+data MenuItem =
+  MenuItem {
+    _ident :: Text,
+    _text :: Text
+  }
+  deriving (Eq, Show)
+
+makeClassy ''MenuItem
diff --git a/lib/Ribosome/Menu/Data/MenuRenderEvent.hs b/lib/Ribosome/Menu/Data/MenuRenderEvent.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Data/MenuRenderEvent.hs
@@ -0,0 +1,10 @@
+module Ribosome.Menu.Data.MenuRenderEvent where
+
+import Ribosome.Menu.Data.Menu (Menu)
+import Ribosome.Menu.Data.MenuEvent (QuitReason)
+
+data MenuRenderEvent m a =
+  Render Bool Menu
+  |
+  Quit (QuitReason m a)
+  deriving Show
diff --git a/lib/Ribosome/Menu/Data/MenuResult.hs b/lib/Ribosome/Menu/Data/MenuResult.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Data/MenuResult.hs
@@ -0,0 +1,13 @@
+module Ribosome.Menu.Data.MenuResult where
+
+data MenuResult a =
+  NoOutput
+  |
+  Error Text
+  |
+  Executed
+  |
+  Aborted
+  |
+  Return a
+  deriving (Eq, Show)
diff --git a/lib/Ribosome/Menu/Data/MenuUpdate.hs b/lib/Ribosome/Menu/Data/MenuUpdate.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Data/MenuUpdate.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Menu.Data.MenuUpdate where
+
+import Control.Lens (makeClassy)
+
+import Ribosome.Menu.Data.Menu (Menu)
+import Ribosome.Menu.Data.MenuEvent (MenuEvent)
+
+data MenuUpdate m a =
+  MenuUpdate {
+    _event :: MenuEvent m a,
+    _menu :: Menu
+  }
+
+makeClassy ''MenuUpdate
diff --git a/lib/Ribosome/Menu/Nvim.hs b/lib/Ribosome/Menu/Nvim.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Nvim.hs
@@ -0,0 +1,37 @@
+module Ribosome.Menu.Nvim where
+
+import Ribosome.Api.Window (redraw, setLine)
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE)
+import Ribosome.Data.Scratch (Scratch(scratchWindow))
+import Ribosome.Data.ScratchOptions (ScratchOptions)
+import Ribosome.Log (logDebug)
+import Ribosome.Menu.Data.Menu (Menu(Menu))
+import qualified Ribosome.Menu.Data.MenuItem as MenuItem (MenuItem(_text))
+import Ribosome.Menu.Data.MenuRenderEvent (MenuRenderEvent)
+import qualified Ribosome.Menu.Data.MenuRenderEvent as MenuRenderEvent (MenuRenderEvent(..))
+import Ribosome.Scratch (killScratch, setScratchContent)
+
+renderNvimMenu ::
+  MonadRibo m =>
+  NvimE e m =>
+  ScratchOptions ->
+  Scratch ->
+  MenuRenderEvent m a ->
+  m ()
+renderNvimMenu _ scratch (MenuRenderEvent.Quit _) =
+  killScratch scratch
+renderNvimMenu options scratch (MenuRenderEvent.Render changed (Menu _ allItems _ selected _)) = do
+  when changed $ setScratchContent options scratch (reverse text)
+  updateCursor
+  redraw
+  where
+    lineNumber =
+      max 0 $ length items - selected - 1
+    text = MenuItem._text <$> items
+    items = take maxItems (reverse allItems)
+    maxItems = 100
+    updateCursor = do
+      logDebug @Text logMsg
+      setLine (scratchWindow scratch) lineNumber
+    logMsg =
+      "updating menu cursor to line " <> show lineNumber <> "; " <> show selected <> "/" <> show (length items)
diff --git a/lib/Ribosome/Menu/Prompt/Data/Codes.hs b/lib/Ribosome/Menu/Prompt/Data/Codes.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/Codes.hs
@@ -0,0 +1,121 @@
+module Ribosome.Menu.Prompt.Data.Codes where
+
+import Control.Exception.Lifted (try)
+import Data.Map (Map, (!?))
+import qualified Data.Map as Map (fromList)
+import qualified Data.Text as Text (singleton)
+
+specialCodes :: Map Text Text
+specialCodes =
+  Map.fromList [
+    ("\x80\xffX", "c-@"),
+    ("\65533kb", "bs"),
+    ("\x80kB", "s-ta"),
+    ("\x0", "c-k"),
+    ("\x80kD", "del"),
+    ("\x9B", "csi"),
+    ("\x80\xfdP", "xcsi"),
+    ("\x80ku", "up"),
+    ("\x80kd", "down"),
+    ("\x80kl", "left"),
+    ("\x80kr", "right"),
+    -- ("\x80\xfd", "s-up"),
+    -- ("\x80\xfd", "s-down"),
+    ("\x80#4", "s-left"),
+    ("\x80%i", "s-right"),
+    ("\x80\xfdT", "c-left"),
+    ("\x80\xfdU", "c-right"),
+    ("\x80k1", "f1"),
+    ("\x80k2", "f2"),
+    ("\x80k3", "f3"),
+    ("\x80k4", "f4"),
+    ("\x80k5", "f5"),
+    ("\x80k6", "f6"),
+    ("\x80k7", "f7"),
+    ("\x80k8", "f8"),
+    ("\x80k9", "f9"),
+    ("\x80k;", "f10"),
+    ("\x80F1", "f11"),
+    ("\x80F2", "f12"),
+    ("\x80\xfd\x06", "s-f1"),
+    ("\x80\xfd\x07", "s-f2"),
+    ("\x80\xfd\x08", "s-f3"),
+    ("\x80\xfd\x09", "s-f4"),
+    ("\x80\xfd\x0A", "s-f5"),
+    ("\x80\xfd\x0B", "s-f6"),
+    ("\x80\xfd\x0C", "s-f7"),
+    ("\x80\xfd\x0D", "s-f8"),
+    ("\x80\xfd\x0E", "s-f9"),
+    ("\x80\xfd\x0F", "s-f10"),
+    ("\x80\xfd\x10", "s-f11"),
+    ("\x80\xfd\x11", "s-f12"),
+    ("\x80%1", "help"),
+    ("\x80&8", "undo"),
+    ("\x80kI", "insert"),
+    ("\x80kh", "home"),
+    ("\x80@7", "end"),
+    ("\x80kP", "pageup"),
+    ("\x80kN", "pagedown"),
+    ("\x80K1", "khome"),
+    ("\x80K4", "kend"),
+    ("\x80K3", "kpageup"),
+    ("\x80K5", "kpagedown"),
+    ("\x80K6", "kplus"),
+    ("\x80K7", "kminus"),
+    ("\x80K9", "kmultiply"),
+    ("\x80K8", "kdivide"),
+    ("\x80KA", "kenter"),
+    ("\x80KB", "kpoint"),
+    ("\x80KC", "k0"),
+    ("\x80KD", "k1"),
+    ("\x80KE", "k2"),
+    ("\x80KF", "k3"),
+    ("\x80KG", "k4"),
+    ("\x80KH", "k5"),
+    ("\x80KI", "k6"),
+    ("\x80KJ", "k7"),
+    ("\x80KK", "k8"),
+    ("\x80KL", "k9")
+    ]
+
+specialNumCodes :: Map Int Text
+specialNumCodes =
+  Map.fromList [
+    (9, "ta"),
+    (10, "c-j"),
+    (11, "c-k"),
+    (12, "fe"),
+    (13, "cr"),
+    (27, "esc"),
+    (32, "space"),
+    (60, "lt"),
+    (92, "bslash"),
+    (124, "bar")
+    ]
+
+modifierCodes :: [(Int, Text)]
+modifierCodes =
+  [
+    (2, "shift"),
+    (4, "control"),
+    (8, "alt"),
+    (16, "meta"),
+    (32, "mouse_double"),
+    (64, "mouse_triple"),
+    (96, "mouse_quadruple"),
+    (128, "command")
+    ]
+
+decodeInputChar :: Text -> Maybe Text
+decodeInputChar =
+  (specialCodes !?)
+
+decodeInputNum ::
+  MonadBaseControl IO m =>
+  Int ->
+  m (Maybe Text)
+decodeInputNum a =
+  maybe codepoint (return . Just) (specialNumCodes !? a)
+  where
+    codepoint =
+      fmap Text.singleton . rightToMaybe @SomeException <$> try (return $ chr a)
diff --git a/lib/Ribosome/Menu/Prompt/Data/CursorUpdate.hs b/lib/Ribosome/Menu/Prompt/Data/CursorUpdate.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/CursorUpdate.hs
@@ -0,0 +1,15 @@
+module Ribosome.Menu.Prompt.Data.CursorUpdate where
+
+data CursorUpdate =
+  Unmodified
+  |
+  OneLeft
+  |
+  OneRight
+  |
+  Append
+  |
+  Prepend
+  |
+  Index Int
+  deriving (Eq, Show)
diff --git a/lib/Ribosome/Menu/Prompt/Data/InputEvent.hs b/lib/Ribosome/Menu/Prompt/Data/InputEvent.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/InputEvent.hs
@@ -0,0 +1,13 @@
+module Ribosome.Menu.Prompt.Data.InputEvent where
+
+data InputEvent =
+  Character Text
+  |
+  NoInput
+  |
+  Unexpected Int
+  |
+  Interrupt
+  |
+  Error Text
+  deriving (Eq, Show)
diff --git a/lib/Ribosome/Menu/Prompt/Data/Prompt.hs b/lib/Ribosome/Menu/Prompt/Data/Prompt.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/Prompt.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Menu.Prompt.Data.Prompt where
+
+import Ribosome.Menu.Prompt.Data.PromptState (PromptState)
+
+data Prompt =
+  Prompt {
+     _cursor :: Int,
+     _state :: PromptState,
+     _text :: Text
+  }
+  deriving (Eq, Show)
+
+deepLenses ''Prompt
diff --git a/lib/Ribosome/Menu/Prompt/Data/PromptConfig.hs b/lib/Ribosome/Menu/Prompt/Data/PromptConfig.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/PromptConfig.hs
@@ -0,0 +1,16 @@
+module Ribosome.Menu.Prompt.Data.PromptConfig where
+
+import Conduit (ConduitT)
+
+import Ribosome.Menu.Prompt.Data.PromptEvent (PromptEvent)
+import Ribosome.Menu.Prompt.Data.PromptRenderer (PromptRenderer)
+import Ribosome.Menu.Prompt.Data.PromptState (PromptState)
+import Ribosome.Menu.Prompt.Data.PromptUpdate (PromptUpdate)
+
+data PromptConfig m =
+  PromptConfig {
+    _source :: ConduitT () PromptEvent m (),
+    _modes :: PromptEvent -> PromptState -> m PromptUpdate,
+    _render :: PromptRenderer m,
+    _insert :: Bool
+  }
diff --git a/lib/Ribosome/Menu/Prompt/Data/PromptConsumed.hs b/lib/Ribosome/Menu/Prompt/Data/PromptConsumed.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/PromptConsumed.hs
@@ -0,0 +1,7 @@
+module Ribosome.Menu.Prompt.Data.PromptConsumed where
+
+data PromptConsumed =
+  Yes
+  |
+  No
+  deriving (Eq, Show)
diff --git a/lib/Ribosome/Menu/Prompt/Data/PromptConsumerUpdate.hs b/lib/Ribosome/Menu/Prompt/Data/PromptConsumerUpdate.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/PromptConsumerUpdate.hs
@@ -0,0 +1,13 @@
+module Ribosome.Menu.Prompt.Data.PromptConsumerUpdate where
+
+import Ribosome.Menu.Prompt.Data.Prompt (Prompt)
+import Ribosome.Menu.Prompt.Data.PromptConsumed (PromptConsumed)
+import Ribosome.Menu.Prompt.Data.PromptEvent (PromptEvent)
+
+data PromptConsumerUpdate =
+  PromptConsumerUpdate {
+    _event :: PromptEvent,
+    _prompt :: Prompt,
+    _consumed :: PromptConsumed
+  }
+  deriving (Eq, Show)
diff --git a/lib/Ribosome/Menu/Prompt/Data/PromptEvent.hs b/lib/Ribosome/Menu/Prompt/Data/PromptEvent.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/PromptEvent.hs
@@ -0,0 +1,15 @@
+module Ribosome.Menu.Prompt.Data.PromptEvent where
+
+data PromptEvent =
+  Init
+  |
+  Character Text
+  |
+  -- SpecialCharacter Text
+  -- |
+  Unexpected Int
+  |
+  Interrupt
+  |
+  Error Text
+  deriving (Eq, Show)
diff --git a/lib/Ribosome/Menu/Prompt/Data/PromptRenderer.hs b/lib/Ribosome/Menu/Prompt/Data/PromptRenderer.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/PromptRenderer.hs
@@ -0,0 +1,10 @@
+module Ribosome.Menu.Prompt.Data.PromptRenderer where
+
+import Ribosome.Menu.Prompt.Data.Prompt (Prompt)
+
+data PromptRenderer m =
+  ∀ a. PromptRenderer {
+    _acquire :: m a,
+    _release :: a -> m (),
+    _render :: Prompt -> m ()
+  }
diff --git a/lib/Ribosome/Menu/Prompt/Data/PromptState.hs b/lib/Ribosome/Menu/Prompt/Data/PromptState.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/PromptState.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Menu.Prompt.Data.PromptState where
+
+data PromptState =
+  Insert
+  |
+  Normal
+  |
+  Quit
+  deriving (Eq, Show)
+
+deepLenses ''PromptState
diff --git a/lib/Ribosome/Menu/Prompt/Data/PromptUpdate.hs b/lib/Ribosome/Menu/Prompt/Data/PromptUpdate.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/PromptUpdate.hs
@@ -0,0 +1,15 @@
+module Ribosome.Menu.Prompt.Data.PromptUpdate where
+
+import Ribosome.Menu.Prompt.Data.CursorUpdate (CursorUpdate)
+import Ribosome.Menu.Prompt.Data.PromptConsumed (PromptConsumed)
+import Ribosome.Menu.Prompt.Data.PromptState (PromptState)
+import Ribosome.Menu.Prompt.Data.TextUpdate (TextUpdate)
+
+data PromptUpdate =
+  PromptUpdate {
+    _state :: PromptState,
+    _cursor :: CursorUpdate,
+    _text :: TextUpdate,
+    _consumed :: PromptConsumed
+  }
+  deriving (Eq, Show)
diff --git a/lib/Ribosome/Menu/Prompt/Data/TextUpdate.hs b/lib/Ribosome/Menu/Prompt/Data/TextUpdate.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Data/TextUpdate.hs
@@ -0,0 +1,11 @@
+module Ribosome.Menu.Prompt.Data.TextUpdate where
+
+data TextUpdate =
+  Unmodified
+  |
+  Insert Text
+  |
+  DeleteLeft
+  |
+  DeleteRight
+  deriving (Eq, Show)
diff --git a/lib/Ribosome/Menu/Prompt/Nvim.hs b/lib/Ribosome/Menu/Prompt/Nvim.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Nvim.hs
@@ -0,0 +1,192 @@
+module Ribosome.Menu.Prompt.Nvim where
+
+import Conduit (ConduitT, yield)
+import Control.Exception.Lifted (bracket_)
+import Control.Monad.DeepError (ignoreError)
+import qualified Data.Text as Text (singleton, splitAt, uncons)
+
+import Ribosome.Api.Atomic (atomic)
+import Ribosome.Api.Function (defineFunction)
+import Ribosome.Api.Variable (setVar)
+import Ribosome.Api.Window (redraw)
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE)
+import Ribosome.Data.Text (escapeQuotes)
+import Ribosome.Menu.Prompt.Data.Codes (decodeInputChar, decodeInputNum)
+import Ribosome.Menu.Prompt.Data.InputEvent (InputEvent)
+import qualified Ribosome.Menu.Prompt.Data.InputEvent as InputEvent (InputEvent(..))
+import Ribosome.Menu.Prompt.Data.Prompt (Prompt(Prompt))
+import Ribosome.Menu.Prompt.Data.PromptEvent (PromptEvent)
+import qualified Ribosome.Menu.Prompt.Data.PromptEvent as PromptEvent (PromptEvent(..))
+import Ribosome.Menu.Prompt.Data.PromptRenderer (PromptRenderer(PromptRenderer))
+import Ribosome.Msgpack.Encode (toMsgpack)
+import Ribosome.Msgpack.Error (DecodeError)
+import qualified Ribosome.Nvim.Api.Data as ApiData (vimCommand)
+import Ribosome.Nvim.Api.IO (vimCallFunction, vimCommand, vimCommandOutput, vimGetOption, vimSetOption)
+import Ribosome.Nvim.Api.RpcCall (RpcError, syncRpcCall)
+import Ribosome.System.Time (sleep)
+
+quitChar :: Char
+quitChar =
+  '†'
+
+quitCharOrd :: Int
+quitCharOrd =
+  ord quitChar
+
+getChar ::
+  NvimE e m =>
+  MonadRibo m =>
+  MonadBaseControl IO m =>
+  m InputEvent
+getChar =
+  catchAs @RpcError InputEvent.Interrupt request
+  where
+    request =
+      event =<< vimCallFunction "getchar" [toMsgpack False]
+    event (Right c) =
+      return $ InputEvent.Character (fromMaybe c (decodeInputChar c))
+    event (Left 0) =
+      return InputEvent.NoInput
+    event (Left num) | num == quitCharOrd =
+      return InputEvent.Interrupt
+    event (Left num) =
+      maybe (InputEvent.Unexpected num) InputEvent.Character <$> decodeInputNum num
+
+getCharC ::
+  MonadIO m =>
+  MonadBaseControl IO m =>
+  NvimE e m =>
+  MonadRibo m =>
+  Double ->
+  ConduitT () PromptEvent m ()
+getCharC interval =
+  recurse
+  where
+    recurse =
+      translate =<< lift getChar
+    translate (InputEvent.Character a) =
+      yield (PromptEvent.Character a) *> recurse
+    translate InputEvent.Interrupt =
+      yield PromptEvent.Interrupt
+    translate (InputEvent.Error e) =
+      yield (PromptEvent.Error e)
+    translate InputEvent.NoInput =
+      sleep interval *> recurse
+    translate (InputEvent.Unexpected _) =
+      recurse
+
+promptFragment :: Text -> Text -> [Text]
+promptFragment hl text =
+  ["echohl " <> hl, "echon '" <> escapeQuotes text <> "'"]
+
+nvimRenderPrompt ::
+  Monad m =>
+  NvimE e m =>
+  MonadDeepError e DecodeError m =>
+  Prompt ->
+  m ()
+nvimRenderPrompt (Prompt cursor _ text) =
+  void $ atomic calls
+  where
+    calls = syncRpcCall . ApiData.vimCommand <$> ("silent! redraw!" : (fragments >>= uncurry promptFragment))
+    fragments =
+      [
+        ("RibosomePromptSign", sign),
+        ("None", pre),
+        ("RibosomePromptCaret", Text.singleton cursorChar),
+        ("None", post)
+        ]
+    (pre, rest) =
+      Text.splitAt cursor text
+    (cursorChar, post) =
+      fromMaybe (' ', "") (Text.uncons rest)
+    sign =
+      "% "
+
+loopFunctionName :: Text
+loopFunctionName =
+  "RibosomeMenuLoop"
+
+loopVarName :: Text
+loopVarName =
+  "ribosome_menu_looping"
+
+defineLoopFunction ::
+  NvimE e m =>
+  m ()
+defineLoopFunction =
+  defineFunction loopFunctionName [] lns
+  where
+    lns =
+      [
+        "echo ''",
+        "while g:" <> loopVarName,
+        "try",
+        "sleep 5m",
+        "catch /^Vim:Interrupt$/",
+        "silent! call feedkeys('" <> Text.singleton quitChar <> "')",
+        "endtry",
+        "endwhile"
+        ]
+
+startLoop ::
+  NvimE e m =>
+  m ()
+startLoop = do
+  defineLoopFunction
+  setVar loopVarName True
+  vimCommand $ "call feedkeys(\":call " <> loopFunctionName <> "()\\<cr>\")"
+
+-- FIXME need to wait for the loop to stop before deleting the function
+killLoop ::
+  NvimE e m =>
+  m ()
+killLoop = do
+  setVar loopVarName False
+  ignoreError @RpcError $ vimCommand $ "delfunction! " <> loopFunctionName
+
+promptBlocker ::
+  NvimE e m =>
+  MonadBaseControl IO m =>
+  m a ->
+  m a
+promptBlocker =
+  bracket_ startLoop killLoop
+
+newtype NvimPromptResources =
+  NvimPromptResources {
+    _guicursor :: Text
+  }
+  deriving (Eq, Show)
+
+nvimAcquire ::
+  NvimE e m =>
+  m NvimPromptResources
+nvimAcquire = do
+  highlightSet <- catchAs @RpcError False $ True <$ vimCommandOutput "highlight RibosomePromptCaret"
+  unless highlightSet $ vimCommand "highlight link RibosomePromptCaret TermCursor"
+  res <- NvimPromptResources <$> vimGetOption "guicursor"
+  vimSetOption "guicursor" (toMsgpack ("a:None" :: Text))
+  () <- vimCallFunction "inputsave" []
+  startLoop
+  return res
+
+nvimRelease ::
+  NvimE e m =>
+  MonadRibo m =>
+  NvimPromptResources ->
+  m ()
+nvimRelease (NvimPromptResources gc) = do
+  vimSetOption "guicursor" (toMsgpack gc)
+  redraw
+  vimCommand "echon ''"
+  () <- vimCallFunction "inputrestore" []
+  killLoop
+
+nvimPromptRenderer ::
+  NvimE e m =>
+  MonadRibo m =>
+  MonadDeepError e DecodeError m =>
+  PromptRenderer m
+nvimPromptRenderer =
+  PromptRenderer nvimAcquire nvimRelease nvimRenderPrompt
diff --git a/lib/Ribosome/Menu/Prompt/Run.hs b/lib/Ribosome/Menu/Prompt/Run.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Prompt/Run.hs
@@ -0,0 +1,181 @@
+module Ribosome.Menu.Prompt.Run where
+
+import Conduit (ConduitT, await, awaitForever, evalStateC, yield, (.|))
+import Data.Conduit.Combinators (peek)
+import qualified Data.Text as Text (drop, dropEnd, length, splitAt)
+
+import Ribosome.Control.Monad.Ribo (MonadRibo)
+import Ribosome.Log (logDebug)
+import Ribosome.Menu.Prompt.Data.CursorUpdate (CursorUpdate)
+import qualified Ribosome.Menu.Prompt.Data.CursorUpdate as CursorUpdate (CursorUpdate(..))
+import Ribosome.Menu.Prompt.Data.Prompt (Prompt(Prompt))
+import Ribosome.Menu.Prompt.Data.PromptConfig (PromptConfig(PromptConfig))
+import Ribosome.Menu.Prompt.Data.PromptConsumed (PromptConsumed)
+import qualified Ribosome.Menu.Prompt.Data.PromptConsumed as PromptConsumed (PromptConsumed(..))
+import Ribosome.Menu.Prompt.Data.PromptConsumerUpdate (PromptConsumerUpdate(PromptConsumerUpdate))
+import Ribosome.Menu.Prompt.Data.PromptEvent (PromptEvent)
+import qualified Ribosome.Menu.Prompt.Data.PromptEvent as PromptEvent (PromptEvent(..))
+import Ribosome.Menu.Prompt.Data.PromptRenderer (PromptRenderer(PromptRenderer))
+import Ribosome.Menu.Prompt.Data.PromptState (PromptState)
+import qualified Ribosome.Menu.Prompt.Data.PromptState as PromptState (PromptState(..))
+import Ribosome.Menu.Prompt.Data.PromptUpdate (PromptUpdate(PromptUpdate))
+import Ribosome.Menu.Prompt.Data.TextUpdate (TextUpdate)
+import qualified Ribosome.Menu.Prompt.Data.TextUpdate as TextUpdate (TextUpdate(..))
+
+updateCursor :: Int -> Text -> CursorUpdate -> Int
+updateCursor current text =
+  update
+  where
+    update CursorUpdate.OneLeft | current > 0 =
+      current - 1
+    update CursorUpdate.OneRight | current <= Text.length text =
+      current + 1
+    update CursorUpdate.Prepend =
+      0
+    update CursorUpdate.Append =
+      Text.length text + 1
+    update _ =
+      current
+
+updateText :: Int -> Text -> TextUpdate -> Text
+updateText cursor text =
+  update
+  where
+    update TextUpdate.Unmodified =
+      text
+    update (TextUpdate.Insert new) =
+      pre <> new <> post
+    update TextUpdate.DeleteLeft =
+      Text.dropEnd 1 pre <> post
+    update TextUpdate.DeleteRight =
+      pre <> Text.drop 1 post
+    (pre, post) = Text.splitAt cursor text
+
+updatePrompt ::
+  Monad m =>
+  (PromptEvent -> PromptState -> m PromptUpdate) ->
+  PromptEvent ->
+  Prompt ->
+  m (PromptConsumed, Prompt)
+updatePrompt modes update (Prompt cursor state text) = do
+  (PromptUpdate newState cursorUpdate textUpdate consumed) <- modes update state
+  let
+    newPrompt =
+      Prompt (updateCursor cursor text cursorUpdate) newState (updateText cursor text textUpdate)
+  return (consumed, newPrompt)
+
+processPromptEvent ::
+  MonadIO m =>
+  MonadRibo m =>
+  PromptConfig m ->
+  PromptEvent ->
+  ConduitT PromptEvent PromptConsumerUpdate (StateT Prompt m) ()
+processPromptEvent (PromptConfig _ modes (PromptRenderer _ _ render) _) event = do
+  logDebug @Text $ "prompt event: " <> show event
+  consumed <- lift . stateM $ lift . updatePrompt modes event
+  newPrompt <- get
+  yield (PromptConsumerUpdate event newPrompt consumed)
+  lift . lift . render $ newPrompt
+
+skippingRenderer ::
+  Monad m =>
+  (Prompt -> m ()) ->
+  ConduitT PromptConsumerUpdate PromptConsumerUpdate m ()
+skippingRenderer render =
+  go
+  where
+    go =
+      check =<< await
+    check (Just next@(PromptConsumerUpdate _ prompt _)) = do
+      yield next
+      renderIfIdle prompt =<< peek
+      go
+    check Nothing =
+      return ()
+    renderIfIdle _ (Just _) =
+      return ()
+    renderIfIdle prompt Nothing =
+      lift (render prompt)
+
+promptC ::
+  MonadIO m =>
+  MonadRibo m =>
+  PromptConfig m ->
+  ConduitT () PromptConsumerUpdate m ()
+promptC config@(PromptConfig source _ (PromptRenderer _ _ render) insert) =
+  sourceWithInit .| process .| skippingRenderer render
+  where
+    sourceWithInit =
+      yield PromptEvent.Init *> source
+    process =
+      evalStateC (pristinePrompt insert) (awaitForever (processPromptEvent config))
+
+unprocessable :: [Text]
+unprocessable =
+  [
+    "cr"
+    ]
+
+consumeUnmodified :: PromptState -> CursorUpdate -> PromptUpdate
+consumeUnmodified s u =
+  PromptUpdate s u TextUpdate.Unmodified PromptConsumed.Yes
+
+basicTransitionNormal ::
+  PromptEvent ->
+  PromptUpdate
+basicTransitionNormal (PromptEvent.Character "esc") =
+  consumeUnmodified PromptState.Quit CursorUpdate.Unmodified
+basicTransitionNormal (PromptEvent.Character "q") =
+  consumeUnmodified PromptState.Quit CursorUpdate.Unmodified
+basicTransitionNormal (PromptEvent.Character "i") =
+  consumeUnmodified PromptState.Insert CursorUpdate.Unmodified
+basicTransitionNormal (PromptEvent.Character "I") =
+  consumeUnmodified PromptState.Insert CursorUpdate.Prepend
+basicTransitionNormal (PromptEvent.Character "a") =
+  consumeUnmodified PromptState.Insert CursorUpdate.OneRight
+basicTransitionNormal (PromptEvent.Character "A") =
+  consumeUnmodified PromptState.Insert CursorUpdate.Append
+basicTransitionNormal (PromptEvent.Character "h") =
+  consumeUnmodified PromptState.Normal CursorUpdate.OneLeft
+basicTransitionNormal (PromptEvent.Character "l") =
+  consumeUnmodified PromptState.Normal CursorUpdate.OneRight
+basicTransitionNormal (PromptEvent.Character "x") =
+  PromptUpdate PromptState.Normal CursorUpdate.OneLeft TextUpdate.DeleteRight PromptConsumed.Yes
+basicTransitionNormal _ =
+  PromptUpdate PromptState.Normal CursorUpdate.Unmodified TextUpdate.Unmodified PromptConsumed.No
+
+basicTransitionInsert ::
+  PromptEvent ->
+  PromptUpdate
+basicTransitionInsert (PromptEvent.Character "esc") =
+  PromptUpdate PromptState.Normal CursorUpdate.OneLeft TextUpdate.Unmodified PromptConsumed.Yes
+basicTransitionInsert (PromptEvent.Character "bs") =
+  PromptUpdate PromptState.Insert CursorUpdate.OneLeft TextUpdate.DeleteLeft PromptConsumed.Yes
+basicTransitionInsert (PromptEvent.Character c) | c `elem` unprocessable =
+  PromptUpdate PromptState.Insert CursorUpdate.Unmodified TextUpdate.Unmodified PromptConsumed.No
+basicTransitionInsert (PromptEvent.Character c) =
+  PromptUpdate PromptState.Insert CursorUpdate.OneRight (TextUpdate.Insert c) PromptConsumed.Yes
+basicTransitionInsert _ =
+  PromptUpdate PromptState.Insert CursorUpdate.Unmodified TextUpdate.Unmodified PromptConsumed.No
+
+basicTransition ::
+  Monad m =>
+  PromptEvent ->
+  PromptState ->
+  m PromptUpdate
+basicTransition event PromptState.Normal =
+  return $ basicTransitionNormal event
+basicTransition event PromptState.Insert =
+  return $ basicTransitionInsert event
+basicTransition _ PromptState.Quit =
+  return $ PromptUpdate PromptState.Quit CursorUpdate.Unmodified TextUpdate.Unmodified PromptConsumed.No
+
+pristinePrompt :: Bool -> Prompt
+pristinePrompt insert =
+  Prompt 0 (if insert then PromptState.Insert else PromptState.Normal) ""
+
+noPromptRenderer ::
+  Applicative m =>
+  PromptRenderer m
+noPromptRenderer =
+  PromptRenderer unit (const unit) (const unit)
diff --git a/lib/Ribosome/Menu/Run.hs b/lib/Ribosome/Menu/Run.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Run.hs
@@ -0,0 +1,170 @@
+module Ribosome.Menu.Run where
+
+import Conduit (ConduitT, await, awaitForever, mapC, yield, (.|))
+import Control.Exception.Lifted (bracket)
+import Control.Monad.Trans.Control (MonadBaseControl)
+import Data.Conduit.Combinators (iterM)
+import qualified Data.Conduit.Combinators as Conduit (last)
+import Data.Conduit.Lift (evalStateC)
+
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE)
+import Ribosome.Data.Conduit (withMergedSources)
+import Ribosome.Data.Scratch (scratchWindow)
+import Ribosome.Data.ScratchOptions (ScratchOptions)
+import Ribosome.Log (showDebug)
+import Ribosome.Menu.Data.Menu (Menu)
+import Ribosome.Menu.Data.MenuAction (MenuAction)
+import qualified Ribosome.Menu.Data.MenuAction as MenuAction (MenuAction(..))
+import Ribosome.Menu.Data.MenuConfig (MenuConfig(MenuConfig))
+import Ribosome.Menu.Data.MenuConsumer (MenuConsumer(MenuConsumer))
+import Ribosome.Menu.Data.MenuEvent (MenuEvent, QuitReason)
+import qualified Ribosome.Menu.Data.MenuEvent as MenuEvent (MenuEvent(..))
+import qualified Ribosome.Menu.Data.MenuEvent as QuitReason (QuitReason(..))
+import Ribosome.Menu.Data.MenuItem (MenuItem)
+import Ribosome.Menu.Data.MenuRenderEvent (MenuRenderEvent)
+import qualified Ribosome.Menu.Data.MenuRenderEvent as MenuRenderEvent (MenuRenderEvent(..))
+import Ribosome.Menu.Data.MenuResult (MenuResult)
+import qualified Ribosome.Menu.Data.MenuResult as MenuResult (MenuResult(..))
+import Ribosome.Menu.Data.MenuUpdate (MenuUpdate(MenuUpdate))
+import Ribosome.Menu.Nvim (renderNvimMenu)
+import Ribosome.Menu.Prompt.Data.Prompt (Prompt(Prompt))
+import Ribosome.Menu.Prompt.Data.PromptConfig (PromptConfig(PromptConfig))
+import Ribosome.Menu.Prompt.Data.PromptConsumed (PromptConsumed)
+import qualified Ribosome.Menu.Prompt.Data.PromptConsumed as PromptConsumed (PromptConsumed(..))
+import Ribosome.Menu.Prompt.Data.PromptConsumerUpdate (PromptConsumerUpdate(PromptConsumerUpdate))
+import Ribosome.Menu.Prompt.Data.PromptEvent (PromptEvent)
+import qualified Ribosome.Menu.Prompt.Data.PromptEvent as PromptEvent (PromptEvent(..))
+import Ribosome.Menu.Prompt.Data.PromptRenderer (PromptRenderer(PromptRenderer))
+import qualified Ribosome.Menu.Prompt.Data.PromptState as PromptState (PromptState(..))
+import Ribosome.Menu.Prompt.Run (promptC)
+import Ribosome.Msgpack.Encode (MsgpackEncode(toMsgpack))
+import Ribosome.Msgpack.Error (DecodeError)
+import Ribosome.Nvim.Api.IO (windowSetOption)
+import Ribosome.Scratch (showInScratch)
+
+promptEvent ::
+  PromptEvent ->
+  Prompt ->
+  PromptConsumed ->
+  MenuEvent m a
+promptEvent _ (Prompt _ PromptState.Quit _) _ =
+  MenuEvent.Quit QuitReason.Aborted
+promptEvent (PromptEvent.Character a) prompt PromptConsumed.No =
+  MenuEvent.Mapping a prompt
+promptEvent (PromptEvent.Character a) prompt@(Prompt _ PromptState.Insert _) _ =
+  MenuEvent.PromptChange a prompt
+promptEvent (PromptEvent.Character a) prompt _ =
+  MenuEvent.Mapping a prompt
+promptEvent PromptEvent.Init prompt _ =
+  MenuEvent.Init prompt
+promptEvent (PromptEvent.Unexpected code) _ _ =
+  MenuEvent.Quit . QuitReason.PromptError $ "unexpected input character code: " <> show code
+promptEvent PromptEvent.Interrupt _ _ =
+  MenuEvent.Quit QuitReason.Aborted
+promptEvent (PromptEvent.Error e) _ _ =
+  MenuEvent.Quit (QuitReason.PromptError e)
+
+menuEvent ::
+  Either PromptConsumerUpdate MenuItem ->
+  MenuEvent m a
+menuEvent =
+  either promptUpdate MenuEvent.NewItems
+  where
+    promptUpdate (PromptConsumerUpdate event prompt consumed) =
+      promptEvent event prompt consumed
+
+updateMenu ::
+  MonadRibo m =>
+  MenuConsumer m a ->
+  Either PromptConsumerUpdate MenuItem ->
+  ConduitT (Either PromptConsumerUpdate MenuItem) (MenuRenderEvent m a) (StateT Menu m) ()
+updateMenu (MenuConsumer consumer) input = do
+  showDebug "menu update:" input
+  action <- lift . stateM $ lift . consumer . MenuUpdate (menuEvent input)
+  showDebug "menu action:" action
+  emit action
+  where
+    emit MenuAction.Continue =
+      return ()
+    emit (MenuAction.Render changed) =
+      yield . MenuRenderEvent.Render changed =<< get
+    emit (MenuAction.Quit reason) =
+      yield (MenuRenderEvent.Quit reason)
+
+menuSources ::
+  MonadIO m =>
+  MonadRibo m =>
+  PromptConfig m ->
+  ConduitT () MenuItem m () ->
+  [ConduitT () (Either PromptConsumerUpdate MenuItem) m ()]
+menuSources promptConfig items =
+  [promptSource, itemSource]
+  where
+    promptSource =
+      promptC promptConfig .| mapC Left
+    itemSource =
+      items .| mapC Right
+
+menuTerminator ::
+  Monad m =>
+  ConduitT (MenuRenderEvent m a) (QuitReason m a) m ()
+menuTerminator =
+  traverse_ check =<< await
+  where
+    check (MenuRenderEvent.Quit reason) =
+      yield reason
+    check _ =
+      menuTerminator
+
+menuResult ::
+  Monad m =>
+  QuitReason m a ->
+  m (MenuResult a)
+menuResult (QuitReason.Return a) =
+  return (MenuResult.Return a)
+menuResult (QuitReason.Execute ma) =
+  MenuResult.Return <$> ma
+menuResult (QuitReason.PromptError err) =
+  return (MenuResult.Error err)
+menuResult QuitReason.NoOutput =
+  return MenuResult.NoOutput
+menuResult QuitReason.Aborted =
+  return MenuResult.Aborted
+
+runMenu ::
+  MonadIO m =>
+  MonadRibo m =>
+  MonadBaseControl IO m =>
+  MenuConfig m a ->
+  m (MenuResult a)
+runMenu (MenuConfig items handle render promptConfig@(PromptConfig _ _ promptRenderer _)) =
+  withPrompt promptRenderer
+  where
+    withPrompt (PromptRenderer acquire release _) =
+      bracket acquire release (const run)
+    run =
+      menuResult =<< quitReason <$> withMergedSources consumer 64 (menuSources promptConfig items)
+    consumer =
+      evalStateC def (awaitForever (updateMenu handle)) .| iterM render .| menuTerminator .| Conduit.last
+    quitReason =
+      fromMaybe QuitReason.NoOutput
+
+nvimMenu ::
+  NvimE e m =>
+  MonadIO m =>
+  MonadRibo m =>
+  MonadBaseControl IO m =>
+  MonadDeepError e DecodeError m =>
+  ScratchOptions ->
+  ConduitT () MenuItem m () ->
+  (MenuUpdate m a -> m (MenuAction m a, Menu)) ->
+  PromptConfig m ->
+  m (MenuResult a)
+nvimMenu options items handle promptConfig =
+  run =<< showInScratch [] options
+  where
+    run scratch = do
+      windowSetOption (scratchWindow scratch) "cursorline" (toMsgpack True)
+      runMenu $ MenuConfig items (MenuConsumer handle) (render scratch) promptConfig
+    render =
+      renderNvimMenu options
diff --git a/lib/Ribosome/Menu/Simple.hs b/lib/Ribosome/Menu/Simple.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Menu/Simple.hs
@@ -0,0 +1,169 @@
+module Ribosome.Menu.Simple where
+
+import Control.Lens ((^?))
+import qualified Control.Lens as Lens (element, over, views)
+import Data.Composition ((.:))
+import Data.Map ((!?))
+import qualified Data.Map as Map (fromList, union)
+import qualified Data.Text as Text (breakOn, null)
+
+import Ribosome.Menu.Data.Menu (Menu(Menu), MenuFilter(MenuFilter))
+import qualified Ribosome.Menu.Data.Menu as Menu (filtered, items, selected)
+import Ribosome.Menu.Data.MenuAction (MenuAction)
+import qualified Ribosome.Menu.Data.MenuAction as MenuAction (MenuAction(..))
+import Ribosome.Menu.Data.MenuConsumerAction (MenuConsumerAction)
+import qualified Ribosome.Menu.Data.MenuConsumerAction as MenuConsumerAction (MenuConsumerAction(..))
+import Ribosome.Menu.Data.MenuEvent (MenuEvent)
+import qualified Ribosome.Menu.Data.MenuEvent as MenuEvent (MenuEvent(..))
+import qualified Ribosome.Menu.Data.MenuEvent as QuitReason (QuitReason(..))
+import Ribosome.Menu.Data.MenuItem (MenuItem)
+import qualified Ribosome.Menu.Data.MenuItem as MenuItem (MenuItem(_text))
+import Ribosome.Menu.Data.MenuUpdate (MenuUpdate(MenuUpdate))
+import Ribosome.Menu.Prompt.Data.Prompt (Prompt(Prompt))
+
+type Mappings m a = Map Text (Menu -> Prompt -> m (MenuConsumerAction m a, Menu))
+
+textContains :: Text -> Text -> Bool
+textContains needle haystack =
+  Text.null needle || (not (Text.null haystack) && search needle haystack)
+  where
+    search =
+      not . Text.null . snd .: Text.breakOn
+
+updateFilter :: Text -> Menu -> (Bool, MenuAction m a, Menu)
+updateFilter text (Menu items oldFiltered stack selected _) =
+  (filtered /= oldFiltered, MenuAction.Continue, Menu items filtered stack selected (MenuFilter text))
+  where
+    filtered =
+      filter (textContains text . MenuItem._text) items
+
+reapplyFilter :: Menu -> (Bool, MenuAction m a, Menu)
+reapplyFilter menu@(Menu _ _ _ _ (MenuFilter currentFilter)) =
+  updateFilter currentFilter menu
+
+basicMenuTransform :: MenuEvent m a -> Menu -> (Bool, MenuAction m a, Menu)
+basicMenuTransform (MenuEvent.PromptChange _ (Prompt _ _ text)) =
+  updateFilter text
+basicMenuTransform (MenuEvent.Mapping _ _) =
+  (False, MenuAction.Continue,)
+basicMenuTransform (MenuEvent.NewItems item) =
+  reapplyFilter . Lens.over Menu.items (item :)
+basicMenuTransform (MenuEvent.Init _) =
+  (True, MenuAction.Continue,)
+basicMenuTransform (MenuEvent.Quit reason) =
+  (False, MenuAction.Quit reason,)
+
+basicMenu ::
+  Monad m =>
+  (MenuUpdate m a -> m (MenuConsumerAction m a, Menu)) ->
+  MenuUpdate m a ->
+  m (MenuAction m a, Menu)
+basicMenu consumer (MenuUpdate event menu) =
+  consumerAction action
+  where
+    (changed, action, newMenu) = basicMenuTransform event menu
+    consumerAction (MenuAction.Quit reason) =
+      return (MenuAction.Quit reason, menu)
+    consumerAction _ =
+      first menuAction <$> consumer (MenuUpdate event newMenu)
+    menuAction MenuConsumerAction.Continue =
+      if changed then MenuAction.Render True else MenuAction.Continue
+    menuAction (MenuConsumerAction.Render consumerChanged) =
+      MenuAction.Render (changed || consumerChanged)
+    menuAction (MenuConsumerAction.QuitWith ma) =
+      MenuAction.Quit (QuitReason.Execute ma)
+    menuAction MenuConsumerAction.Quit =
+      MenuAction.Quit QuitReason.Aborted
+    menuAction (MenuConsumerAction.Return a) =
+      MenuAction.Quit (QuitReason.Return a)
+
+mappingConsumer ::
+  Monad m =>
+  Mappings m a ->
+  MenuUpdate m a ->
+  m (MenuConsumerAction m a, Menu)
+mappingConsumer mappings (MenuUpdate (MenuEvent.Mapping char prompt) menu) =
+  handler menu prompt
+  where
+    handler =
+      fromMaybe (const . menuContinue) (mappings !? char)
+mappingConsumer _ (MenuUpdate _ menu) =
+  menuContinue menu
+
+simpleMenu ::
+  Monad m =>
+  Mappings m a ->
+  MenuUpdate m a ->
+  m (MenuAction m a, Menu)
+simpleMenu =
+  basicMenu . mappingConsumer
+
+menuCycle ::
+  Monad m =>
+  Int ->
+  Menu ->
+  Prompt ->
+  m (MenuConsumerAction m a, Menu)
+menuCycle offset m _ =
+  menuRender False (Lens.over Menu.selected add m)
+  where
+    count =
+      Lens.views Menu.filtered length m
+    add current =
+      if count == 0 then 0 else (current + offset) `mod` count
+
+defaultMappings ::
+  Monad m =>
+  Mappings m a
+defaultMappings =
+  Map.fromList [("k", menuCycle 1), ("j", menuCycle (-1))]
+
+defaultMenu ::
+  Monad m =>
+  Mappings m a ->
+  MenuUpdate m a ->
+  m (MenuAction m a, Menu)
+defaultMenu =
+  simpleMenu . (`Map.union` defaultMappings)
+
+menuContinue ::
+  Monad m =>
+  Menu ->
+  m (MenuConsumerAction m a, Menu)
+menuContinue =
+  return . (MenuConsumerAction.Continue,)
+
+menuRender ::
+  Monad m =>
+  Bool ->
+  Menu ->
+  m (MenuConsumerAction m a, Menu)
+menuRender changed =
+  return . (MenuConsumerAction.Render changed,)
+
+menuQuit ::
+  Monad m =>
+  Menu ->
+  m (MenuConsumerAction m a, Menu)
+menuQuit =
+  return . (MenuConsumerAction.Quit,)
+
+menuQuitWith ::
+  Monad m =>
+  m a ->
+  Menu ->
+  m (MenuConsumerAction m a, Menu)
+menuQuitWith next =
+  return . (MenuConsumerAction.QuitWith next,)
+
+menuReturn ::
+  Monad m =>
+  a ->
+  Menu ->
+  m (MenuConsumerAction m a, Menu)
+menuReturn a =
+  return . (MenuConsumerAction.Return a,)
+
+selectedMenuItem :: Menu -> Maybe MenuItem
+selectedMenuItem (Menu _ items _ selected _) =
+  items ^? Lens.element (length items - selected - 1)
diff --git a/lib/Ribosome/Monad.hs b/lib/Ribosome/Monad.hs
deleted file mode 100644
--- a/lib/Ribosome/Monad.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-module Ribosome.Monad(
-  liftExceptTIO,
-  liftExceptT,
-) where
-
-import Control.Monad.IO.Class (MonadIO, liftIO)
-import Control.Monad.Trans.Except (ExceptT(ExceptT))
-
-liftExceptTIO :: (MonadIO m) => IO a -> ExceptT e m a
-liftExceptTIO fa = ExceptT $ Right <$> liftIO fa
-
-liftExceptT :: (Functor m) => m a -> ExceptT e m a
-liftExceptT fa = ExceptT $ Right <$> fa
diff --git a/lib/Ribosome/Msgpack/Decode.hs b/lib/Ribosome/Msgpack/Decode.hs
--- a/lib/Ribosome/Msgpack/Decode.hs
+++ b/lib/Ribosome/Msgpack/Decode.hs
@@ -1,30 +1,36 @@
-{-# LANGUAGE TypeOperators #-}
-
-module Ribosome.Msgpack.Decode(
-  MsgpackDecode(..),
-) where
+module Ribosome.Msgpack.Decode where
 
-import Data.ByteString.Internal (unpackChars)
-import Data.Map.Strict (Map, (!?))
-import qualified Data.Map.Strict as Map (fromList, toList)
+import Control.Monad.DeepError (MonadDeepError, hoistEitherWith)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString.UTF8 as ByteString (toString)
+import Data.Either.Combinators (mapLeft)
+import Data.Int (Int64)
+import Data.Map (Map, (!?))
+import qualified Data.Map as Map (fromList, toList)
 import Data.MessagePack (Object(..))
+import Data.Text.Prettyprint.Doc (pretty)
+import GHC.Float (double2Float)
 import GHC.Generics (
+  C1,
+  Constructor,
+  D1,
   Generic,
-  Rep,
-  (:*:)(..),
+  K1(..),
   M1(..),
-  D1,
-  C1,
+  Rep,
   S1,
-  K1(..),
   Selector,
-  Constructor,
+  conIsRecord,
   selName,
   to,
-  conIsRecord,
+  (:*:)(..),
   )
+import Neovim (CommandArguments)
+
+import Ribosome.Msgpack.Error (DecodeError)
+import qualified Ribosome.Msgpack.Error as DecodeError (DecodeError(Failed))
 import Ribosome.Msgpack.Util (Err)
-import qualified Ribosome.Msgpack.Util as Util (string, invalid, missingRecordKey, illegalType)
+import qualified Ribosome.Msgpack.Util as Util (illegalType, invalid, missingRecordKey, string)
 
 class MsgpackDecode a where
   fromMsgpack :: Object -> Either Err a
@@ -102,24 +108,95 @@
         return (k1, v1)
   fromMsgpack o = Util.illegalType "Map" o
 
+msgpackIntegral :: (Integral a, Read a) => Object -> Either Err a
+msgpackIntegral (ObjectInt i) = Right $ fromIntegral i
+msgpackIntegral (ObjectUInt i) = Right $ fromIntegral i
+msgpackIntegral (ObjectString s) = mapLeft pretty . readEither . ByteString.toString $ s
+msgpackIntegral o = Util.illegalType "Integral" o
+
 instance MsgpackDecode Int where
-  fromMsgpack (ObjectInt i) = Right $ fromIntegral i
-  fromMsgpack o = Util.illegalType "Int" o
+  fromMsgpack = msgpackIntegral
 
+instance MsgpackDecode Int64 where
+  fromMsgpack = msgpackIntegral
+
+instance MsgpackDecode Float where
+  fromMsgpack (ObjectFloat a) = Right a
+  fromMsgpack (ObjectDouble a) = Right (double2Float a)
+  fromMsgpack (ObjectInt a) = Right (fromIntegral a)
+  fromMsgpack (ObjectUInt a) = Right (fromIntegral a)
+  fromMsgpack o = Util.illegalType "Float" o
+
 instance {-# OVERLAPPING #-} MsgpackDecode String where
-  fromMsgpack (ObjectString os) = Right $ unpackChars os
+  fromMsgpack (ObjectString os) = Right $ decodeUtf8 os
+  fromMsgpack (ObjectBinary os) = Right $ decodeUtf8 os
   fromMsgpack o = Util.illegalType "String" o
 
 instance {-# OVERLAPPABLE #-} MsgpackDecode a => MsgpackDecode [a] where
   fromMsgpack (ObjectArray oa) = traverse fromMsgpack oa
   fromMsgpack o = Util.illegalType "List" o
 
+instance MsgpackDecode Text where
+  fromMsgpack (ObjectString os) = Right (decodeUtf8 os)
+  fromMsgpack (ObjectBinary os) = Right (decodeUtf8 os)
+  fromMsgpack o = Util.illegalType "Text" o
+
+instance MsgpackDecode ByteString where
+  fromMsgpack (ObjectString os) = Right os
+  fromMsgpack (ObjectBinary os) = Right os
+  fromMsgpack o = Util.illegalType "ByteString" o
+
+instance MsgpackDecode Char where
+  fromMsgpack o@(ObjectString os) =
+    check . decodeUtf8 $ os
+    where
+      check [c] = Right c
+      check _ = Util.invalid "multiple characters when decoding Char" o
+  fromMsgpack o = Util.illegalType "Char" o
+
 instance MsgpackDecode a => MsgpackDecode (Maybe a) where
   fromMsgpack ObjectNil = Right Nothing
   fromMsgpack o = Just <$> fromMsgpack o
-
   missingKey _ _ = Right Nothing
 
+instance (MsgpackDecode a, MsgpackDecode b) => MsgpackDecode (Either a b) where
+  fromMsgpack o =
+    fromRight (Left <$> fromMsgpack o) (Right . Right <$> fromMsgpack o)
+
 instance MsgpackDecode Bool where
   fromMsgpack (ObjectBool a) = Right a
   fromMsgpack o = Util.illegalType "Bool" o
+
+instance MsgpackDecode () where
+  fromMsgpack _ = Right ()
+
+instance MsgpackDecode Object where
+  fromMsgpack = Right
+
+instance (MsgpackDecode a, MsgpackDecode b) => MsgpackDecode (a, b) where
+  fromMsgpack (ObjectArray [a, b]) =
+    (,) <$> fromMsgpack a <*> fromMsgpack b
+  fromMsgpack o@(ObjectArray _) =
+    Util.invalid "invalid array length for pair" o
+  fromMsgpack o =
+    Util.illegalType "pair" o
+
+instance MsgpackDecode CommandArguments where
+
+fromMsgpack' ::
+  ∀ a e m.
+  MonadDeepError e DecodeError m =>
+  MsgpackDecode a =>
+  Object ->
+  m a
+fromMsgpack' =
+  hoistEitherWith DecodeError.Failed . fromMsgpack
+
+msgpackFromString :: IsString a => String -> Object -> Either Err a
+msgpackFromString name o =
+  adapt $ fromMsgpack o
+  where
+    adapt (Right a) =
+      Right $ fromString a
+    adapt (Left _) =
+      Util.illegalType name o
diff --git a/lib/Ribosome/Msgpack/Encode.hs b/lib/Ribosome/Msgpack/Encode.hs
--- a/lib/Ribosome/Msgpack/Encode.hs
+++ b/lib/Ribosome/Msgpack/Encode.hs
@@ -1,30 +1,30 @@
 {-# LANGUAGE DefaultSignatures #-}
 {-# LANGUAGE TypeOperators #-}
 
-module Ribosome.Msgpack.Encode(
-  MsgpackEncode(..),
-) where
+module Ribosome.Msgpack.Encode where
 
 import Data.Bifunctor (bimap)
-import Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map (fromList, toList)
+import Data.ByteString (ByteString)
+import Data.Int (Int64)
+import Data.Map (Map)
+import qualified Data.Map as Map (fromList, toList)
 import Data.MessagePack (Object(..))
 import GHC.Generics (
+  C1,
+  Constructor,
+  D1,
   Generic,
-  Rep,
-  (:*:)(..),
+  K1(..),
   M1(..),
-  D1,
-  C1,
+  Rep,
   S1,
-  K1(..),
   Selector,
-  Constructor,
-  selName,
-  from,
   conIsRecord,
+  from,
+  selName,
+  (:*:)(..),
   )
-import qualified Ribosome.Msgpack.Util as Util (string, assembleMap)
+import qualified Ribosome.Msgpack.Util as Util (assembleMap, string, text)
 
 class MsgpackEncode a where
   toMsgpack :: a -> Object
@@ -55,8 +55,8 @@
       f = if conIsRecord c then Util.assembleMap . msgpackEncodeRecord else prodOrNewtype
 
 instance (MsgpackEncodeProd f, MsgpackEncodeProd g) => MsgpackEncodeProd (f :*: g) where
-  msgpackEncodeRecord (f :*: g) = msgpackEncodeRecord f ++ msgpackEncodeRecord g
-  msgpackEncodeProd (f :*: g) = msgpackEncodeProd f ++ msgpackEncodeProd g
+  msgpackEncodeRecord (f :*: g) = msgpackEncodeRecord f <> msgpackEncodeRecord g
+  msgpackEncodeProd (f :*: g) = msgpackEncodeProd f <> msgpackEncodeProd g
 
 instance (Selector s, GMsgpackEncode f) => MsgpackEncodeProd (S1 s f) where
   msgpackEncodeRecord s@(M1 f) = [(selName s, gMsgpackEncode f)]
@@ -71,14 +71,35 @@
 instance MsgpackEncode Int where
   toMsgpack = ObjectInt . fromIntegral
 
+instance MsgpackEncode Int64 where
+  toMsgpack = ObjectInt . fromIntegral
+
+instance MsgpackEncode Float where
+  toMsgpack = ObjectFloat
+
 instance {-# OVERLAPPING #-} MsgpackEncode String where
   toMsgpack = Util.string
 
 instance {-# OVERLAPPABLE #-} MsgpackEncode a => MsgpackEncode [a] where
   toMsgpack = ObjectArray . fmap toMsgpack
 
+instance MsgpackEncode Text where
+  toMsgpack = Util.text
+
 instance MsgpackEncode a => MsgpackEncode (Maybe a) where
   toMsgpack = maybe ObjectNil toMsgpack
 
 instance MsgpackEncode Bool where
   toMsgpack = ObjectBool
+
+instance MsgpackEncode () where
+  toMsgpack _ = ObjectNil
+
+instance MsgpackEncode Object where
+  toMsgpack = id
+
+instance MsgpackEncode ByteString where
+  toMsgpack = ObjectString
+
+instance (MsgpackEncode a, MsgpackEncode b) => MsgpackEncode (a, b) where
+  toMsgpack (a, b) = ObjectArray [toMsgpack a, toMsgpack b]
diff --git a/lib/Ribosome/Msgpack/Error.hs b/lib/Ribosome/Msgpack/Error.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Msgpack/Error.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Msgpack.Error where
+
+import Data.DeepPrisms (deepPrisms)
+import System.Log.Logger (Priority(ERROR))
+
+import Data.Text.Prettyprint.Doc (defaultLayoutOptions, layoutPretty)
+import Data.Text.Prettyprint.Doc.Render.Text (renderStrict)
+import Ribosome.Data.ErrorReport (ErrorReport(ErrorReport))
+import Ribosome.Error.Report.Class (ReportError(..))
+import Ribosome.Msgpack.Util (Err)
+
+newtype DecodeError =
+  Failed Err
+  deriving Show
+
+deepPrisms ''DecodeError
+
+instance ReportError DecodeError where
+  errorReport (Failed err) =
+    ErrorReport "error decoding response from neovim" ["DecodeError:", rendered] ERROR
+    where
+      rendered = renderStrict $ layoutPretty defaultLayoutOptions err
diff --git a/lib/Ribosome/Msgpack/NvimObject.hs b/lib/Ribosome/Msgpack/NvimObject.hs
--- a/lib/Ribosome/Msgpack/NvimObject.hs
+++ b/lib/Ribosome/Msgpack/NvimObject.hs
@@ -6,11 +6,11 @@
 import Control.DeepSeq (NFData)
 import GHC.Generics (Generic)
 import Neovim (NvimObject(..))
-import Ribosome.Msgpack.Encode (MsgpackEncode(..))
 import Ribosome.Msgpack.Decode (MsgpackDecode(..))
+import Ribosome.Msgpack.Encode (MsgpackEncode(..))
 
 newtype NO a =
-  NO a
+  NO { unNO :: a }
   deriving (Eq, Show, Generic, NFData)
 
 instance (MsgpackEncode a, MsgpackDecode a, NFData a) => NvimObject (NO a) where
diff --git a/lib/Ribosome/Msgpack/Util.hs b/lib/Ribosome/Msgpack/Util.hs
--- a/lib/Ribosome/Msgpack/Util.hs
+++ b/lib/Ribosome/Msgpack/Util.hs
@@ -1,26 +1,23 @@
-module Ribosome.Msgpack.Util(
-  Err,
-  string,
-  assembleMap,
-  invalid,
-  missingRecordKey,
-  illegalType,
-) where
+module Ribosome.Msgpack.Util where
 
 import Data.Bifunctor (first)
-import Data.ByteString.Internal (packChars)
-import qualified Data.Map.Strict as Map (fromList)
+import qualified Data.ByteString.UTF8 as ByteString (fromString)
+import qualified Data.Map as Map (fromList)
 import Data.MessagePack (Object(..))
-import Data.Text.Prettyprint.Doc (Doc, (<+>), viaShow, pretty)
+import Data.Text.Prettyprint.Doc (Doc, pretty, viaShow, (<+>))
 import Data.Text.Prettyprint.Doc.Render.Terminal (AnsiStyle)
 
 type Err = Doc AnsiStyle
 
 string :: String -> Object
-string = ObjectString . packChars
+string = ObjectString . ByteString.fromString
 
+text :: Text -> Object
+text = ObjectString . encodeUtf8
+
 assembleMap :: [(String, Object)] -> Object
-assembleMap = ObjectMap . Map.fromList . (fmap . first) string
+assembleMap =
+  ObjectMap . Map.fromList . (fmap . first) string
 
 invalid :: String -> Object -> Either Err a
 invalid msg obj =
@@ -31,4 +28,5 @@
   invalid $ "missing record key " ++ key ++ " in ObjectMap"
 
 illegalType :: String -> Object -> Either Err a
-illegalType tpe = invalid $ "illegal type for " ++ tpe
+illegalType tpe =
+  invalid $ "illegal type for " ++ tpe
diff --git a/lib/Ribosome/Nvim/Api/Data.hs b/lib/Ribosome/Nvim/Api/Data.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Nvim/Api/Data.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Nvim.Api.Data where
+
+import Data.MessagePack (Object(ObjectExt))
+import Prelude
+import Ribosome.Msgpack.Decode (MsgpackDecode(..))
+import Ribosome.Msgpack.Encode (MsgpackEncode(..))
+
+import Ribosome.Nvim.Api.GenerateData (generateData)
+
+generateData
diff --git a/lib/Ribosome/Nvim/Api/Generate.hs b/lib/Ribosome/Nvim/Api/Generate.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Nvim/Api/Generate.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Nvim.Api.Generate where
+
+import Control.Monad (join)
+import Data.Bifunctor (first)
+import Data.Char (toUpper)
+import Data.Int (Int64)
+import Data.Map (Map)
+import qualified Data.Map as Map (fromList, lookup)
+import Data.Maybe (fromMaybe)
+import Data.MessagePack (Object)
+import Language.Haskell.TH
+import Neovim.API.Parser (
+  NeovimAPI(functions),
+  NeovimFunction(NeovimFunction),
+  NeovimType(NestedType, SimpleType, Void),
+  customTypes,
+  parseAPI,
+  )
+
+camelcase :: String -> String
+camelcase =
+  snd . foldr folder (False, "")
+  where
+    folder '_' (_, z) = (True, z)
+    folder a (True, h : t) = (False, a : toUpper h : t)
+    folder a (True, []) = (False, [a])
+    folder a (False, z) = (False, a : z)
+
+haskellTypes :: Map String TypeQ
+haskellTypes =
+  Map.fromList [
+    ("Boolean", [t|Bool|]),
+    ("Integer", [t|Int|]),
+    ("Float", [t|Double|]),
+    ("String", [t|Text|]),
+    ("Array", [t|[Object]|]),
+    ("Dictionary", [t|Map Text Object|]),
+    ("void", [t|()|])
+    ]
+
+haskellType :: NeovimType -> Q Type
+haskellType at =
+  case at of
+    Void -> [t|()|]
+    NestedType t Nothing ->
+      appT listT $ haskellType t
+    NestedType t (Just n) ->
+      foldl appT (tupleT n) . replicate n $ haskellType t
+    SimpleType t ->
+      fromMaybe (conT . mkName $ t) $ Map.lookup t haskellTypes
+
+data FunctionData =
+  FunctionData {
+    apiName :: String,
+    ccName :: Name,
+    async :: Bool,
+    names :: [Name],
+    types :: [Type],
+    returnType :: NeovimType
+    }
+  deriving (Eq, Show)
+
+functionData :: NeovimFunction -> Q FunctionData
+functionData (NeovimFunction name parameters _ async returnType) = do
+  names <- traverse newName prefixedNames
+  types <- traverse haskellType (fst <$> parameters)
+  return (FunctionData name (mkName . camelcase $ name) async names types returnType)
+  where
+    prefix i n = "arg" <> show i <> "_" <> n
+    prefixedNames = zipWith prefix [0 :: Int ..] (snd <$> parameters)
+
+generateFromApi :: (FunctionData -> Q [Dec]) -> (Name -> Int64 -> DecsQ) -> Q [Dec]
+generateFromApi handleFunction handleExtType = do
+  api <- either (fail . show) return =<< runIO parseAPI
+  funcs <- traverse functionData (functions api)
+  funcDecs <- traverse handleFunction funcs
+  tpeDecs <- traverse (uncurry handleExtType) $ first mkName <$> customTypes api
+  return $ join (funcDecs <> tpeDecs)
diff --git a/lib/Ribosome/Nvim/Api/GenerateData.hs b/lib/Ribosome/Nvim/Api/GenerateData.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Nvim/Api/GenerateData.hs
@@ -0,0 +1,85 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Nvim.Api.GenerateData where
+
+import Data.ByteString (ByteString)
+import Data.Int (Int64)
+import Data.MessagePack (Object(ObjectExt))
+import Language.Haskell.TH
+import Neovim.Plugin.Classes (FunctionName(F))
+
+import Ribosome.Msgpack.Decode (MsgpackDecode)
+import Ribosome.Msgpack.Encode (MsgpackEncode)
+import Ribosome.Msgpack.Util (illegalType)
+import Ribosome.Nvim.Api.Generate (FunctionData(FunctionData), generateFromApi)
+import Ribosome.Nvim.Api.RpcCall (AsyncRpcCall(..), RpcCall(..), SyncRpcCall(..))
+
+dataSig :: [Type] -> Name -> Bool -> DecQ
+dataSig types name async = do
+  returnType <- if async then [t|AsyncRpcCall|] else [t|SyncRpcCall|]
+  sigD name . return . foldr (AppT . AppT ArrowT) returnType $ types
+
+dataBody :: String -> Name -> Bool -> [Name] -> DecQ
+dataBody apiName name async params =
+  funD name [clause (varP <$> params) (normalB $ appE syncCtor rpcCall) []]
+  where
+    rpcCall = [|RpcCall|] `appE` funcName `appE` listE (toObjVar <$> params)
+    funcName = [|F . fromString|] `appE` (litE . stringL $ apiName)
+    toObjVar v = [|toMsgpack $(varE v)|]
+    syncCtor = if async then [|AsyncRpcCall|] else [|SyncRpcCall|]
+
+genCallData :: FunctionData -> DecsQ
+genCallData (FunctionData apiName name async names types _) = do
+  sig <- dataSig types name async
+  body <- dataBody apiName name async names
+  return [sig, body]
+
+extData :: Name -> DecQ
+extData name =
+  dataD (return []) name [] Nothing [ctor] (deriv ["Eq", "Show"])
+  where
+    ctor = normalC name [(Bang NoSourceUnpackedness SourceStrict,) <$> [t|ByteString|]]
+    deriv = return . return . DerivClause Nothing . (ConT . mkName <$>)
+
+decClause :: Name -> Int64 -> ClauseQ
+decClause name number = do
+  bytesVar <- newName "bytes"
+  clause [pattern bytesVar] (decBody bytesVar) []
+  where
+    pattern bytesVar = conP (mkName "ObjectExt") [(litP . integerL . fromIntegral) number, varP bytesVar]
+    decBody bytesVar = (normalB [|return $ $(conE name) $(varE bytesVar)|])
+
+decErrorClause :: Name -> ClauseQ
+decErrorClause name = do
+  objectVar <- newName "object"
+  clause [varP objectVar] (decBody objectVar) []
+  where
+    nameString = nameBase name
+    decBody objectVar = normalB [|illegalType nameString $(varE objectVar)|]
+
+encClause :: Name -> Int64 -> ClauseQ
+encClause name number = do
+  bytesVar <- newName "bytes"
+  clause [conP name [varP bytesVar]] (encBody bytesVar) []
+  where
+    encBody bytesVar = normalB [|ObjectExt $((litE . integerL . fromIntegral) number) $(varE bytesVar)|]
+
+extDataCodec :: Name -> Int64 -> DecsQ
+extDataCodec name number = do
+  dec <- inst [t|MsgpackDecode|] [method "fromMsgpack" [decClause name number, decErrorClause name]]
+  enc <- inst [t|MsgpackEncode|] [method "toMsgpack" [encClause name number]]
+  return [dec, enc]
+  where
+    inst t = instanceD (return []) (tpe t)
+    tpe = (`appT` conT name)
+    method methodName clauses = funD (mkName methodName) clauses
+
+genExtTypes :: Name -> Int64 -> DecsQ
+genExtTypes name number = do
+  dat <- extData name
+  codec <- extDataCodec name number
+  return (dat : codec)
+
+generateData :: DecsQ
+generateData =
+  generateFromApi genCallData genExtTypes
diff --git a/lib/Ribosome/Nvim/Api/GenerateIO.hs b/lib/Ribosome/Nvim/Api/GenerateIO.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Nvim/Api/GenerateIO.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Nvim.Api.GenerateIO where
+
+import Control.Monad ((<=<))
+import Control.Monad.DeepError (MonadDeepError, hoistEither)
+import Data.Maybe (maybeToList)
+import Language.Haskell.TH
+import Language.Haskell.TH.Syntax
+import Neovim.API.Parser (NeovimType(SimpleType))
+
+import Ribosome.Control.Monad.Ribo (Nvim(call))
+import Ribosome.Msgpack.Decode (MsgpackDecode)
+import Ribosome.Nvim.Api.Generate (FunctionData(FunctionData), generateFromApi, haskellType)
+import Ribosome.Nvim.Api.RpcCall (RpcError)
+
+rpcModule :: Module
+rpcModule =
+  Module (mkPkgName "Ribosome.Nvim.Api") (mkModName "Data")
+
+msgpackDecodeConstraint :: NeovimType -> Q (Maybe Type)
+msgpackDecodeConstraint (SimpleType "Object") =
+  Just <$> [t|MsgpackDecode $(varT $ mkName "a")|]
+msgpackDecodeConstraint _ =
+  return Nothing
+
+newT :: String -> TypeQ
+newT =
+  varT <=< newName
+
+ioReturnType :: NeovimType -> Q Type
+ioReturnType (SimpleType "Object") =
+  return (VarT $ mkName "a")
+ioReturnType a =
+  haskellType a
+
+analyzeReturnType :: NeovimType -> Q (Type, Maybe Type)
+analyzeReturnType tpe = do
+  rt <- ioReturnType tpe
+  constraint <- msgpackDecodeConstraint tpe
+  return (rt, constraint)
+
+ioSig :: Name -> [Type] -> NeovimType -> DecQ
+ioSig name types returnType = do
+  mType <- newT "m"
+  nvimConstraint <- [t|Nvim $(pure mType)|]
+  (retType, decodeConstraint) <- analyzeReturnType returnType
+  monadErrorConstraint <- [t|MonadDeepError $(newT "e") RpcError $(pure mType)|]
+  let
+    params = foldr (AppT . AppT ArrowT) (AppT mType retType) types
+    constraints = [nvimConstraint, monadErrorConstraint] <> maybeToList decodeConstraint
+  sigD name $ return (ForallT [] constraints params)
+
+ioBody :: Name -> Bool -> [Name] -> DecQ
+ioBody name _ names =
+  funD name [clause (varP <$> names) (normalB body) []]
+  where
+    responseName = mkName "response"
+    callPat = varP responseName
+    callExp = appE [|call|] args
+    checkExp = appE [|hoistEither|] (varE responseName)
+    args = foldl appE (varE $ mkName $ "RpcData." <> show name) (varE <$> names)
+    body = doE [bindS callPat callExp, noBindS checkExp]
+
+genIO :: FunctionData -> Q [Dec]
+genIO (FunctionData _ name async names types returnType) = do
+  sig <- ioSig name types returnType
+  body <- ioBody name async names
+  return [sig, body]
+
+generateIO :: Q [Dec]
+generateIO =
+  generateFromApi genIO (const . const . return $ [])
diff --git a/lib/Ribosome/Nvim/Api/IO.hs b/lib/Ribosome/Nvim/Api/IO.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Nvim/Api/IO.hs
@@ -0,0 +1,11 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Nvim.Api.IO where
+
+import Data.MessagePack (Object)
+
+import Ribosome.Nvim.Api.Data (Buffer, Tabpage, Window)
+import qualified Ribosome.Nvim.Api.Data as RpcData
+import Ribosome.Nvim.Api.GenerateIO (generateIO)
+
+generateIO
diff --git a/lib/Ribosome/Nvim/Api/RpcCall.hs b/lib/Ribosome/Nvim/Api/RpcCall.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Nvim/Api/RpcCall.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Nvim.Api.RpcCall where
+
+import Data.DeepPrisms (deepPrisms)
+import Data.Either.Combinators (mapLeft)
+import Data.MessagePack (Object)
+import Data.Text.Prettyprint.Doc (defaultLayoutOptions, layoutPretty)
+import Data.Text.Prettyprint.Doc.Render.Text (renderStrict)
+import Neovim (Neovim)
+import Neovim.Exceptions (NeovimException)
+import Neovim.Plugin.Classes (FunctionName)
+import Neovim.RPC.FunctionCall (scall)
+import System.Log.Logger (Priority(ERROR))
+
+import Ribosome.Data.ErrorReport (ErrorReport(ErrorReport))
+import Ribosome.Error.Report.Class (ReportError(..))
+import Ribosome.Msgpack.Decode (MsgpackDecode(..))
+import Ribosome.Msgpack.Util (Err)
+
+data RpcCall =
+  RpcCall {
+    rpcCallName :: FunctionName,
+    rpcCallArgs :: [Object]
+  }
+  deriving (Eq, Show)
+
+newtype AsyncRpcCall =
+  AsyncRpcCall { asyncRpcCall :: RpcCall }
+  deriving (Eq, Show)
+
+newtype SyncRpcCall =
+  SyncRpcCall { syncRpcCall :: RpcCall }
+  deriving (Eq, Show)
+
+data RpcError =
+  Decode Err
+  |
+  Nvim RpcCall NeovimException
+  |
+  Atomic Text
+  deriving Show
+
+deepPrisms ''RpcError
+
+class Rpc c a where
+  call :: c -> Neovim e (Either RpcError a)
+
+instance Rpc AsyncRpcCall () where
+  call (AsyncRpcCall c@(RpcCall name args)) =
+    mapLeft (Nvim c) <$> scall name args
+
+instance MsgpackDecode a => Rpc SyncRpcCall a where
+  call (SyncRpcCall c@(RpcCall name args)) =
+    either (Left . Nvim c) (mapLeft Decode . fromMsgpack) <$> scall name args
+
+instance ReportError RpcError where
+  errorReport (Decode err) =
+    ErrorReport "error decoding neovim response" ["RpcError.Decode:", rendered] ERROR
+    where
+      rendered = renderStrict $ layoutPretty defaultLayoutOptions err
+  errorReport (Nvim c exc)  =
+    ErrorReport "error in request to neovim" ["RpcError.Nvim:", show c, show exc] ERROR
+  errorReport (Atomic msg)  =
+    ErrorReport "error in request to neovim" ["RpcError.Atomic:", msg] ERROR
diff --git a/lib/Ribosome/Orphans.hs b/lib/Ribosome/Orphans.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Orphans.hs
@@ -0,0 +1,9 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Ribosome.Orphans where
+
+import Control.Monad.Catch (MonadCatch(..), MonadMask(..))
+import Neovim.Context.Internal (Neovim(..))
+
+deriving instance MonadCatch (Neovim e)
+deriving instance MonadMask (Neovim e)
diff --git a/lib/Ribosome/Persist.hs b/lib/Ribosome/Persist.hs
--- a/lib/Ribosome/Persist.hs
+++ b/lib/Ribosome/Persist.hs
@@ -1,55 +1,132 @@
-module Ribosome.Persist(
-  persistStore,
-  persistenceFile,
-  persistencePath,
-  defaultPersistencePath,
-  persistLoad,
-) where
+module Ribosome.Persist where
 
-import GHC.IO.Exception (IOException)
-import Control.Exception (try)
+import Control.Exception (IOException, try)
+import Control.Monad (unless)
+import Control.Monad.Catch (MonadThrow)
 import Control.Monad.IO.Class (MonadIO, liftIO)
-import Control.Monad.Trans.Except (ExceptT(ExceptT), catchE)
-import Data.Aeson (ToJSON, FromJSON, encode, eitherDecode)
-import qualified Data.ByteString.Lazy as B (writeFile, readFile, ByteString)
-import System.FilePath (takeDirectory, (</>))
-import System.Directory (getXdgDirectory, XdgDirectory(XdgCache), createDirectoryIfMissing)
-import Ribosome.Monad (liftExceptT)
-import Ribosome.Control.Ribo (Ribo)
-import qualified Ribosome.Control.Ribo as Ribo (name)
-import Ribosome.Config.Setting (settingE)
+import Data.Aeson (FromJSON, ToJSON, eitherDecodeFileStrict', encodeFile)
+import Path (Abs, Dir, File, Path, Rel, parent, parseAbsDir, parseRelDir, toFilePath, (<.>), (</>))
+import Path.IO (XdgDirectory(XdgCache), createDirIfMissing, doesFileExist, getXdgDir)
+
+import Ribosome.Config.Setting (setting)
 import qualified Ribosome.Config.Settings as S (persistenceDir)
+import Ribosome.Control.Monad.Error (recoveryFor)
+import Ribosome.Control.Monad.Ribo
+import Ribosome.Data.PersistError (PersistError)
+import qualified Ribosome.Data.PersistError as PersistError (PersistError(..))
+import Ribosome.Data.SettingError (SettingError)
 
-defaultPersistencePath :: FilePath -> IO FilePath
+defaultPersistencePath :: MonadIO m => m (Path Abs Dir)
 defaultPersistencePath =
-  getXdgDirectory XdgCache
+  liftIO $ getXdgDir XdgCache Nothing
 
-persistencePath :: FilePath -> Ribo e FilePath
+persistencePath ::
+  MonadRibo m =>
+  NvimE e m =>
+  MonadIO m =>
+  MonadThrow m =>
+  MonadDeepError e SettingError m =>
+  Path Rel File ->
+  m (Path Abs File)
 persistencePath path = do
-  name <- Ribo.name
-  let prefixed = name </> path
-  custom <- settingE S.persistenceDir
-  either (const $ liftIO $ defaultPersistencePath prefixed) (\c -> return $ c </> prefixed) custom
+  base <- defaultPersistencePath `recoveryFor` (parseAbsDir =<< setting S.persistenceDir)
+  name <- parseRelDir . toString =<< pluginName
+  return $ base </> name </> path
 
-persistenceFile :: FilePath -> Ribo e FilePath
+persistenceFile ::
+  MonadRibo m =>
+  NvimE e m =>
+  MonadIO m =>
+  MonadThrow m =>
+  MonadDeepError e SettingError m =>
+  Path Rel File ->
+  m (Path Abs File)
 persistenceFile path = do
   file <- persistencePath path
-  liftIO $ createDirectoryIfMissing True (takeDirectory file)
-  return $ file ++ ".json"
+  createDirIfMissing True (parent file)
+  file <.> "json"
 
-persistStore :: ToJSON a => FilePath -> a -> Ribo e ()
+persistStore ::
+  MonadRibo m =>
+  NvimE e m =>
+  MonadIO m =>
+  MonadThrow m =>
+  MonadDeepError e SettingError m =>
+  ToJSON a =>
+  Path Rel File ->
+  a ->
+  m ()
 persistStore path a = do
   file <- persistenceFile path
-  liftIO $ B.writeFile file (encode a)
+  liftIO $ encodeFile (toFilePath file) a
 
-noSuchFile :: Monad m => FilePath -> ExceptT String m a
-noSuchFile file = ExceptT $ return $ Left $ "persistence file " ++ file ++ " doesn't exist"
+noSuchFile :: MonadDeepError e PersistError m => Path Abs File -> m a
+noSuchFile = throwHoist . PersistError.NoSuchFile . toFilePath
 
-safeReadFile :: MonadIO m => FilePath -> m (Either IOException B.ByteString)
-safeReadFile file = liftIO $ try $ B.readFile file
+ensureExistence ::
+  MonadIO m =>
+  MonadDeepError e PersistError m =>
+  Path Abs File ->
+  m ()
+ensureExistence file = do
+  exists <- liftIO $ doesFileExist file
+  unless exists (noSuchFile file)
 
-persistLoad :: FromJSON a => FilePath -> ExceptT String (Ribo e) a
+decodeError ::
+  MonadDeepError e SettingError m =>
+  MonadDeepError e PersistError m =>
+  Path Abs File ->
+  Text ->
+  m a
+decodeError file = throwHoist . PersistError.Decode (toFilePath file)
+
+fileNotReadable ::
+  MonadDeepError e PersistError m =>
+  Path Abs File ->
+  IOException ->
+  m (Either String a)
+fileNotReadable file _ = throwHoist $ PersistError.FileNotReadable (toFilePath file)
+
+safeDecodeFile ::
+  MonadIO m =>
+  MonadDeepError e SettingError m =>
+  MonadDeepError e PersistError m =>
+  FromJSON a =>
+  Path Abs File ->
+  m a
+safeDecodeFile file = do
+  result <- either (fileNotReadable file) return =<< (liftIO . try . eitherDecodeFileStrict' . toFilePath $ file)
+  either (decodeError file) return . mapLeft toText $ result
+
+persistLoad ::
+  MonadIO m =>
+  MonadRibo m =>
+  NvimE e m =>
+  MonadThrow m =>
+  MonadDeepError e SettingError m =>
+  MonadDeepError e PersistError m =>
+  FromJSON a =>
+  Path Rel File ->
+  m a
 persistLoad path = do
-  file <- liftExceptT $ persistenceFile path
-  json <- catchE (ExceptT $ safeReadFile file) (const $ noSuchFile file)
-  ExceptT $ return $ eitherDecode json
+  file <- persistenceFile path
+  ensureExistence file
+  safeDecodeFile file
+
+mayPersistLoad ::
+  MonadIO m =>
+  MonadRibo m =>
+  NvimE e m =>
+  MonadDeepError e SettingError m =>
+  MonadDeepError e PersistError m =>
+  MonadThrow m =>
+  FromJSON a =>
+  Path Rel File ->
+  m (Maybe a)
+mayPersistLoad =
+  catchAt recover . persistLoad
+  where
+    recover (PersistError.NoSuchFile _) =
+      return Nothing
+    recover e =
+      throwHoist e
diff --git a/lib/Ribosome/Plugin.hs b/lib/Ribosome/Plugin.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Plugin.hs
@@ -0,0 +1,158 @@
+module Ribosome.Plugin (
+  module Ribosome.Plugin,
+  rpcHandler,
+  rpcHandlerDef,
+  RpcHandlerConfig(..),
+  RpcDef(..),
+) where
+
+import Control.Monad (join, (<=<))
+import Control.Monad.DeepError (MonadDeepError)
+import Control.Monad.Trans.Except (runExceptT)
+import Data.Default (def)
+import qualified Data.Map as Map ()
+import Data.MessagePack (Object(ObjectNil, ObjectBool))
+import Neovim.Context (Neovim)
+import Neovim.Plugin.Classes (
+  AutocmdOptions(acmdPattern),
+  CommandOption,
+  FunctionName(..),
+  FunctionalityDescription(..),
+  Synchronous(..),
+  )
+import Neovim.Plugin.Internal (ExportedFunctionality(..), Plugin(..))
+
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE)
+import Ribosome.Data.Mapping (MappingError)
+import Ribosome.Data.Text (capitalize)
+import Ribosome.Plugin.Builtin (deleteScratchRpc)
+import Ribosome.Plugin.Mapping (MappingHandler, handleMappingRequest)
+import Ribosome.Plugin.RpcHandler (RpcHandler(..))
+import Ribosome.Plugin.TH (rpcHandler, rpcHandlerDef)
+import Ribosome.Plugin.TH.Handler (
+  RpcDef(RpcDef),
+  RpcDefDetail(..),
+  RpcHandlerConfig(..),
+  rhcCmd,
+  )
+import Ribosome.Plugin.Watch (handleWatcherRequestSafe, watchedVariables)
+
+poll ::
+  Monad m =>
+  [Object] ->
+  m Object
+poll _ =
+  return (ObjectBool True)
+
+pollRpc ::
+  MonadDeepError e MappingError m =>
+  Text ->
+  RpcDef m
+pollRpc pluginName =
+  RpcDef (RpcFunction Sync) (capitalize pluginName <> "Poll") poll
+
+mappingHandlerRpc ::
+  MonadDeepError e MappingError m =>
+  Text ->
+  [MappingHandler m] ->
+  RpcDef m
+mappingHandlerRpc pluginName mappings =
+  RpcDef (RpcFunction Async) (capitalize pluginName <> "Mapping") (handleMappingRequest mappings)
+
+watcherRpc ::
+  MonadBaseControl IO m =>
+  MonadDeepError e MappingError m =>
+  MonadRibo m =>
+  NvimE e m =>
+  Text ->
+  Map Text (Object -> m ()) ->
+  [RpcDef m]
+watcherRpc pluginName variables =
+  chromatinInit : (rpcDef <$> ["CmdlineLeave", "BufWinEnter", "VimEnter"])
+  where
+    chromatinInit = rpcDefFromDetail ((detail "User") { raOptions = def { acmdPattern = toString ciName } }) ciName
+    ciName = "ChromatinInitialized"
+    rpcDef event =
+      rpcDefFromDetail (detail event) event
+    rpcDefFromDetail dt event =
+      RpcDef dt (name' event) (handleWatcherRequestSafe (watchedVariables variables))
+    name' event = capitalize pluginName <> "VariableChanged" <> event
+    detail event = RpcAutocmd event Async def
+
+compileRpcDef ::
+  RpcHandler e env m =>
+  (e -> m ()) ->
+  RpcDef m ->
+  ExportedFunctionality env
+compileRpcDef errorHandler (RpcDef detail name' rpcHandler') =
+  EF (wrapDetail detail (F (encodeUtf8 name')), executeRpcHandler errorHandler rpcHandler')
+  where
+    wrapDetail (RpcFunction sync') n =
+      Function n sync'
+    wrapDetail (RpcCommand options) n =
+      Command n options
+    wrapDetail (RpcAutocmd event sync' options) n =
+      Autocmd (encodeUtf8 event) n sync' options
+
+nvimPlugin ::
+  MonadDeepError e MappingError m =>
+  RpcHandler e env m =>
+  env ->
+  [[RpcDef m]] ->
+  (e -> m ()) ->
+  Plugin env
+nvimPlugin env rpcDefs errorHandler =
+  Plugin env (compileRpcDef errorHandler <$> join rpcDefs)
+
+riboPlugin ::
+  MonadBaseControl IO m =>
+  MonadDeepError e MappingError m =>
+  MonadRibo m =>
+  NvimE e m =>
+  RpcHandler e env m =>
+  Text ->
+  env ->
+  [[RpcDef m]] ->
+  [MappingHandler m] ->
+  (e -> m ()) ->
+  Map Text (Object -> m ()) ->
+  Plugin env
+riboPlugin pluginName env rpcDefs mappings errorHandler variables =
+  Plugin env ((compileRpcDef errorHandler <$> extra) ++ efs)
+  where
+    Plugin _ efs = nvimPlugin env rpcDefs errorHandler
+    extra = deleteScratchRpc pluginName : pollRpc pluginName : mappingHandlerRpc pluginName mappings : watch
+    watch = watcherRpc pluginName variables
+
+executeRpcHandler ::
+  ∀ e env m.
+  RpcHandler e env m =>
+  (e -> m ()) ->
+  ([Object] -> m Object) ->
+  [Object] ->
+  Neovim env Object
+executeRpcHandler errorHandler rpcHandler' =
+  either handleError return <=< runExceptT . native . rpcHandler'
+  where
+    handleError e =
+      ObjectNil <$ (runExceptT . native @e . errorHandler $ e)
+
+cmd :: [CommandOption] -> RpcHandlerConfig -> RpcHandlerConfig
+cmd opts conf =
+  conf { rhcCmd = Just opts }
+
+sync :: RpcHandlerConfig -> RpcHandlerConfig
+sync conf =
+  conf { rhcSync = Sync }
+
+name :: Text -> RpcHandlerConfig -> RpcHandlerConfig
+name n conf =
+  conf { rhcName = Just n }
+
+autocmd :: Text -> RpcHandlerConfig -> RpcHandlerConfig
+autocmd event conf =
+  conf { rhcAutocmd = Just event }
+
+autocmdOptions :: AutocmdOptions -> RpcHandlerConfig -> RpcHandlerConfig
+autocmdOptions options conf =
+  conf { rhcAutocmdOptions = Just options }
diff --git a/lib/Ribosome/Plugin/Builtin.hs b/lib/Ribosome/Plugin/Builtin.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Plugin/Builtin.hs
@@ -0,0 +1,29 @@
+module Ribosome.Plugin.Builtin where
+
+import Neovim.Plugin.Classes (Synchronous(Async))
+
+import Data.MessagePack (Object(ObjectString, ObjectNil))
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE)
+import Ribosome.Data.Mapping (MappingError)
+import Ribosome.Data.Text (capitalize)
+import Ribosome.Plugin.TH.Handler (RpcDef(RpcDef), RpcDefDetail(RpcFunction))
+import Ribosome.Scratch (killScratchByName)
+
+deleteScratch ::
+  MonadRibo m =>
+  NvimE e m =>
+  [Object] ->
+  m Object
+deleteScratch [ObjectString name] = do
+  ObjectNil <$ killScratchByName (decodeUtf8 name)
+deleteScratch _ =
+  return ObjectNil
+
+deleteScratchRpc ::
+  MonadDeepError e MappingError m =>
+  MonadRibo m =>
+  NvimE e m =>
+  Text ->
+  RpcDef m
+deleteScratchRpc pluginName =
+  RpcDef (RpcFunction Async) (capitalize pluginName <> "DeleteScratch") deleteScratch
diff --git a/lib/Ribosome/Plugin/Mapping.hs b/lib/Ribosome/Plugin/Mapping.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Plugin/Mapping.hs
@@ -0,0 +1,37 @@
+module Ribosome.Plugin.Mapping where
+
+import Control.Monad.DeepError (MonadDeepError(throwHoist))
+import Data.Foldable (find)
+import Data.MessagePack (Object(ObjectString, ObjectNil))
+
+import Ribosome.Data.Mapping (MappingError(InvalidArgs, NoSuchMapping), MappingIdent(MappingIdent))
+
+data MappingHandler m =
+  MappingHandler {
+    mhMapping :: MappingIdent,
+    mhHandler :: m ()
+  }
+
+mappingHandler :: Functor m => Text -> m () -> MappingHandler m
+mappingHandler ident =
+  MappingHandler (MappingIdent ident)
+
+mapping :: MappingIdent -> [MappingHandler m] -> Maybe (MappingHandler m)
+mapping ident =
+  find ((ident ==) . mhMapping)
+
+noSuchMapping :: MonadDeepError e MappingError m => MappingIdent -> m a
+noSuchMapping =
+  throwHoist . NoSuchMapping
+
+executeMapping :: Functor m => MappingHandler m -> m ()
+executeMapping (MappingHandler _ f) =
+  f
+
+handleMappingRequest :: MonadDeepError e MappingError m => [MappingHandler m] -> [Object] -> m Object
+handleMappingRequest mappings [ObjectString s] =
+  ObjectNil <$ maybe (noSuchMapping ident) executeMapping (mapping ident mappings)
+  where
+    ident = MappingIdent (decodeUtf8 s)
+handleMappingRequest _ args =
+  throwHoist (InvalidArgs args)
diff --git a/lib/Ribosome/Plugin/RpcHandler.hs b/lib/Ribosome/Plugin/RpcHandler.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Plugin/RpcHandler.hs
@@ -0,0 +1,10 @@
+module Ribosome.Plugin.RpcHandler where
+
+import Control.Monad.Trans.Except (ExceptT)
+import Neovim (Neovim)
+
+class RpcHandler e env m | m -> e env where
+  native :: m a -> ExceptT e (Neovim env) a
+
+instance RpcHandler e env (ExceptT e (Neovim env)) where
+  native = id
diff --git a/lib/Ribosome/Plugin/TH.hs b/lib/Ribosome/Plugin/TH.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Plugin/TH.hs
@@ -0,0 +1,64 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Plugin.TH where
+
+import Data.Default (def)
+import Data.Maybe (fromMaybe, maybeToList)
+import qualified Data.Text as Text (unpack)
+import Language.Haskell.TH
+import Neovim.Plugin.Classes (
+  AutocmdOptions,
+  Synchronous(..),
+  )
+
+import Ribosome.Data.String (capitalize)
+import Ribosome.Plugin.TH.Command (handlerParams, rpcCommand)
+import Ribosome.Plugin.TH.Handler (
+  RpcDef(RpcDef),
+  RpcDefDetail(RpcFunction, RpcAutocmd),
+  RpcHandlerConfig(RpcHandlerConfig),
+  argsCase,
+  defaultRpcHandlerConfig,
+  functionParamTypes,
+  lambdaNames,
+  listParamsPattern,
+  rpcLambdaWithErrorCase,
+  )
+
+functionImplementation :: Name -> ExpQ
+functionImplementation name = do
+  paramTypes <- functionParamTypes name
+  paramNames <- lambdaNames (length paramTypes)
+  rpcLambdaWithErrorCase name (argsCase name (listParamsPattern paramNames) paramNames)
+
+rpcFunction :: String -> Synchronous -> Name -> ExpQ
+rpcFunction name sync funcName = do
+  fun <- functionImplementation funcName
+  [|RpcDef (RpcFunction sync) $((litE (StringL name))) $(return fun)|]
+
+rpcAutocmd :: String -> Name -> Synchronous -> Maybe AutocmdOptions -> String -> ExpQ
+rpcAutocmd name funcName sync options event = do
+  fun <- functionImplementation funcName
+  [|RpcDef (RpcAutocmd event sync (fromMaybe def options)) $((litE (StringL name))) $(return fun)|]
+
+vimName :: Name -> Maybe String -> String
+vimName funcName =
+  capitalize . fromMaybe (nameBase funcName)
+
+rpcHandler :: (RpcHandlerConfig -> RpcHandlerConfig) -> Name -> ExpQ
+rpcHandler confTrans =
+  handler (confTrans defaultRpcHandlerConfig)
+  where
+    handler (RpcHandlerConfig sync name cmd autocmd auOptions) funcName = do
+      params <- handlerParams funcName
+      rpcFun <- rpcFunction vimName' sync funcName
+      rpcCmd <- traverse (rpcCommand vimName' funcName params) cmd
+      rpcAu <- traverse (rpcAutocmd vimName' funcName sync auOptions) (Text.unpack <$> autocmd)
+      listE $ return <$> rpcFun : maybeToList rpcCmd <> maybeToList rpcAu
+      where
+        vimName' = vimName funcName (Text.unpack <$> name)
+
+rpcHandlerDef :: Name -> ExpQ
+rpcHandlerDef =
+  rpcHandler id
diff --git a/lib/Ribosome/Plugin/TH/Command.hs b/lib/Ribosome/Plugin/TH/Command.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Plugin/TH/Command.hs
@@ -0,0 +1,285 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Plugin.TH.Command where
+
+import Control.Exception (throw)
+import Control.Monad ((<=<))
+import Data.Aeson (FromJSON, eitherDecodeStrict)
+import qualified Data.ByteString as ByteString (intercalate)
+import Data.Either.Combinators (mapLeft)
+import Data.MessagePack (Object(ObjectArray))
+import Data.Text.Prettyprint.Doc (Pretty(..))
+import Language.Haskell.TH
+import Neovim.Exceptions (NeovimException(ErrorMessage))
+import Neovim.Plugin.Classes (
+  CommandArguments,
+  CommandOption(..),
+  mkCommandOptions,
+  )
+
+import Ribosome.Msgpack.Decode (fromMsgpack)
+import Ribosome.Msgpack.Encode (MsgpackEncode(toMsgpack))
+import Ribosome.Msgpack.Util (Err)
+import Ribosome.Plugin.TH.Handler (
+  RpcDef(RpcDef),
+  RpcDefDetail(RpcCommand),
+  argsCase,
+  decodedCallSequence,
+  functionParamTypes,
+  lambdaNames,
+  listParamsPattern,
+  )
+
+data CmdParams =
+  ZeroParams
+  |
+  OnlyPrims Int
+  |
+  OnlyData
+  |
+  DataPlus Int
+  deriving (Eq, Show)
+
+data HandlerParams =
+  HandlerParams {
+    handlerHasArgsParam :: Bool,
+    handlerCmdParams :: CmdParams
+    }
+  deriving (Eq, Show)
+
+colon :: Name
+colon =
+  mkName ":"
+
+colonE :: ExpQ
+colonE =
+  varE colon
+
+colonP :: PatQ
+colonP =
+  varP colon
+
+cmdArgsCase :: Name -> [Name] -> Q Match
+cmdArgsCase handlerName paramNames =
+  argsCase handlerName (listParamsPattern (mkName "_" : paramNames)) paramNames
+
+decodeJson :: FromJSON a => [Object] -> Either Err a
+decodeJson =
+  mapLeft pretty . eitherDecodeStrict . ByteString.intercalate " " <=< traverse fromMsgpack
+
+primDispatch ::
+  String ->
+  Name ->
+  Name ->
+  Name ->
+  [Name] ->
+  Bool ->
+  ExpQ
+primDispatch rpcName argsName handlerName cmdArgsName paramNames hasArgsParam =
+  caseE (varE argsName) [matching, invalidArgs]
+  where
+    matching =
+      match (primArgPattern paramNames) (normalB $ decodedCallSequence handlerName vars) []
+    invalidArgs =
+      match wildP (normalB [|invalidArgCount $(nameLit)|]) []
+    vars = varE <$> params
+    params =
+      if hasArgsParam then cmdArgsName : paramNames else paramNames
+    nameLit =
+      litE (StringL rpcName)
+
+jsonDispatch ::
+  Name ->
+  Name ->
+  Name ->
+  [Name] ->
+  Bool ->
+  ExpQ
+jsonDispatch restName handlerName cmdArgsName paramNames hasArgsParam =
+  infixApp prims [|(<*>)|] decodedRest
+  where
+    prims = decodedCallSequence handlerName vars
+    vars = varE <$> params
+    params = if hasArgsParam then cmdArgsName : paramNames else paramNames
+    decodedRest = [|decodeJson $(varE restName)|]
+
+primArgPattern :: [Name] -> PatQ
+primArgPattern paramNames =
+  foldr f (listP []) (varP <$> paramNames)
+  where
+    f a = infixP a (mkName ":")
+
+jsonArgPattern :: [Name] -> Name -> PatQ
+jsonArgPattern paramNames restName =
+  foldr f (varP restName) (varP <$> paramNames)
+  where
+    f a = infixP a (mkName ":")
+
+newtype ArgNormalizer m =
+  ArgNormalizer (Text -> [Object] -> m (Object, [Object]))
+
+shapeError :: Text -> m a
+shapeError =
+  throw . ErrorMessage . pretty . errorMessage
+  where
+    errorMessage =
+      ("Bad argument shape for rpc command: " <>)
+
+normalizeArgsFlat ::
+  Monad m =>
+  ArgNormalizer m
+normalizeArgsFlat =
+  ArgNormalizer normalize
+  where
+    normalize _ (cmdArgs : rest) =
+      return (cmdArgs, rest)
+    normalize rpcName _ =
+      shapeError rpcName
+
+normalizeArgsPlus ::
+  Monad m =>
+  ArgNormalizer m
+normalizeArgsPlus =
+  ArgNormalizer normalize
+  where
+    normalize _ [cmdArgs, first', ObjectArray rest] =
+      return (cmdArgs, first' : rest)
+    normalize rpcName _ =
+      shapeError rpcName
+
+normalizeArgs ::
+  CmdParams ->
+  ExpQ
+normalizeArgs ZeroParams =
+  [|normalizeArgsFlat|]
+normalizeArgs (OnlyPrims 1) =
+  [|normalizeArgsFlat|]
+normalizeArgs _ =
+  [|normalizeArgsPlus|]
+
+rpc ::
+  Monad m =>
+  MsgpackEncode a =>
+  Text ->
+  ArgNormalizer m ->
+  (Object -> [Object] -> Either Err (m a)) ->
+  [Object] ->
+  m Object
+rpc rpcName (ArgNormalizer normalize) dispatch =
+  toMsgpack <$$> decodeResult . uncurry dispatch <=< normalize rpcName
+  where
+    decodeResult =
+      either (throw . ErrorMessage) id
+
+invalidArgCount :: String -> m a
+invalidArgCount =
+  throw . ErrorMessage . pretty . (msg <>)
+  where
+    msg =
+      "Wrong number of arguments for rpc handler: "
+
+command ::
+  String ->
+  Name ->
+  [Name] ->
+  HandlerParams ->
+  PatQ ->
+  (Name -> Name -> [Name] -> Bool ->ExpQ) ->
+  ExpQ
+command rpcName handlerName paramNames (HandlerParams hasCmdArgs cmdParams) argsPattern dispatch = do
+  cmdArgsName <- newName "cmdArgs"
+  let
+    handler =
+      lamE [firstParam cmdArgsName, argsPattern] (dispatch handlerName cmdArgsName paramNames hasCmdArgs)
+  [|rpc $(nameLit) $(normalizeArgs cmdParams) $(handler)|]
+  where
+    firstParam cmdArgsName =
+      if hasCmdArgs then varP cmdArgsName
+      else wildP
+    nameLit =
+      litE (StringL rpcName)
+
+primCommand ::
+  String ->
+  Name ->
+  [Name] ->
+  HandlerParams ->
+  ExpQ
+primCommand rpcName handlerName paramNames handlerPar = do
+  argsName <- newName "args"
+  command rpcName handlerName paramNames handlerPar (varP argsName) (primDispatch rpcName argsName)
+
+jsonCommand :: String -> Name -> [Name] -> HandlerParams -> ExpQ
+jsonCommand rpcName handlerName paramNames handlerPar = do
+  restName <- newName "rest"
+  command rpcName handlerName paramNames handlerPar (jsonArgPattern paramNames restName) (jsonDispatch restName)
+
+commandImplementation :: String -> Name -> HandlerParams -> ExpQ
+commandImplementation rpcName handlerName hps@(HandlerParams _ params) =
+  forParams params
+  where
+    forParams ZeroParams =
+      primCommand rpcName handlerName [] hps
+    forParams (OnlyPrims paramCount) = do
+      paramNames <- lambdaNames paramCount
+      primCommand rpcName handlerName paramNames hps
+    forParams (DataPlus paramCount) = do
+      paramNames <- lambdaNames paramCount
+      jsonCommand rpcName handlerName paramNames hps
+    forParams OnlyData =
+      jsonCommand rpcName handlerName [] hps
+
+isRecord :: Info -> Bool
+isRecord (TyConI (DataD _ _ _ _ [RecC _ _] _)) =
+  True
+isRecord _ =
+  False
+
+isJsonDecodable :: Type -> Q Bool
+isJsonDecodable (ConT name) =
+  isRecord <$> reify name
+isJsonDecodable _ =
+  return False
+
+analyzeCmdParams :: [Type] -> Q CmdParams
+analyzeCmdParams =
+  check . reverse
+  where
+    check [a] = do
+      isD <- isJsonDecodable a
+      return $ if isD then OnlyData else OnlyPrims 1
+    check (a : rest) = do
+      isD <- isJsonDecodable a
+      return $ if isD then DataPlus (length rest) else OnlyPrims (length rest + 1)
+    check [] =
+      return ZeroParams
+
+cmdNargs :: CmdParams -> CommandOption
+cmdNargs ZeroParams =
+  CmdNargs "0"
+cmdNargs (OnlyPrims 1) =
+  CmdNargs "1"
+cmdNargs _ =
+  CmdNargs "+"
+
+rpcCommand :: String -> Name -> HandlerParams -> [CommandOption] -> ExpQ
+rpcCommand rpcName funcName hps@(HandlerParams _ params) opts = do
+  fun <- commandImplementation rpcName funcName hps
+  [|RpcDef (RpcCommand $ mkCommandOptions (nargs : opts)) $((litE (StringL rpcName))) $(return fun)|]
+  where
+    nargs = cmdNargs params
+
+removeArgsParam :: [Type] -> Q (Bool, [Type])
+removeArgsParam [] =
+  return (False, [])
+removeArgsParam (p1 : rest) = do
+  argsType <- [t|CommandArguments|]
+  return $ if p1 == argsType then (True, rest) else (False, p1 : rest)
+
+handlerParams :: Name -> Q HandlerParams
+handlerParams name = do
+  types <- functionParamTypes name
+  (hasArgsParam, userTypes) <- removeArgsParam types
+  cp <- analyzeCmdParams userTypes
+  return $ HandlerParams hasArgsParam cp
diff --git a/lib/Ribosome/Plugin/TH/Handler.hs b/lib/Ribosome/Plugin/TH/Handler.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Plugin/TH/Handler.hs
@@ -0,0 +1,163 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Ribosome.Plugin.TH.Handler where
+
+import Control.Exception (throw)
+import Control.Monad (replicateM)
+import Data.Functor ((<&>))
+import Data.Maybe (maybeToList)
+import Data.MessagePack (Object)
+import Data.Text.Prettyprint.Doc (Doc, Pretty(..))
+import Data.Text.Prettyprint.Doc.Render.Terminal (AnsiStyle)
+import Language.Haskell.TH
+import Language.Haskell.TH.Syntax (Lift(..))
+import Neovim.Exceptions (NeovimException(ErrorMessage))
+import Neovim.Plugin.Classes (
+  AutocmdOptions(AutocmdOptions),
+  CommandOption(..),
+  CommandOptions,
+  RangeSpecification(..),
+  Synchronous(..),
+  )
+
+import Ribosome.Msgpack.Decode (fromMsgpack)
+import Ribosome.Msgpack.Encode (toMsgpack)
+
+data RpcHandlerConfig =
+  RpcHandlerConfig {
+    rhcSync :: Synchronous,
+    rhcName :: Maybe Text,
+    rhcCmd :: Maybe [CommandOption],
+    rhcAutocmd :: Maybe Text,
+    rhcAutocmdOptions :: Maybe AutocmdOptions
+  }
+  deriving (Eq, Show)
+
+defaultRpcHandlerConfig :: RpcHandlerConfig
+defaultRpcHandlerConfig =
+  RpcHandlerConfig Async Nothing Nothing Nothing Nothing
+
+data RpcDefDetail =
+  RpcFunction { rfSync :: Synchronous }
+  |
+  RpcCommand { rcOptions :: CommandOptions }
+  |
+  RpcAutocmd {
+    raEvent :: Text,
+    raSync :: Synchronous,
+    raOptions :: AutocmdOptions
+    }
+
+data RpcDef m =
+  RpcDef {
+    rdDetail :: RpcDefDetail,
+    rdName :: Text,
+    rdHandler :: [Object] -> m Object
+  }
+
+instance Lift Synchronous where
+  lift Sync = [|Sync|]
+  lift Async = [|Async|]
+
+instance Lift RangeSpecification where
+  lift CurrentLine =
+    [|CurrentLine|]
+  lift WholeFile =
+    [|WholeFile|]
+  lift (RangeCount a) =
+    [|RangeCount a|]
+
+instance Lift CommandOption where
+  lift (CmdSync a) =
+    [|CmdSync a|]
+  lift CmdRegister =
+    [|CmdRegister|]
+  lift (CmdNargs a) =
+    [|CmdNargs a|]
+  lift (CmdRange a) =
+    [|CmdRange a|]
+  lift (CmdCount a) =
+    [|CmdCount a|]
+  lift CmdBang =
+    [|CmdBang|]
+
+instance Lift AutocmdOptions where
+  lift (AutocmdOptions p n g) =
+    [|AutocmdOptions p n g|]
+
+unfoldFunctionParams :: Type -> [Type]
+unfoldFunctionParams (ForallT _ _ t) =
+  unfoldFunctionParams t
+unfoldFunctionParams (AppT (AppT ArrowT t) r) =
+  t : unfoldFunctionParams r
+unfoldFunctionParams _ = []
+
+functionParamTypes :: Name -> Q [Type]
+functionParamTypes name =
+  reify name <&> \case
+    (VarI _ functionType _) -> unfoldFunctionParams functionType
+    _ -> fail $ "rpc handler `" <> show name <> "` is not a function"
+
+errorBody :: Name -> BodyQ
+errorBody rpcName =
+  normalB [|throw . ErrorMessage . pretty $ ($(errLit) :: String)|]
+  where
+    errLit =
+      litE (StringL errMsg)
+    errMsg =
+      "Wrong number of arguments for rpc handler: " <> nameBase rpcName
+
+errorCase :: Name -> Q Match
+errorCase rpcName =
+  match wildP (errorBody rpcName) []
+
+failedEvaluation :: Q Match
+failedEvaluation = do
+  e <- newName "e"
+  match (conP (mkName "Left") [varP e]) (normalB [|throw . ErrorMessage $ ($(varE e) :: Doc AnsiStyle)|]) []
+
+successfulEvaluation :: Q Match
+successfulEvaluation = do
+  action <- newName "action"
+  match (conP (mkName "Right") [varP action]) (normalB [|toMsgpack <$> $(varE action)|]) []
+
+dispatchCase :: PatQ -> ExpQ -> Q Match
+dispatchCase params dispatch =
+  match params (normalB (caseE dispatch resultCases)) []
+  where
+    resultCases = [successfulEvaluation, failedEvaluation]
+
+decodedCallSequence :: Name -> [ExpQ] -> ExpQ
+decodedCallSequence handlerName =
+  foldl decodeSeq [|pure $(varE handlerName)|]
+  where
+    decodeSeq z a = infixApp z [|(<*>)|] [|fromMsgpack $(a)|]
+
+argsCase :: Name -> PatQ -> [Name] -> Q Match
+argsCase handlerName params paramNames =
+  dispatchCase params dispatch
+  where
+    dispatch = decodedCallSequence handlerName paramVars
+    paramVars = varE <$> paramNames
+
+rpcLambda :: Q Match -> Maybe (Q Match) -> ExpQ
+rpcLambda matchingArgsCase errorCase' = do
+  args <- newName "args"
+  lamE [varP args] [|$(caseE (varE args) (matchingArgsCase : maybeToList errorCase'))|]
+
+rpcLambdaWithErrorCase :: Name -> Q Match -> ExpQ
+rpcLambdaWithErrorCase funcName matchingArgsCase =
+  rpcLambda matchingArgsCase $ Just (errorCase funcName)
+
+rpcLambdaWithoutErrorCase :: Q Match -> ExpQ
+rpcLambdaWithoutErrorCase matchingArgsCase =
+  rpcLambda matchingArgsCase Nothing
+
+listParamsPattern :: [Name] -> PatQ
+listParamsPattern =
+  listP . (varP <$>)
+
+lambdaNames :: Int -> Q [Name]
+lambdaNames count =
+  replicateM count (newName "a")
diff --git a/lib/Ribosome/Plugin/Watch.hs b/lib/Ribosome/Plugin/Watch.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Plugin/Watch.hs
@@ -0,0 +1,82 @@
+module Ribosome.Plugin.Watch where
+
+import Control.Lens (Lens')
+import qualified Control.Lens as Lens (at)
+import Control.Monad.DeepError (catchAt)
+import qualified Data.Map as Map (toList)
+import Data.Map.Strict (Map)
+import Data.MessagePack (Object(ObjectNil))
+
+import Ribosome.Control.Lock (lockOrSkip)
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE, pluginInternalL, pluginInternalModifyL)
+import Ribosome.Control.Ribosome (RibosomeInternal)
+import qualified Ribosome.Control.Ribosome as RibosomeInternal (watchedVariables)
+import Ribosome.Nvim.Api.IO (vimGetVar)
+import Ribosome.Nvim.Api.RpcCall (RpcError)
+
+data WatchedVariable m =
+  WatchedVariable {
+    wvName :: Text,
+    wvHandler :: Object -> m ()
+  }
+
+watchedVariables :: Map Text (Object -> m ()) -> [WatchedVariable m]
+watchedVariables =
+  fmap create . Map.toList
+  where
+    create (name, handler) = WatchedVariable name handler
+
+storedVarLens :: Text -> Lens' RibosomeInternal (Maybe Object)
+storedVarLens name =
+  RibosomeInternal.watchedVariables . Lens.at name
+
+runHandler ::
+  MonadRibo m =>
+  WatchedVariable m ->
+  Object ->
+  m ()
+runHandler (WatchedVariable name handler) new = do
+  pluginInternalModifyL (storedVarLens name) (const (Just new))
+  handler new
+
+compareVar ::
+  MonadRibo m =>
+  WatchedVariable m ->
+  Maybe Object ->
+  Object ->
+  m ()
+compareVar _ (Just old) new | old == new =
+  return ()
+compareVar wv _ new =
+  runHandler wv new
+
+checkVar ::
+  MonadRibo m =>
+  NvimE e m =>
+  WatchedVariable m ->
+  m ()
+checkVar wv@(WatchedVariable name _) = do
+  old <- pluginInternalL (storedVarLens name)
+  new <- catchAt @RpcError recover (Right <$> vimGetVar name)
+  traverse_ (compareVar wv old) new
+  where
+  recover _ = return (Left ())
+
+handleWatcherRequest ::
+  MonadRibo m =>
+  NvimE e m =>
+  [WatchedVariable m] ->
+  [Object] ->
+  m Object
+handleWatcherRequest variables _ =
+  ObjectNil <$ traverse_ checkVar variables
+
+handleWatcherRequestSafe ::
+  MonadBaseControl IO m =>
+  MonadRibo m =>
+  NvimE e m =>
+  [WatchedVariable m] ->
+  [Object] ->
+  m Object
+handleWatcherRequestSafe variables o =
+  ObjectNil <$ lockOrSkip "variable-watcher" (void $ handleWatcherRequest variables o)
diff --git a/lib/Ribosome/Prelude.hs b/lib/Ribosome/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Prelude.hs
@@ -0,0 +1,95 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Ribosome.Prelude (
+  module Control.Monad.DeepError,
+  module Control.Monad.DeepState,
+  module Control.Monad.Trans.Control,
+  module Data.DeepLenses,
+  module Data.DeepPrisms,
+  module Data.Default,
+  module Data.Foldable,
+  module Relude,
+  dbg,
+  dbgs,
+  dbgm,
+  makeClassy,
+  mapLeft,
+  modify,
+  tuple,
+  undefined,
+  unit,
+  unsafeLog,
+  unsafeLogS,
+  (<$$>),
+) where
+
+import Control.Lens (makeClassy)
+import Control.Monad.DeepError (MonadDeepError(throwHoist), catchAs, catchAt, hoistEither, hoistMaybe, ignoreError)
+import Control.Monad.DeepState (
+  MonadDeepState,
+  get,
+  getL,
+  gets,
+  getsL,
+  modifyL,
+  modifyM,
+  modifyM',
+  put,
+  setL,
+  stateM,
+  )
+import qualified Control.Monad.DeepState as DeepState (modify)
+import Control.Monad.Trans.Control (MonadBaseControl)
+import Data.DeepLenses (deepLenses)
+import Data.DeepPrisms (deepPrisms)
+import Data.Default (Default(def))
+import Data.Either.Combinators (mapLeft)
+import Data.Foldable (foldl, traverse_)
+import Data.Functor (void)
+import Data.Functor.Syntax ((<$$>))
+import GHC.Err (undefined)
+import GHC.IO.Unsafe (unsafePerformIO)
+import Relude hiding (undefined, Type, state, modify, gets, get, put)
+
+dbg :: Monad m => Text -> m ()
+dbg msg = do
+  () <- return $ unsafePerformIO (putStrLn msg)
+  return ()
+
+dbgs :: Monad m => Show a => a -> m ()
+dbgs =
+  dbg . show
+
+dbgm :: Monad m => Show a => m a -> m a
+dbgm ma = do
+  a <- ma
+  dbgs a
+  return a
+
+unit ::
+  Applicative f =>
+  f ()
+unit =
+  pure ()
+
+tuple ::
+  Applicative f =>
+  f a ->
+  f b ->
+  f (a, b)
+tuple fa fb =
+  (,) <$> fa <*> fb
+
+unsafeLogS :: Show a => a -> b -> b
+unsafeLogS a b = unsafePerformIO $ print a >> return b
+
+unsafeLog :: Text -> b -> b
+unsafeLog a b = unsafePerformIO $ putStrLn a >> return b
+
+modify ::
+  ∀ s' s m .
+  MonadDeepState s s' m =>
+  (s' -> s') ->
+  m ()
+modify =
+  DeepState.modify
diff --git a/lib/Ribosome/PreludeExport.hs b/lib/Ribosome/PreludeExport.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/PreludeExport.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Ribosome.PreludeExport (
+  module Ribosome.Prelude,
+  module Ribosome.Control.Monad.Ribo,
+  module Ribosome.Log,
+  module Ribosome.Nvim.Api.RpcCall,
+  module Ribosome.Msgpack.Decode,
+  module Ribosome.Msgpack.Encode,
+  sleep,
+) where
+
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE)
+import Ribosome.Log (logDebug, logError, logInfo, showDebug)
+import Ribosome.Msgpack.Decode (MsgpackDecode(fromMsgpack))
+import Ribosome.Msgpack.Encode (MsgpackEncode(toMsgpack))
+import Ribosome.Nvim.Api.RpcCall (RpcError(..))
+import Ribosome.Prelude
+import Ribosome.System.Time (sleep)
diff --git a/lib/Ribosome/Scratch.hs b/lib/Ribosome/Scratch.hs
--- a/lib/Ribosome/Scratch.hs
+++ b/lib/Ribosome/Scratch.hs
@@ -1,83 +1,237 @@
-module Ribosome.Scratch(
-  showInScratch,
-) where
+module Ribosome.Scratch where
 
-import Neovim (
-  Neovim,
-  Buffer,
-  Window,
-  Tabpage,
-  toObject,
-  vim_command',
-  vim_get_current_window',
-  vim_get_current_tabpage',
-  buffer_set_option',
-  buffer_set_name',
-  window_get_buffer',
-  window_set_option',
-  )
+import Control.Lens (Lens')
+import qualified Control.Lens as Lens (at, set)
+import Control.Monad (unless)
+import Data.Default (Default(def))
+import Data.Foldable (traverse_)
+
+import Ribosome.Api.Buffer (setBufferContent, wipeBuffer)
+import Ribosome.Api.Syntax (executeWindowSyntax)
+import Ribosome.Api.Tabpage (closeTabpage)
+import Ribosome.Api.Window (closeWindow)
+import Ribosome.Control.Monad.Ribo (MonadRibo, NvimE, pluginInternalL, pluginInternalModify, pluginName)
+import Ribosome.Control.Ribosome (RibosomeInternal)
+import qualified Ribosome.Control.Ribosome as Ribosome (scratch)
 import Ribosome.Data.Scratch (Scratch(Scratch))
+import qualified Ribosome.Data.Scratch as Scratch (Scratch(scratchPrevious, scratchWindow))
 import Ribosome.Data.ScratchOptions (ScratchOptions(ScratchOptions))
-import Ribosome.Api.Buffer (setBufferContent)
+import qualified Ribosome.Data.ScratchOptions as ScratchOptions (ScratchOptions(name, resize, maxSize))
+import Ribosome.Data.Text (capitalize)
+import Ribosome.Mapping (activateBufferMapping)
+import Ribosome.Msgpack.Encode (toMsgpack)
+import Ribosome.Msgpack.Error (DecodeError)
+import Ribosome.Nvim.Api.Data (Buffer, Tabpage, Window)
+import Ribosome.Nvim.Api.IO (
+  bufferGetNumber,
+  bufferSetName,
+  bufferSetOption,
+  vimCommand,
+  vimGetCurrentTabpage,
+  vimGetCurrentWindow,
+  vimSetCurrentWindow,
+  windowGetBuffer,
+  windowIsValid,
+  windowSetHeight,
+  windowSetOption,
+  )
+import Ribosome.Nvim.Api.RpcCall (RpcError)
 
-createScratchTab :: Neovim e Tabpage
+createScratchTab :: NvimE e m => m Tabpage
 createScratchTab = do
-  vim_command' "tabnew"
-  vim_get_current_tabpage'
+  vimCommand "tabnew"
+  vimGetCurrentTabpage
 
-createScratchWindow :: Bool -> Bool -> Maybe Int -> Neovim e Window
-createScratchWindow vertical wrap size = do
-  vim_command' $ prefix ++ cmd
-  win <- vim_get_current_window'
-  window_set_option' win "wrap" (toObject wrap)
+createScratchWindow :: NvimE e m => Bool -> Bool -> Bool -> Maybe Int -> m Window
+createScratchWindow vertical wrap bottom size = do
+  vimCommand $ prefix <> cmd
+  win <- vimGetCurrentWindow
+  windowSetOption win "wrap" (toMsgpack wrap)
+  windowSetOption win "number" (toMsgpack False)
+  windowSetOption win "cursorline" (toMsgpack True)
+  when bottom (vimCommand "wincmd J")
   return win
   where
     cmd = if vertical then "vnew" else "new"
     prefix = maybe "" show size
 
-createScratchUiInTab :: Neovim e (Window, Maybe Tabpage)
+createScratchUiInTab :: NvimE e m => m (Window, Maybe Tabpage)
 createScratchUiInTab = do
   tab <- createScratchTab
-  win <- vim_get_current_window'
+  win <- vimGetCurrentWindow
   return (win, Just tab)
 
-createScratchUiInWindow :: Bool -> Bool -> Maybe Int -> Neovim e (Window, Maybe Tabpage)
-createScratchUiInWindow vertical wrap size = do
-  win <- createScratchWindow vertical wrap size
+createScratchUiInWindow :: NvimE e m => Bool -> Bool -> Bool -> Maybe Int -> m (Window, Maybe Tabpage)
+createScratchUiInWindow vertical wrap bottom size = do
+  win <- createScratchWindow vertical wrap bottom size
   return (win, Nothing)
 
-createScratchUi :: Bool -> Bool -> Bool -> Maybe Int -> Neovim e (Window, Maybe Tabpage)
-createScratchUi True _ _ _ =
+createScratchUi :: NvimE e m => ScratchOptions -> m (Window, Maybe Tabpage)
+createScratchUi (ScratchOptions False vertical wrap _ _ bottom _ size _ _ _) =
+  createScratchUiInWindow vertical wrap bottom size
+createScratchUi _ =
   createScratchUiInTab
-createScratchUi False vertical wrap size =
-  createScratchUiInWindow vertical wrap size
 
-configureScratchBuffer :: Buffer -> String -> Neovim e ()
+configureScratchBuffer :: NvimE e m => Buffer -> Text -> m ()
 configureScratchBuffer buffer name = do
-  buffer_set_option' buffer "buftype" (toObject ("nofile" :: String))
-  buffer_set_option' buffer "bufhidden" (toObject ("wipe" :: String))
-  buffer_set_name' buffer name
+  bufferSetOption buffer "buftype" (toMsgpack ("nofile" :: Text))
+  bufferSetOption buffer "bufhidden" (toMsgpack ("wipe" :: Text))
+  bufferSetName buffer name
 
-setupScratchBuffer :: Window -> String -> Neovim e Buffer
+setupScratchBuffer :: NvimE e m => Window -> Text -> m Buffer
 setupScratchBuffer window name = do
-  buffer <- window_get_buffer' window
+  buffer <- windowGetBuffer window
   configureScratchBuffer buffer name
   return buffer
 
-createScratch :: ScratchOptions -> Neovim e Scratch
-createScratch (ScratchOptions useTab vertical size wrap name) = do
-  (window, tab) <- createScratchUi useTab vertical wrap size
+scratchLens :: Text -> Lens' RibosomeInternal (Maybe Scratch)
+scratchLens name =
+  Ribosome.scratch . Lens.at name
+
+setupScratchIn ::
+  MonadDeepError e DecodeError m =>
+  MonadRibo m =>
+  NvimE e m =>
+  Window ->
+  Window ->
+  Maybe Tabpage ->
+  ScratchOptions ->
+  m Scratch
+setupScratchIn previous window tab (ScratchOptions useTab _ _ focus _ _ _ _ syntax mappings name) = do
   buffer <- setupScratchBuffer window name
-  return (Scratch buffer window tab)
+  traverse_ (executeWindowSyntax window) syntax
+  traverse_ (activateBufferMapping buffer) mappings
+  unless (focus || useTab) $ vimSetCurrentWindow previous
+  let scratch = Scratch name buffer window previous tab
+  pluginInternalModify $ Lens.set (scratchLens name) (Just scratch)
+  setupDeleteAutocmd scratch
+  return scratch
 
-setScratchContent :: Scratch -> [String] -> Neovim e ()
-setScratchContent (Scratch buffer _ _) lines' = do
-  buffer_set_option' buffer "modifiable" (toObject True)
-  setBufferContent buffer lines'
-  buffer_set_option' buffer "modifiable" (toObject False)
+setupDeleteAutocmd ::
+  MonadRibo m =>
+  NvimE e m =>
+  Scratch ->
+  m ()
+setupDeleteAutocmd (Scratch name buffer _ _ _) = do
+  pname <- capitalize <$> pluginName
+  number <- bufferGetNumber buffer
+  vimCommand "augroup RibosomeScratch"
+  vimCommand $ "autocmd RibosomeScratch BufDelete <buffer=" <> show number <> "> " <> deleteCall pname
+  where
+    deleteCall pname =
+      "silent! call " <> pname <> "DeleteScratch('" <> name <> "')"
 
-showInScratch :: [String] -> ScratchOptions -> Neovim e Scratch
+createScratch ::
+  MonadDeepError e DecodeError m =>
+  MonadRibo m =>
+  NvimE e m =>
+  ScratchOptions ->
+  m Scratch
+createScratch options = do
+  previous <- vimGetCurrentWindow
+  (window, tab) <- createScratchUi options
+  setupScratchIn previous window tab options
+
+updateScratch ::
+  MonadDeepError e DecodeError m =>
+  MonadRibo m =>
+  NvimE e m =>
+  Scratch ->
+  ScratchOptions ->
+  m Scratch
+updateScratch (Scratch _ _ oldWindow _ oldTab) options = do
+  previous <- vimGetCurrentWindow
+  winValid <- windowIsValid oldWindow
+  (window, tab) <- if winValid then return (oldWindow, oldTab) else createScratchUi options
+  setupScratchIn previous window tab options
+
+lookupScratch ::
+  MonadRibo m =>
+  Text ->
+  m (Maybe Scratch)
+lookupScratch name =
+  pluginInternalL (scratchLens name)
+
+ensureScratch ::
+  MonadDeepError e DecodeError m =>
+  MonadRibo m =>
+  NvimE e m =>
+  ScratchOptions ->
+  m Scratch
+ensureScratch options = do
+  f <- maybe createScratch updateScratch <$> lookupScratch (ScratchOptions.name options)
+  f options
+
+setScratchContent ::
+  Foldable t =>
+  NvimE e m =>
+  ScratchOptions ->
+  Scratch ->
+  t Text ->
+  m ()
+setScratchContent options (Scratch _ buffer win _ _) lines' = do
+  bufferSetOption buffer "modifiable" (toMsgpack True)
+  setBufferContent buffer (toList lines')
+  bufferSetOption buffer "modifiable" (toMsgpack False)
+  when (ScratchOptions.resize options) (ignoreError @RpcError $ windowSetHeight win size)
+  where
+    size = max 1 $ min (length lines') (fromMaybe 30 (ScratchOptions.maxSize options))
+
+showInScratch ::
+  Foldable t =>
+  MonadDeepError e DecodeError m =>
+  MonadRibo m =>
+  NvimE e m =>
+  t Text ->
+  ScratchOptions ->
+  m Scratch
 showInScratch lines' options = do
-  scratch <- createScratch options
-  setScratchContent scratch lines'
+  scratch <- ensureScratch options
+  setScratchContent options scratch lines'
   return scratch
+
+showInScratchDef ::
+  Foldable t =>
+  MonadDeepError e DecodeError m =>
+  MonadRibo m =>
+  NvimE e m =>
+  t Text ->
+  m Scratch
+showInScratchDef lines' =
+  showInScratch lines' def
+
+killScratch ::
+  MonadRibo m =>
+  NvimE e m =>
+  Scratch ->
+  m ()
+killScratch (Scratch name buffer window _ tab) = do
+  catchAs @RpcError () removeAutocmd
+  traverse_ closeTabpage tab *> closeWindow window *> wipeBuffer buffer
+  pluginInternalModify $ Lens.set (scratchLens name) Nothing
+  where
+    removeAutocmd = do
+      number <- bufferGetNumber buffer
+      vimCommand $ "autocmd! RibosomeScratch BufDelete <buffer=" <> show number <> ">"
+
+killScratchByName ::
+  MonadRibo m =>
+  NvimE e m =>
+  Text ->
+  m ()
+killScratchByName name = do
+  traverse_ killScratch =<< lookupScratch name
+
+scratchPreviousWindow ::
+  MonadRibo m =>
+  Text ->
+  m (Maybe Window)
+scratchPreviousWindow =
+  fmap Scratch.scratchPrevious <$$> lookupScratch
+
+scratchWindow ::
+  MonadRibo m =>
+  Text ->
+  m (Maybe Window)
+scratchWindow =
+  fmap Scratch.scratchWindow <$$> lookupScratch
diff --git a/lib/Ribosome/System/Time.hs b/lib/Ribosome/System/Time.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/System/Time.hs
@@ -0,0 +1,20 @@
+module Ribosome.System.Time where
+
+import Control.Concurrent (threadDelay)
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Data.Time.Clock.POSIX (getPOSIXTime)
+import GHC.Float (word2Double)
+
+epochSeconds :: MonadIO m => m Int
+epochSeconds = liftIO $ fmap round getPOSIXTime
+
+usleep :: MonadIO m => Double -> m ()
+usleep =
+  liftIO . threadDelay . round
+
+sleep :: MonadIO m => Double -> m ()
+sleep seconds =
+  usleep $ seconds * 1e6
+
+sleepW :: MonadIO m => Word -> m ()
+sleepW = sleep . word2Double
diff --git a/lib/Ribosome/Test/Embed.hs b/lib/Ribosome/Test/Embed.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Embed.hs
+++ /dev/null
@@ -1,180 +0,0 @@
-module Ribosome.Test.Embed(
-  defaultTestConfig,
-  defaultTestConfigWith,
-  TestConfig (..),
-  Vars(..),
-  unsafeEmbeddedSpec,
-  setVars,
-  setupPluginEnv,
-  quitNvim,
-) where
-
-import Control.Monad.IO.Class (liftIO)
-import Control.Monad.Reader (runReaderT)
-import Control.Monad.Trans.Resource (runResourceT)
-import Data.Default (Default(def))
-import Data.Foldable (traverse_)
-import Data.Functor (void)
-import Data.Maybe (fromMaybe)
-import GHC.IO.Handle (Handle)
-import Neovim (Neovim, Object, vim_set_var', vim_command)
-import qualified Neovim.Context.Internal as Internal(
-  Neovim(Neovim),
-  Config,
-  newConfig,
-  retypeConfig,
-  mkFunctionMap,
-  pluginSettings,
-  globalFunctionMap,
-  )
-import Neovim.RPC.Common (RPCConfig, newRPCConfig)
-import Neovim.RPC.EventHandler (runEventHandler)
-import Neovim.RPC.SocketReader (runSocketReader)
-import System.Directory (makeAbsolute)
-import System.Exit (ExitCode)
-import qualified System.Posix.Signals as Signal (signalProcess, killProcess)
-import System.Process (getPid)
-import System.Process.Typed (
-  ProcessConfig,
-  Process,
-  withProcess,
-  proc,
-  setStdin,
-  setStdout,
-  getStdin,
-  getStdout,
-  unsafeProcessHandle,
-  createPipe,
-  getExitCode,
-  )
-import UnliftIO.Async (async, cancel, race)
-import UnliftIO.Exception (tryAny, bracket)
-import UnliftIO.STM (atomically, putTMVar)
-
-import Ribosome.Api.Option (rtpCat)
-import Ribosome.Control.Ribo (Ribo)
-import Ribosome.Control.Ribosome (Ribosome(Ribosome), newInternalTVar)
-import Ribosome.Data.Time (sleep, sleepW)
-
-type Runner env = TestConfig -> Neovim env () -> Neovim env ()
-
-newtype Vars = Vars [(String, Object)]
-
-data TestConfig =
-  TestConfig {
-    tcPluginName :: String,
-    tcExtraRtp :: String,
-    tcLogPath :: FilePath,
-    tcTimeout :: Word,
-    tcCmdline :: Maybe [String],
-    tcCmdArgs :: [String],
-    tcVariables :: Vars
-  }
-
-instance Default TestConfig where
-  def = TestConfig "ribosome" "test/f/fixtures/rtp" "test/f/temp/log" 5 def def (Vars [])
-
-defaultTestConfigWith :: String -> Vars -> TestConfig
-defaultTestConfigWith name = TestConfig name "test/f/fixtures/rtp" "test/f/temp/log" 5 def def
-
-defaultTestConfig :: String -> TestConfig
-defaultTestConfig name = defaultTestConfigWith name (Vars [])
-
-setVars :: Vars -> Neovim e ()
-setVars (Vars vars) =
-  traverse_ (uncurry vim_set_var') vars
-
-setupPluginEnv :: TestConfig -> Neovim e ()
-setupPluginEnv (TestConfig _ rtp _ _ _ _ vars) = do
-  absRtp <- liftIO $ makeAbsolute rtp
-  rtpCat absRtp
-  setVars vars
-
-killPid :: Integral a => a -> IO ()
-killPid =
-  void . tryAny . Signal.signalProcess Signal.killProcess . fromIntegral
-
-killProcess :: Process i o e -> IO ()
-killProcess prc = do
-  let handle = unsafeProcessHandle prc
-  mayPid <- getPid handle
-  traverse_ killPid mayPid
-
-testNvimProcessConfig :: TestConfig -> ProcessConfig Handle Handle ()
-testNvimProcessConfig TestConfig {..} =
-  setStdin createPipe $ setStdout createPipe $ proc "nvim" $ args ++ tcCmdArgs
-  where
-    args = fromMaybe defaultArgs tcCmdline
-    defaultArgs = ["--embed", "-n", "-u", "NONE", "-i", "NONE"]
-
-startHandlers :: NvimProc -> TestConfig -> Internal.Config RPCConfig -> IO (IO ())
-startHandlers prc TestConfig{..} nvimConf = do
-  socketReader <- run runSocketReader getStdout
-  eventHandler <- run runEventHandler getStdin
-  atomically $ putTMVar (Internal.globalFunctionMap nvimConf) (Internal.mkFunctionMap [])
-  let stopEventHandlers = traverse_ cancel [socketReader, eventHandler]
-  return stopEventHandlers
-  where
-    run runner stream = async . void $ runner (stream prc) emptyConf
-    emptyConf = nvimConf { Internal.pluginSettings = Nothing }
-
-runNeovimThunk :: Internal.Config e -> Neovim e a -> IO ()
-runNeovimThunk cfg (Internal.Neovim thunk) =
-  void $ runReaderT (runResourceT thunk) cfg
-
-type NvimProc = Process Handle Handle ()
-
-waitQuit :: NvimProc -> IO (Maybe ExitCode)
-waitQuit prc =
-  wait 30
-  where
-    wait :: Int -> IO (Maybe ExitCode)
-    wait 0 = return Nothing
-    wait count = do
-      code <- getExitCode prc
-      case code of
-        Just a -> return $ Just a
-        Nothing -> do
-          sleep 0.1
-          wait $ count - 1
-
-quitNvim :: Internal.Config e -> NvimProc -> IO ()
-quitNvim testCfg prc = do
-  quitThread <- async $ runNeovimThunk testCfg quit
-  result <- waitQuit prc
-  case result of
-    Just _ -> return ()
-    Nothing -> killProcess prc
-  cancel quitThread
-  where
-    quit = vim_command "qall!"
-
-shutdownNvim :: Internal.Config e -> NvimProc -> IO () -> IO ()
-shutdownNvim _ prc stopEventHandlers = do
-  stopEventHandlers
-  killProcess prc
-  -- quitNvim testCfg prc
-
-runTest :: TestConfig -> Internal.Config (Ribosome e) -> Ribo e () -> IO () -> IO ()
-runTest TestConfig{..} testCfg thunk _ = do
-  result <- race (sleepW tcTimeout) (runNeovimThunk testCfg thunk)
-  case result of
-    Right _ -> return ()
-    Left _ -> fail $ "test exceeded timeout of " ++ show tcTimeout ++ " seconds"
-
-runEmbeddedNvim :: TestConfig -> Ribosome e -> Ribo e () -> NvimProc -> IO ()
-runEmbeddedNvim conf ribo thunk prc = do
-  nvimConf <- Internal.newConfig (pure Nothing) newRPCConfig
-  let testCfg = Internal.retypeConfig ribo nvimConf
-  bracket (startHandlers prc conf nvimConf) (shutdownNvim testCfg prc) (runTest conf testCfg thunk)
-
-runEmbedded :: TestConfig -> Ribosome e -> Ribo e () -> IO ()
-runEmbedded conf ribo thunk = do
-  let pc = testNvimProcessConfig conf
-  withProcess pc $ runEmbeddedNvim conf ribo thunk
-
-unsafeEmbeddedSpec :: Runner (Ribosome e) -> TestConfig -> e -> Ribo e () -> IO ()
-unsafeEmbeddedSpec runner conf env spec = do
-  internal <- newInternalTVar
-  let ribo = Ribosome (tcPluginName conf) internal env
-  runEmbedded conf ribo $ runner conf spec
diff --git a/lib/Ribosome/Test/Exists.hs b/lib/Ribosome/Test/Exists.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Exists.hs
+++ /dev/null
@@ -1,41 +0,0 @@
-module Ribosome.Test.Exists(
-  waitForPlugin,
-) where
-
-import Control.Monad.IO.Class (MonadIO)
-import Data.Char (toUpper)
-import Neovim
-
-import Ribosome.Data.Time (epochSeconds, sleep)
-
-capitalize :: String -> String
-capitalize [] = []
-capitalize (head' : tail') = toUpper head' : tail'
-
-retry :: MonadIO f => Double -> Int -> f a -> (a -> f (Either String b)) -> f b
-retry interval timeout thunk check = do
-  start <- epochSeconds
-  step start
-  where
-    step start = do
-      result <- thunk
-      checked <- check result
-      recurse start checked
-    recurse _ (Right b) = return b
-    recurse start (Left e) = do
-      current <- epochSeconds
-      if (current - start) < timeout
-      then do
-        sleep interval
-        step start
-      else fail e
-
-waitForPlugin :: String -> Double -> Int -> Neovim env ()
-waitForPlugin name interval timeout =
-  retry interval timeout thunk check
-  where
-    thunk = vim_call_function "exists" [toObject $ "*" ++ capitalize name ++ "Poll"]
-    check (Right (ObjectInt 1)) = return $ Right ()
-    check (Right a) = return $ Left $ errormsg ++ "weird return type " ++ show a
-    check (Left e) = return $ Left $ errormsg ++ show e
-    errormsg = "plugin didn't start within " ++ show timeout ++ " seconds: "
diff --git a/lib/Ribosome/Test/File.hs b/lib/Ribosome/Test/File.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/File.hs
+++ /dev/null
@@ -1,32 +0,0 @@
-module Ribosome.Test.File(
-  tempDirIO,
-  tempDir,
-  fixture,
-) where
-
-import Control.Monad.IO.Class (liftIO, MonadIO)
-import System.Directory (canonicalizePath, createDirectoryIfMissing, removePathForcibly)
-import System.FilePath ((</>))
-
-testDir :: String -> IO FilePath
-testDir prefix = canonicalizePath $ "test" </> prefix
-
--- raises exception if cwd is not the package root so we don't damage anything
-tempDirIO :: String -> FilePath -> IO FilePath
-tempDirIO prefix path = do
-  base <- testDir prefix
-  let dir = base </> "temp"
-  removePathForcibly dir
-  createDirectoryIfMissing False dir
-  let absPath = dir </> path
-  createDirectoryIfMissing True absPath
-  return absPath
-
-tempDir :: MonadIO m => String -> FilePath -> m FilePath
-tempDir prefix path =
-  liftIO $ tempDirIO prefix path
-
-fixture :: MonadIO m => String -> FilePath -> m FilePath
-fixture prefix path = do
-  base <- liftIO $ testDir prefix
-  return $ base </> "fixtures" </> path
diff --git a/lib/Ribosome/Test/Functional.hs b/lib/Ribosome/Test/Functional.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Functional.hs
+++ /dev/null
@@ -1,81 +0,0 @@
-module Ribosome.Test.Functional(
-  startPlugin,
-  fSpec,
-  functionalSpec,
-  tempDir,
-  tempFile,
-  fixture,
-) where
-
-import Control.Exception (finally)
-import Control.Monad (when)
-import Control.Monad.IO.Class
-import Data.Foldable (traverse_)
-import Neovim (Neovim, vim_command')
-import System.Console.ANSI (setSGR, SGR(SetColor, Reset), ConsoleLayer(Foreground), ColorIntensity(Dull), Color(Green))
-import System.Directory (getCurrentDirectory, createDirectoryIfMissing, removePathForcibly, doesFileExist, makeAbsolute)
-import System.FilePath (takeDirectory, (</>), takeFileName)
-
-import Ribosome.Control.Ribo (Ribo)
-import Ribosome.Test.Embed (TestConfig(..), unsafeEmbeddedSpec, setupPluginEnv)
-import Ribosome.Test.Exists (waitForPlugin)
-import qualified Ribosome.Test.File as F (tempDir, fixture)
-
-jobstart :: MonadIO f => String -> f String
-jobstart cmd = do
-  dir <- liftIO getCurrentDirectory
-  return $ "call jobstart('" ++ cmd ++ "', { 'rpc': v:true, 'cwd': '" ++ dir ++ "' })"
-
-logFile :: TestConfig -> IO FilePath
-logFile TestConfig{..} = makeAbsolute $ tcLogPath ++ "-spec"
-
-startPlugin :: TestConfig -> Neovim env ()
-startPlugin tc@TestConfig{..} = do
-  absLogPath <- liftIO $ makeAbsolute tcLogPath
-  absLogFile <- liftIO $ logFile tc
-  liftIO $ createDirectoryIfMissing True (takeDirectory absLogPath)
-  liftIO $ removePathForcibly absLogFile
-  setupPluginEnv tc
-  cmd <- jobstart $ "stack run -- -l " ++ absLogFile ++ " -v INFO"
-  vim_command' cmd
-  waitForPlugin tcPluginName 0.1 3
-
-fSpec :: TestConfig -> Neovim env () -> Neovim env ()
-fSpec conf spec = startPlugin conf >> spec
-
-showLog' :: String -> IO ()
-showLog' output = do
-  putStrLn ""
-  setSGR [SetColor Foreground Dull Green]
-  putStrLn "plugin output:"
-  setSGR [Reset]
-  traverse_ putStrLn (lines output)
-  putStrLn ""
-
-showLog :: TestConfig -> IO ()
-showLog conf = do
-  file <- logFile conf
-  exists <- doesFileExist file
-  when exists $ do
-    output <- readFile file
-    case output of
-      [] -> return ()
-      o -> showLog' o
-
-functionalSpec :: TestConfig -> Ribo () () -> IO ()
-functionalSpec conf spec =
-  finally (unsafeEmbeddedSpec fSpec conf () spec) (showLog conf)
-
-fPrefix :: String
-fPrefix = "f"
-
-tempDir :: FilePath -> Neovim e FilePath
-tempDir = F.tempDir fPrefix
-
-tempFile :: FilePath -> Neovim e FilePath
-tempFile file = do
-  absDir <- tempDir $ takeDirectory file
-  return $ absDir </> takeFileName file
-
-fixture :: MonadIO m => FilePath -> m FilePath
-fixture = F.fixture fPrefix
diff --git a/lib/Ribosome/Test/Unit.hs b/lib/Ribosome/Test/Unit.hs
deleted file mode 100644
--- a/lib/Ribosome/Test/Unit.hs
+++ /dev/null
@@ -1,37 +0,0 @@
-module Ribosome.Test.Unit(
-  unitSpec,
-  tempDir,
-  tempFile,
-  uPrefix,
-  fixture,
-) where
-
-import Control.Monad.IO.Class (MonadIO)
-import Neovim (Neovim)
-import Ribosome.Control.Ribo (Ribo)
-import Ribosome.Test.Embed (TestConfig(..), setupPluginEnv, unsafeEmbeddedSpec)
-import qualified Ribosome.Test.File as F (tempDir, fixture)
-import System.FilePath (takeDirectory, takeFileName, (</>))
-
-uPrefix :: String
-uPrefix = "u"
-
-uSpec :: TestConfig -> Neovim env () -> Neovim env ()
-uSpec conf spec = do
-  setupPluginEnv conf
-  spec
-
-unitSpec :: TestConfig -> e -> Ribo e () -> IO ()
-unitSpec =
-  unsafeEmbeddedSpec uSpec
-
-tempDir :: FilePath -> Neovim e FilePath
-tempDir = F.tempDir uPrefix
-
-tempFile :: FilePath -> Neovim e FilePath
-tempFile file = do
-  absDir <- tempDir $ takeDirectory file
-  return $ absDir </> takeFileName file
-
-fixture :: MonadIO m => FilePath -> m FilePath
-fixture = F.fixture uPrefix
diff --git a/lib/Ribosome/Tmux/Run.hs b/lib/Ribosome/Tmux/Run.hs
new file mode 100644
--- /dev/null
+++ b/lib/Ribosome/Tmux/Run.hs
@@ -0,0 +1,36 @@
+module Ribosome.Tmux.Run where
+
+import Chiasma.Data.TmuxError (TmuxError)
+import Chiasma.Monad.Stream (TmuxProg)
+import qualified Chiasma.Monad.Stream as Chiasma (runTmux)
+import Chiasma.Native.Api (TmuxNative(TmuxNative))
+import Control.Monad.Catch (MonadMask)
+import Control.Monad.DeepError (MonadDeepError)
+import Control.Monad.IO.Class (MonadIO)
+import Control.Monad.Trans.Except (ExceptT, runExceptT)
+import Data.DeepPrisms (DeepPrisms)
+
+import Ribosome.Config.Setting (settingMaybe)
+import Ribosome.Config.Settings (tmuxSocket)
+import Ribosome.Control.Monad.Ribo (MonadRibo, Nvim, Ribo)
+
+runTmux ::
+  (MonadIO m, MonadRibo m, MonadDeepError e TmuxError m, MonadMask m, Nvim m) =>
+  TmuxProg m a ->
+  m a
+runTmux prog = do
+  socket <- settingMaybe tmuxSocket
+  Chiasma.runTmux (TmuxNative socket) prog
+
+runTmuxE ::
+  (MonadIO m, MonadRibo m, MonadMask m, Nvim m) =>
+  TmuxProg (ExceptT TmuxError m) a ->
+  m (Either TmuxError a)
+runTmuxE =
+  runExceptT . runTmux
+
+class RunTmux m where
+  runRiboTmux :: TmuxProg m b -> m b
+
+instance DeepPrisms e TmuxError => RunTmux (Ribo s e) where
+    runRiboTmux = runTmux
diff --git a/lib/Ribosome/Unsafe.hs b/lib/Ribosome/Unsafe.hs
--- a/lib/Ribosome/Unsafe.hs
+++ b/lib/Ribosome/Unsafe.hs
@@ -1,8 +1,9 @@
-module Ribosome.Unsafe(
-  unsafeLog,
-) where
+module Ribosome.Unsafe where
 
 import GHC.IO.Unsafe (unsafePerformIO)
 
-unsafeLog :: Show a => a -> b -> b
-unsafeLog a b = unsafePerformIO $ (print a) >> return b
+unsafeLogS :: Show a => a -> b -> b
+unsafeLogS a b = unsafePerformIO $ print a >> return b
+
+unsafeLog :: Text -> b -> b
+unsafeLog a b = unsafePerformIO $ putStrLn a >> return b
diff --git a/ribosome.cabal b/ribosome.cabal
--- a/ribosome.cabal
+++ b/ribosome.cabal
@@ -1,7 +1,7 @@
 cabal-version: 1.12
 name: ribosome
-version: 0.2.2.0
-license: MIT
+version: 0.3.0.0
+license: OtherLicense
 license-file: LICENSE
 copyright: 2019 Torsten Schmits
 maintainer: tek@tryp.io
@@ -10,7 +10,7 @@
 bug-reports: https://github.com/tek/ribosome-hs/issues
 synopsis: api extensions for nvim-hs
 description:
-    Please see the README on GitHub at <https://github.com/tek/proteome-hs>
+    Please see the README on GitHub at <https://github.com/tek/ribosome-hs>
 category: Neovim
 build-type: Simple
 
@@ -20,53 +20,111 @@
 
 library
     exposed-modules:
+        Ribosome.Api.Atomic
+        Ribosome.Api.Autocmd
         Ribosome.Api.Buffer
         Ribosome.Api.Echo
         Ribosome.Api.Exists
         Ribosome.Api.Function
+        Ribosome.Api.Input
         Ribosome.Api.Option
         Ribosome.Api.Path
+        Ribosome.Api.Process
         Ribosome.Api.Response
         Ribosome.Api.Sleep
+        Ribosome.Api.Syntax
+        Ribosome.Api.Tabpage
+        Ribosome.Api.Variable
         Ribosome.Api.Window
         Ribosome.Config.Setting
         Ribosome.Config.Settings
+        Ribosome.Control.Concurrent.Wait
+        Ribosome.Control.Exception
         Ribosome.Control.Lock
+        Ribosome.Control.Monad.Error
         Ribosome.Control.Monad.Ribo
-        Ribosome.Control.Monad.RiboE
-        Ribosome.Control.Monad.State
-        Ribosome.Control.Monad.Trans.Ribo
-        Ribosome.Control.Monad.Trans.Unlift
-        Ribosome.Control.Ribo
         Ribosome.Control.Ribosome
+        Ribosome.Control.StrictRibosome
+        Ribosome.Data.Conduit
+        Ribosome.Data.Conduit.Composition
         Ribosome.Data.ErrorReport
         Ribosome.Data.Errors
         Ribosome.Data.Foldable
+        Ribosome.Data.Mapping
         Ribosome.Data.Maybe
+        Ribosome.Data.PersistError
         Ribosome.Data.Scratch
         Ribosome.Data.ScratchOptions
-        Ribosome.Data.Time
+        Ribosome.Data.Setting
+        Ribosome.Data.SettingError
+        Ribosome.Data.String
+        Ribosome.Data.Syntax
+        Ribosome.Data.Text
         Ribosome.Error.Report
+        Ribosome.Error.Report.Class
         Ribosome.File
         Ribosome.Internal.IO
         Ribosome.Internal.NvimObject
         Ribosome.Log
-        Ribosome.Monad
+        Ribosome.Mapping
+        Ribosome.Menu.Data.Menu
+        Ribosome.Menu.Data.MenuAction
+        Ribosome.Menu.Data.MenuConfig
+        Ribosome.Menu.Data.MenuConsumer
+        Ribosome.Menu.Data.MenuConsumerAction
+        Ribosome.Menu.Data.MenuConsumerUpdate
+        Ribosome.Menu.Data.MenuEvent
+        Ribosome.Menu.Data.MenuItem
+        Ribosome.Menu.Data.MenuRenderEvent
+        Ribosome.Menu.Data.MenuResult
+        Ribosome.Menu.Data.MenuUpdate
+        Ribosome.Menu.Nvim
+        Ribosome.Menu.Prompt.Data.Codes
+        Ribosome.Menu.Prompt.Data.CursorUpdate
+        Ribosome.Menu.Prompt.Data.InputEvent
+        Ribosome.Menu.Prompt.Data.Prompt
+        Ribosome.Menu.Prompt.Data.PromptConfig
+        Ribosome.Menu.Prompt.Data.PromptConsumed
+        Ribosome.Menu.Prompt.Data.PromptConsumerUpdate
+        Ribosome.Menu.Prompt.Data.PromptEvent
+        Ribosome.Menu.Prompt.Data.PromptRenderer
+        Ribosome.Menu.Prompt.Data.PromptState
+        Ribosome.Menu.Prompt.Data.PromptUpdate
+        Ribosome.Menu.Prompt.Data.TextUpdate
+        Ribosome.Menu.Prompt.Nvim
+        Ribosome.Menu.Prompt.Run
+        Ribosome.Menu.Run
+        Ribosome.Menu.Simple
         Ribosome.Msgpack.Decode
         Ribosome.Msgpack.Encode
+        Ribosome.Msgpack.Error
         Ribosome.Msgpack.NvimObject
         Ribosome.Msgpack.Util
+        Ribosome.Nvim.Api.Data
+        Ribosome.Nvim.Api.Generate
+        Ribosome.Nvim.Api.GenerateData
+        Ribosome.Nvim.Api.GenerateIO
+        Ribosome.Nvim.Api.IO
+        Ribosome.Nvim.Api.RpcCall
+        Ribosome.Orphans
         Ribosome.Persist
+        Ribosome.Plugin
+        Ribosome.Plugin.Builtin
+        Ribosome.Plugin.Mapping
+        Ribosome.Plugin.RpcHandler
+        Ribosome.Plugin.TH
+        Ribosome.Plugin.TH.Command
+        Ribosome.Plugin.TH.Handler
+        Ribosome.Plugin.Watch
+        Ribosome.Prelude
+        Ribosome.PreludeExport
         Ribosome.Scratch
-        Ribosome.Test.Embed
-        Ribosome.Test.Exists
-        Ribosome.Test.File
-        Ribosome.Test.Functional
-        Ribosome.Test.Unit
+        Ribosome.System.Time
+        Ribosome.Tmux.Run
         Ribosome.Unsafe
     hs-source-dirs: lib
     other-modules:
-        Paths_ribosome
+        Prelude
     default-language: Haskell2010
     default-extensions: AutoDeriveTypeable BangPatterns BinaryLiterals
                         ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable
@@ -77,150 +135,58 @@
                         MonadFailDesugaring MultiParamTypeClasses MultiWayIf NamedFieldPuns
                         OverloadedStrings PartialTypeSignatures PatternGuards PolyKinds
                         RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving
-                        TupleSections TypeApplications TypeFamilies TypeSynonymInstances
-                        UnicodeSyntax ViewPatterns
+                        TupleSections TypeApplications TypeFamilies TypeOperators
+                        TypeSynonymInstances UnicodeSyntax ViewPatterns
     build-depends:
         MissingH >=1.4.1.0 && <1.5,
         aeson >=1.3.1.1 && <1.4,
         ansi-terminal >=0.8.2 && <0.9,
-        base >=4.7 && <5,
+        base-noprelude >=4.7 && <5,
         bytestring >=0.10.8.2 && <0.11,
+        cereal >=0.5.7.0 && <0.6,
+        cereal-conduit >=0.8.0 && <0.9,
+        chiasma >=0.1.0.0 && <0.2,
+        composition >=1.0.2.1 && <1.1,
+        composition-extra >=2.0.0 && <2.1,
+        conduit >=1.3.1 && <1.4,
+        conduit-extra >=1.3.0 && <1.4,
         containers >=0.5.11.0 && <0.6,
+        cornea >=0.2.2.0 && <0.3,
         data-default >=0.7.1.1 && <0.8,
         deepseq >=1.4.3.0 && <1.5,
         directory >=1.3.1.5 && <1.4,
         either >=5.0.1 && <5.1,
+        exceptions >=0.10.0 && <0.11,
         filepath >=1.4.2 && <1.5,
+        free >=5.0.2 && <5.1,
         hslogger >=1.2.12 && <1.3,
         lens >=4.16.1 && <4.17,
+        lifted-async >=0.10.0.3 && <0.11,
+        lifted-base >=0.2.3.12 && <0.3,
         messagepack >=0.5.4 && <0.6,
+        monad-control >=1.0.2.3 && <1.1,
+        monad-loops >=0.4.3 && <0.5,
         mtl >=2.2.2 && <2.3,
-        nvim-hs >=1.0.0.3 && <1.1,
+        nvim-hs >=2.1.0.0 && <2.2,
+        path >=0.6.1 && <0.7,
+        path-io >=1.3.3 && <1.4,
         pretty-terminal >=0.1.0.0 && <0.2,
         prettyprinter >=1.2.1 && <1.3,
         prettyprinter-ansi-terminal >=1.1.1.2 && <1.2,
         process >=1.6.3.0 && <1.7,
+        relude >=0.1.1 && <0.2,
         resourcet >=1.2.2 && <1.3,
         safe >=0.3.17 && <0.4,
         split >=0.2.3.3 && <0.3,
         stm >=2.4.5.1 && <2.5,
-        text >=1.2.3.1 && <1.3,
-        time >=1.8.0.2 && <1.9,
-        transformers >=0.5.5.0 && <0.6,
-        typed-process >=0.2.3.0 && <0.3,
-        unix >=2.7.2.2 && <2.8,
-        unliftio >=0.2.9.0 && <0.3,
-        unliftio-core >=0.1.2.0 && <0.2,
-        utf8-string >=1.0.1.1 && <1.1
-
-test-suite ribosome-functional
-    type: exitcode-stdio-1.0
-    main-is: SpecMain.hs
-    hs-source-dirs: test/f
-    other-modules:
-        Paths_ribosome
-    default-language: Haskell2010
-    default-extensions: AutoDeriveTypeable BangPatterns BinaryLiterals
-                        ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable
-                        DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable
-                        DoAndIfThenElse EmptyDataDecls ExistentialQuantification
-                        FlexibleContexts FlexibleInstances FunctionalDependencies GADTs
-                        GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase
-                        MonadFailDesugaring MultiParamTypeClasses MultiWayIf NamedFieldPuns
-                        OverloadedStrings PartialTypeSignatures PatternGuards PolyKinds
-                        RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving
-                        TupleSections TypeApplications TypeFamilies TypeSynonymInstances
-                        UnicodeSyntax ViewPatterns
-    ghc-options: -threaded -rtsopts -with-rtsopts=-N
-    build-depends:
-        HTF >=0.13.2.5 && <0.14,
-        MissingH >=1.4.1.0 && <1.5,
-        aeson >=1.3.1.1 && <1.4,
-        ansi-terminal >=0.8.2 && <0.9,
-        base >=4.7 && <5,
-        bytestring >=0.10.8.2 && <0.11,
-        containers >=0.5.11.0 && <0.6,
-        data-default >=0.7.1.1 && <0.8,
-        deepseq >=1.4.3.0 && <1.5,
-        directory >=1.3.1.5 && <1.4,
-        either >=5.0.1 && <5.1,
-        filepath >=1.4.2 && <1.5,
-        hslogger >=1.2.12 && <1.3,
-        lens >=4.16.1 && <4.17,
-        messagepack >=0.5.4 && <0.6,
-        mtl >=2.2.2 && <2.3,
-        nvim-hs >=1.0.0.3 && <1.1,
-        pretty-terminal >=0.1.0.0 && <0.2,
-        prettyprinter >=1.2.1 && <1.3,
-        prettyprinter-ansi-terminal >=1.1.1.2 && <1.2,
-        process >=1.6.3.0 && <1.7,
-        resourcet >=1.2.2 && <1.3,
-        ribosome -any,
-        safe >=0.3.17 && <0.4,
-        split >=0.2.3.3 && <0.3,
-        stm >=2.4.5.1 && <2.5,
-        text >=1.2.3.1 && <1.3,
-        time >=1.8.0.2 && <1.9,
-        transformers >=0.5.5.0 && <0.6,
-        typed-process >=0.2.3.0 && <0.3,
-        unix >=2.7.2.2 && <2.8,
-        unliftio >=0.2.9.0 && <0.3,
-        unliftio-core >=0.1.2.0 && <0.2,
-        utf8-string >=1.0.1.1 && <1.1
-
-test-suite ribosome-unit
-    type: exitcode-stdio-1.0
-    main-is: SpecMain.hs
-    hs-source-dirs: test/u
-    other-modules:
-        MsgpackSpec
-        RiboSpec
-        RiboTransSpec
-        ScratchSpec
-        Paths_ribosome
-    default-language: Haskell2010
-    default-extensions: AutoDeriveTypeable BangPatterns BinaryLiterals
-                        ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable
-                        DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable
-                        DoAndIfThenElse EmptyDataDecls ExistentialQuantification
-                        FlexibleContexts FlexibleInstances FunctionalDependencies GADTs
-                        GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase
-                        MonadFailDesugaring MultiParamTypeClasses MultiWayIf NamedFieldPuns
-                        OverloadedStrings PartialTypeSignatures PatternGuards PolyKinds
-                        RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving
-                        TupleSections TypeApplications TypeFamilies TypeSynonymInstances
-                        UnicodeSyntax ViewPatterns
-    ghc-options: -threaded -rtsopts -with-rtsopts=-N
-    build-depends:
-        HTF >=0.13.2.5 && <0.14,
-        MissingH >=1.4.1.0 && <1.5,
-        aeson >=1.3.1.1 && <1.4,
-        ansi-terminal >=0.8.2 && <0.9,
-        base >=4.7 && <5,
-        bytestring >=0.10.8.2 && <0.11,
-        containers >=0.5.11.0 && <0.6,
-        data-default >=0.7.1.1 && <0.8,
-        deepseq >=1.4.3.0 && <1.5,
-        directory >=1.3.1.5 && <1.4,
-        either >=5.0.1 && <5.1,
-        filepath >=1.4.2 && <1.5,
-        hslogger >=1.2.12 && <1.3,
-        lens >=4.16.1 && <4.17,
-        messagepack >=0.5.4 && <0.6,
-        mtl >=2.2.2 && <2.3,
-        nvim-hs >=1.0.0.3 && <1.1,
-        pretty-terminal >=0.1.0.0 && <0.2,
-        prettyprinter >=1.2.1 && <1.3,
-        prettyprinter-ansi-terminal >=1.1.1.2 && <1.2,
-        process >=1.6.3.0 && <1.7,
-        resourcet >=1.2.2 && <1.3,
-        ribosome -any,
-        safe >=0.3.17 && <0.4,
-        split >=0.2.3.3 && <0.3,
-        stm >=2.4.5.1 && <2.5,
+        stm-chans >=3.0.0.4 && <3.1,
+        stm-conduit >=4.0.1 && <4.1,
+        template-haskell >=2.13.0.0 && <2.14,
         text >=1.2.3.1 && <1.3,
+        th-abstraction >=0.2.10.0 && <0.3,
         time >=1.8.0.2 && <1.9,
         transformers >=0.5.5.0 && <0.6,
+        transformers-base >=0.4.5.2 && <0.5,
         typed-process >=0.2.3.0 && <0.3,
         unix >=2.7.2.2 && <2.8,
         unliftio >=0.2.9.0 && <0.3,
diff --git a/test/f/SpecMain.hs b/test/f/SpecMain.hs
deleted file mode 100644
--- a/test/f/SpecMain.hs
+++ /dev/null
@@ -1,10 +0,0 @@
-{-# OPTIONS_GHC -F -pgmF htfpp #-}
-
-module Main where
-
-import Test.Framework
-import Test.Framework.BlackBoxTest ()
-
-main :: IO ()
-main = return ()
--- main = htfMain htf_importedTests
diff --git a/test/u/MsgpackSpec.hs b/test/u/MsgpackSpec.hs
deleted file mode 100644
--- a/test/u/MsgpackSpec.hs
+++ /dev/null
@@ -1,98 +0,0 @@
-{-# OPTIONS_GHC -F -pgmF htfpp #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}
-
-module MsgpackSpec(
-  htf_thisModulesTests
-) where
-
-import Data.Either.Combinators (mapLeft)
-import Data.Int (Int64)
-import Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map (fromList)
-import Data.MessagePack (Object(..))
-import Data.Text (Text)
-import Data.Text.Prettyprint.Doc (Doc, layoutPretty, defaultLayoutOptions)
-import Data.Text.Prettyprint.Doc.Render.Terminal (renderStrict, AnsiStyle)
-import GHC.Generics (Generic)
-import Test.Framework
-
-import Ribosome.Msgpack.Decode (MsgpackDecode(..))
-import Ribosome.Msgpack.Encode (MsgpackEncode(..))
-import qualified Ribosome.Msgpack.Util as Util (string)
-
-newtype NT =
-  NT String
-  deriving (Eq, Show, Generic, MsgpackEncode, MsgpackDecode)
-
-data Blob =
-  Blob {
-    key4 :: [[Int]],
-    key5 :: Map Int String,
-    key6 :: NT
-  }
-  deriving (Eq, Show, Generic, MsgpackEncode, MsgpackDecode)
-
-data Prod =
-  Prod String Int
-  deriving (Eq, Show, Generic, MsgpackEncode, MsgpackDecode)
-
-data Dat =
-  Dat {
-    key1 :: Blob,
-    key2 :: Bool,
-    key3 :: Maybe Prod
-  }
-  deriving (Eq, Show, Generic, MsgpackEncode, MsgpackDecode)
-
-dat :: Dat
-dat = Dat (Blob [[1, 2], [3]] (Map.fromList [(1, "1"), (2, "2")]) (NT "nt")) False (Just $ Prod "dat" 27)
-
-os :: String -> Object
-os = Util.string
-
-i :: Int64 -> Object
-i = ObjectInt
-
-encodedBlob :: Object
-encodedBlob =
-  ObjectMap $ Map.fromList [
-    (os "key4", ObjectArray [ObjectArray [i 1, i 2], ObjectArray [i 3]]),
-    (os "key5", ObjectMap $ Map.fromList [(i 1, os "1"), (i 2, os "2")]),
-    (os "key6", os "nt")
-    ]
-
-encoded :: Object
-encoded =
-  ObjectMap $ Map.fromList [
-    (os "key1", encodedBlob),
-    (os "key2", ObjectBool False),
-    (os "key3", ObjectArray [os "dat", i 27])
-    ]
-
-test_encode :: IO ()
-test_encode =
-  assertEqual encoded (toMsgpack dat)
-
-doc2Text :: Either (Doc AnsiStyle) a -> Either Text a
-doc2Text =
-  mapLeft (renderStrict . layoutPretty defaultLayoutOptions)
-
-test_decode :: IO ()
-test_decode =
-  assertEqual (Right dat) (doc2Text $ fromMsgpack encoded)
-
-data Nope =
-  Nope {
-    present :: Int,
-    absent :: Maybe Int
-  }
-  deriving (Eq, Show, Generic, MsgpackDecode)
-
-encodedNope :: Object
-encodedNope =
-  ObjectMap $ Map.fromList [(os "present", i 5)]
-
-test_maybeMissing :: IO ()
-test_maybeMissing =
-  assertEqual (Right (Nope 5 Nothing)) (doc2Text $ fromMsgpack encodedNope)
diff --git a/test/u/RiboSpec.hs b/test/u/RiboSpec.hs
deleted file mode 100644
--- a/test/u/RiboSpec.hs
+++ /dev/null
@@ -1,37 +0,0 @@
-{-# OPTIONS_GHC -F -pgmF htfpp #-}
-
-module RiboSpec(
-  htf_thisModulesTests,
-) where
-
-import Control.Monad.IO.Class (liftIO)
-import Data.Bifunctor (Bifunctor(..))
-import Data.Default (Default(def))
-import Neovim
-import Ribosome.Api.Buffer
-import Ribosome.Control.Monad.Ribo
-import Ribosome.Test.Unit (unitSpec)
-import Test.Framework
-import UnliftIO.STM
-
-newtype Env =
-  Env Int
-  deriving (Eq, Show)
-
-rib :: RiboT Env (Doc AnsiStyle) [String]
-rib = do
-  a <- nvim currentBufferContent
-  nvim $ vim_command' "tabnew"
-  return a
-
-riboSpec :: Ribo (TVar Env) ()
-riboSpec = do
-  let target = ["line 1", "line 2"]
-  _ <- setCurrentBufferContent target
-  content <- unsafeToNeovim $ first (const ()) rib
-  liftIO $ assertEqual target content
-
-test_ribo :: IO ()
-test_ribo = do
-  t <- newTVarIO (Env 5)
-  unitSpec def t riboSpec
diff --git a/test/u/RiboTransSpec.hs b/test/u/RiboTransSpec.hs
deleted file mode 100644
--- a/test/u/RiboTransSpec.hs
+++ /dev/null
@@ -1,38 +0,0 @@
-{-# OPTIONS_GHC -F -pgmF htfpp #-}
-
-module RiboTransSpec(
-  htf_thisModulesTests,
-) where
-
-import Control.Monad.IO.Class
-import Control.Monad.Trans.Reader
-import Data.Default (Default(def))
-import Neovim
-import Test.Framework
-import UnliftIO.STM
-
-import Ribosome.Api.Buffer
-import Ribosome.Control.Monad.Trans.Ribo
-import Ribosome.Test.Unit (unitSpec)
-
-newtype Env =
-  Env Int
-  deriving (Eq, Show)
-
-rib :: RiboT (ReaderT Env) Env (Doc AnsiStyle) [String]
-rib = do
-  a <- nvim currentBufferContent
-  nvim $ vim_command' "tabnew"
-  return a
-
-riboSpec :: Ribo Env ()
-riboSpec = do
-  let target = ["line 1", "line 2"]
-  _ <- setCurrentBufferContent target
-  content <- unsafeToNeovimWith (`runReaderT` Env 6) $ mapE (const ()) rib
-  liftIO $ assertEqual target content
-
-test_riboTrans :: IO ()
-test_riboTrans = do
-  t <- newTVarIO (Env 5)
-  unitSpec def t riboSpec
diff --git a/test/u/ScratchSpec.hs b/test/u/ScratchSpec.hs
deleted file mode 100644
--- a/test/u/ScratchSpec.hs
+++ /dev/null
@@ -1,28 +0,0 @@
-{-# OPTIONS_GHC -F -pgmF htfpp #-}
-
-module ScratchSpec(
-  htf_thisModulesTests
-) where
-
-import Control.Monad.IO.Class (liftIO)
-import Data.Default (def)
-import Test.Framework
-
-import Ribosome.Api.Buffer (currentBufferContent)
-import Ribosome.Control.Ribo (Ribo)
-import Ribosome.Data.ScratchOptions (ScratchOptions(ScratchOptions))
-import Ribosome.Scratch (showInScratch)
-import Ribosome.Test.Unit (unitSpec)
-
-target :: [String]
-target = ["line 1", "line 2"]
-
-scratchSpec :: Ribo e ()
-scratchSpec = do
-  _ <- showInScratch target (ScratchOptions False True (Just 0) False "buffi")
-  content <- currentBufferContent
-  liftIO $ assertEqual target content
-
-test_scratch :: IO ()
-test_scratch =
-  unitSpec def () scratchSpec
diff --git a/test/u/SpecMain.hs b/test/u/SpecMain.hs
deleted file mode 100644
--- a/test/u/SpecMain.hs
+++ /dev/null
@@ -1,12 +0,0 @@
-{-# OPTIONS_GHC -F -pgmF htfpp #-}
-
-module Main where
-
-import {-@ HTF_TESTS @-} ScratchSpec
-import {-@ HTF_TESTS @-} MsgpackSpec
-import {-@ HTF_TESTS @-} RiboSpec
-import Test.Framework
-import Test.Framework.BlackBoxTest ()
-
-main :: IO ()
-main = htfMain htf_importedTests
