Index: global.c
===================================================================
--- global.c	(revision 23fddadbb7a375688e9be9627f1f5ca39aaa54d0)
+++ global.c	(revision df0138f05e0bae986f09d167195c6ce676bac37d)
@@ -113,4 +113,5 @@
   owl_message_init_fmtext_cache();
   owl_list_create(&(g->dispatchlist));
+  owl_list_create(&(g->io_dispatch_list));
   owl_list_create(&(g->psa_list));
   g->timerlist = NULL;
@@ -945,4 +946,9 @@
 }
 
+owl_list *owl_global_get_io_dispatch_list(owl_global *g)
+{
+  return &(g->io_dispatch_list);
+}
+
 owl_list *owl_global_get_psa_list(owl_global *g)
 {
Index: owl.h
===================================================================
--- owl.h	(revision 6337cb5742f397a3db9201557de56be05c9394a1)
+++ owl.h	(revision df0138f05e0bae986f09d167195c6ce676bac37d)
@@ -105,7 +105,7 @@
 #define OWL_MESSAGE_DIRECTION_OUT   2
 
-#define OWL_MUX_READ   1
-#define OWL_MUX_WRITE  2
-#define OWL_MUX_EXCEPT 4
+#define OWL_IO_READ   1
+#define OWL_IO_WRITE  2
+#define OWL_IO_EXCEPT 4
 
 #define OWL_DIRECTION_NONE      0
@@ -508,4 +508,13 @@
   void *data;
 } owl_dispatch;
+
+typedef struct _owl_io_dispatch {
+  int fd;                                     /* FD to watch for dispatch. */
+  int mode;
+  int needs_gc;
+  void (*callback)(const struct _owl_io_dispatch *, void *); /* C function to dispatch to. */
+  void (*destroy)(const struct _owl_io_dispatch *);  /* Destructor */
+  void *data;
+} owl_io_dispatch;
 
 typedef struct _owl_ps_action {
@@ -595,4 +604,5 @@
   owl_obarray obarray;
   owl_list dispatchlist;
+  owl_list io_dispatch_list;
   owl_list psa_list;
   GList *timerlist;
Index: perlconfig.c
===================================================================
--- perlconfig.c	(revision fb7742c26c2c6ef87a9fafbc1dd594987d2ef4c9)
+++ perlconfig.c	(revision df0138f05e0bae986f09d167195c6ce676bac37d)
@@ -510,4 +510,9 @@
 }
 
+void owl_perlconfig_io_dispatch_destroy(const owl_io_dispatch *d)
+{
+  SvREFCNT_dec(d->data);
+}
+
 void owl_perlconfig_edit_callback(owl_editwin *e)
 {
@@ -580,4 +585,29 @@
 }
 
+void owl_perlconfig_io_dispatch(const owl_io_dispatch *d, void *data)
+{
+  SV *cb = data;
+  dSP;
+  if(cb == NULL) {
+    owl_function_error("Perl callback is NULL!");
+    return;
+  }
+
+  ENTER;
+  SAVETMPS;
+
+  PUSHMARK(SP);
+  PUTBACK;
+
+  call_sv(cb, G_DISCARD|G_KEEPERR|G_EVAL);
+
+  if(SvTRUE(ERRSV)) {
+    owl_function_error("%s", SvPV_nolen(ERRSV));
+  }
+
+  FREETMPS;
+  LEAVE;
+}
+
 void owl_perlconfig_perl_timer(owl_timer *t, void *data)
 {
Index: select.c
===================================================================
--- select.c	(revision 4f2166b9a2565b2ccc5fbcbb9869b2a41119a342)
+++ select.c	(revision df0138f05e0bae986f09d167195c6ce676bac37d)
@@ -203,6 +203,4 @@
 
   dl = owl_global_get_dispatchlist(&g);
-  FD_ZERO(r);
-  FD_ZERO(e);
   max_fd = 0;
   len = owl_select_dispatch_count();
@@ -260,4 +258,157 @@
 }
 
+static const owl_io_dispatch *owl_select_find_io_dispatch_by_fd(const int fd)
+{
+  int i, len;
+  const owl_list *dl;
+  owl_io_dispatch *d;
+  dl = owl_global_get_io_dispatch_list(&g);
+  len = owl_list_get_size(dl);
+  for(i = 0; i < len; i++) {
+    d = owl_list_get_element(dl, i);
+    if (d->fd == fd) return d;
+  }
+  return NULL;
+}
+
+static int owl_select_find_io_dispatch(const owl_io_dispatch *in)
+{
+  int i, len;
+  const owl_list *dl;
+
+  if (in != NULL) {
+    dl = owl_global_get_io_dispatch_list(&g);
+    len = owl_list_get_size(dl);
+    for(i = 0; i < len; i++) {
+      const owl_io_dispatch *d = owl_list_get_element(dl, i);
+      if (d == in) return i;
+    }
+  }
+  return -1;
+}
+
+void owl_select_remove_io_dispatch(const owl_io_dispatch *in)
+{
+  int elt;
+  if (in != NULL) {
+    elt = owl_select_find_io_dispatch(in);
+    if (elt != -1) {
+      owl_list *dl = owl_global_get_io_dispatch_list(&g);
+      owl_io_dispatch *d = owl_list_get_element(dl, elt);
+      if (dispatch_active)
+        d->needs_gc = 1;
+      else {
+        owl_list_remove_element(dl, elt);
+        if (d->destroy)
+          d->destroy(d);
+        owl_free(d);
+      }
+    }
+  }
+}
+
+void owl_select_io_dispatch_gc(void)
+{
+  int i;
+  owl_list *dl;
+
+  dl = owl_global_get_io_dispatch_list(&g);
+  /*
+   * Count down so we aren't set off by removing items from the list
+   * during the iteration.
+   */
+  for(i = owl_list_get_size(dl) - 1; i >= 0; i--) {
+    owl_io_dispatch *d = owl_list_get_element(dl, i);
+    if(d->needs_gc) {
+      owl_select_remove_io_dispatch(d);
+    }
+  }
+}
+
+/* Each FD may have at most one dispatcher.
+ * If a new dispatch is added for an FD, the old one is removed.
+ * mode determines what types of events are watched for, and may be any combination of:
+ * OWL_IO_READ, OWL_IO_WRITE, OWL_IO_EXCEPT
+ */
+const owl_io_dispatch *owl_select_add_io_dispatch(int fd, int mode, void (*cb)(const owl_io_dispatch *, void *), void (*destroy)(const owl_io_dispatch *), void *data)
+{
+  owl_io_dispatch *d = owl_malloc(sizeof(owl_io_dispatch));
+  owl_list *dl = owl_global_get_io_dispatch_list(&g);
+
+  d->fd = fd;
+  d->needs_gc = 0;
+  d->mode = mode;
+  d->callback = cb;
+  d->destroy = destroy;
+  d->data = data;
+
+  owl_select_remove_io_dispatch(owl_select_find_io_dispatch_by_fd(fd));
+  owl_list_append_element(dl, d);
+
+  return d;
+}
+
+int owl_select_prepare_io_dispatch_fd_sets(fd_set *rfds, fd_set *wfds, fd_set *efds) {
+  int i, len, max_fd;
+  owl_io_dispatch *d;
+  owl_list *dl = owl_global_get_io_dispatch_list(&g);
+
+  max_fd = 0;
+  len = owl_list_get_size(dl);
+  for (i = 0; i < len; i++) {
+    d = owl_list_get_element(dl, i);
+    if (d->mode & (OWL_IO_READ | OWL_IO_WRITE | OWL_IO_EXCEPT)) {
+      if (max_fd < d->fd) max_fd = d->fd;
+      if (d->mode & OWL_IO_READ) FD_SET(d->fd, rfds);
+      if (d->mode & OWL_IO_WRITE) FD_SET(d->fd, wfds);
+      if (d->mode & OWL_IO_EXCEPT) FD_SET(d->fd, efds);
+    }
+  }
+  return max_fd + 1;
+}
+
+void owl_select_io_dispatch(const fd_set *rfds, const fd_set *wfds, const fd_set *efds, const int max_fd)
+{
+  int i, len;
+  owl_io_dispatch *d;
+  owl_list *dl = owl_global_get_io_dispatch_list(&g);
+
+  dispatch_active = 1;
+  len = owl_list_get_size(dl);
+  for (i = 0; i < len; i++) {
+    d = owl_list_get_element(dl, i);
+    if (d->fd < max_fd && d->callback != NULL &&
+        ((d->mode & OWL_IO_READ && FD_ISSET(d->fd, rfds)) ||
+         (d->mode & OWL_IO_WRITE && FD_ISSET(d->fd, wfds)) ||
+         (d->mode & OWL_IO_EXCEPT && FD_ISSET(d->fd, efds)))) {
+      d->callback(d, d->data);
+    }
+  }
+  dispatch_active = 0;
+  owl_select_io_dispatch_gc();
+}
+
+int owl_select_add_perl_io_dispatch(int fd, int mode, SV *cb)
+{
+  const owl_io_dispatch *d = owl_select_find_io_dispatch_by_fd(fd);
+  if (d != NULL && d->callback != owl_perlconfig_io_dispatch) {
+    /* Don't mess with non-perl dispatch functions from here. */
+    return 1;
+  }
+  owl_select_add_io_dispatch(fd, mode, owl_perlconfig_io_dispatch, owl_perlconfig_io_dispatch_destroy, cb);
+  return 0;
+}
+
+int owl_select_remove_perl_io_dispatch(int fd)
+{
+  const owl_io_dispatch *d = owl_select_find_io_dispatch_by_fd(fd);
+  if (d != NULL && d->callback == owl_perlconfig_io_dispatch) {
+    /* Only remove perl io dispatchers from here. */
+    owl_select_remove_io_dispatch(d);
+    return 0;
+  }
+  return 1;
+}
+
 int owl_select_aim_hack(fd_set *rfds, fd_set *wfds)
 {
@@ -266,6 +417,4 @@
   int max_fd;
 
-  FD_ZERO(rfds);
-  FD_ZERO(wfds);
   max_fd = 0;
   sess = owl_global_get_aimsess(&g);
@@ -414,6 +563,7 @@
 void owl_select(void)
 {
-  int i, max_fd, aim_max_fd, aim_done, ret;
+  int i, max_fd, max_fd2, aim_done, ret;
   fd_set r;
+  fd_set w;
   fd_set e;
   fd_set aim_rfds, aim_wfds;
@@ -430,6 +580,11 @@
     return;
   }
+  FD_ZERO(&r);
+  FD_ZERO(&w);
+  FD_ZERO(&e);
 
   max_fd = owl_select_dispatch_prepare_fd_sets(&r, &e);
+  max_fd2 = owl_select_prepare_io_dispatch_fd_sets(&r, &w, &e);
+  if (max_fd < max_fd2) max_fd = max_fd2;
 
   /* AIM HACK: 
@@ -448,11 +603,15 @@
   if (owl_global_is_doaimevents(&g)) {
     aim_done = 0;
-    aim_max_fd = owl_select_aim_hack(&aim_rfds, &aim_wfds);
-    if (max_fd < aim_max_fd) max_fd = aim_max_fd;
-    for(i = 0; i <= aim_max_fd; i++) {
+    max_fd2 = owl_select_aim_hack(&aim_rfds, &aim_wfds);
+    if (max_fd < max_fd2) max_fd = max_fd2;
+    for(i = 0; i <= max_fd2; i++) {
       if (FD_ISSET(i, &aim_rfds)) {
         FD_SET(i, &r);
         FD_SET(i, &e);
       }
+      if (FD_ISSET(i, &aim_wfds)) {
+        FD_SET(i, &w);
+        FD_SET(i, &e);
+      }
     }
   }
@@ -464,5 +623,5 @@
   }
 
-  ret = pselect(max_fd+1, &r, &aim_wfds, &e, &timeout, &mask);
+  ret = pselect(max_fd+1, &r, &w, &e, &timeout, &mask);
 
   if(ret < 0 && errno == EINTR) {
@@ -482,5 +641,5 @@
       /* Merge all interesting FDs into one set, since we have a
          single dispatch per FD. */
-      if (FD_ISSET(i, &r) || FD_ISSET(i, &aim_wfds) || FD_ISSET(i, &e)) {
+      if (FD_ISSET(i, &r) || FD_ISSET(i, &w) || FD_ISSET(i, &e)) {
         /* AIM HACK: no separate dispatch, just process here if
            needed, and only once per run through. */
@@ -497,4 +656,5 @@
        and read ready FDs. */
     owl_select_dispatch(&r, max_fd);
-  }
-}
+    owl_select_io_dispatch(&r, &w, &e, max_fd);
+  }
+}
